logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma
commit: 8b6221d4ecd1d7e354e7de831dd46e285cb85077
parent eea879eb362d3310d4fe047fb6412b69dd8711fe
Author: feld <feld@feld.me>
Date:   Tue, 13 Oct 2020 14:47:29 +0000

Merge branch 'feature/1822-files-consistency' into 'develop'

Feature/1822 files consistency

Closes #1822

See merge request pleroma/pleroma!2680

Diffstat:

M.credo.exs6++----
Mconfig/config.exs2+-
Mconfig/description.exs4++--
Mconfig/test.exs2+-
Mcoveralls.json3++-
Mdocs/configuration/cheatsheet.md14++++----------
Mdocs/dev.md4++--
Alib/credo/check/consistency/file_location.ex166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rlib/mix/tasks/pleroma/ecto/ecto.ex -> lib/mix/tasks/pleroma/ecto.ex0
Rlib/mix/tasks/pleroma/robotstxt.ex -> lib/mix/tasks/pleroma/robots_txt.ex0
Rlib/transports.ex -> lib/phoenix/transports/web_socket/raw.ex0
Mlib/pleroma/application.ex4++--
Mlib/pleroma/bbs/authenticator.ex2+-
Rlib/pleroma/captcha/captcha.ex -> lib/pleroma/captcha.ex0
Rlib/pleroma/captcha/captcha_service.ex -> lib/pleroma/captcha/service.ex0
Mlib/pleroma/config/deprecation_warnings.ex19++++++++++++++++++-
Rlib/pleroma/config/config_db.ex -> lib/pleroma/config_db.ex0
Rlib/pleroma/conversation/participation_recipient_ship.ex -> lib/pleroma/conversation/participation/recipient_ship.ex0
Rlib/pleroma/gun/gun.ex -> lib/pleroma/gun.ex0
Rlib/pleroma/http/http.ex -> lib/pleroma/http.ex0
Dlib/pleroma/plugs/admin_secret_authentication_plug.ex60------------------------------------------------------------
Dlib/pleroma/plugs/authentication_plug.ex80-------------------------------------------------------------------------------
Dlib/pleroma/plugs/basic_auth_decoder_plug.ex25-------------------------
Dlib/pleroma/plugs/cache.ex136-------------------------------------------------------------------------------
Dlib/pleroma/plugs/ensure_authenticated_plug.ex41-----------------------------------------
Dlib/pleroma/plugs/ensure_public_or_authenticated_plug.ex35-----------------------------------
Dlib/pleroma/plugs/ensure_user_key_plug.ex18------------------
Dlib/pleroma/plugs/expect_authenticated_check_plug.ex20--------------------
Dlib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex21---------------------
Dlib/pleroma/plugs/federating_plug.ex32--------------------------------
Dlib/pleroma/plugs/frontend_static.ex55-------------------------------------------------------
Dlib/pleroma/plugs/http_security_plug.ex225-------------------------------------------------------------------------------
Dlib/pleroma/plugs/idempotency_plug.ex84-------------------------------------------------------------------------------
Dlib/pleroma/plugs/instance_static.ex53-----------------------------------------------------
Dlib/pleroma/plugs/legacy_authentication_plug.ex42------------------------------------------
Dlib/pleroma/plugs/oauth_plug.ex120-------------------------------------------------------------------------------
Dlib/pleroma/plugs/oauth_scopes_plug.ex77-----------------------------------------------------------------------------
Dlib/pleroma/plugs/plug_helper.ex40----------------------------------------
Dlib/pleroma/plugs/rate_limiter/limiter_supervisor.ex54------------------------------------------------------
Dlib/pleroma/plugs/rate_limiter/rate_limiter.ex267-------------------------------------------------------------------------------
Dlib/pleroma/plugs/rate_limiter/supervisor.ex20--------------------
Dlib/pleroma/plugs/remote_ip.ex48------------------------------------------------
Dlib/pleroma/plugs/session_authentication_plug.ex21---------------------
Dlib/pleroma/plugs/set_format_plug.ex24------------------------
Dlib/pleroma/plugs/set_locale_plug.ex63---------------------------------------------------------------
Dlib/pleroma/plugs/set_user_session_id_plug.ex19-------------------
Dlib/pleroma/plugs/static_fe_plug.ex26--------------------------
Dlib/pleroma/plugs/trailing_format_plug.ex42------------------------------------------
Dlib/pleroma/plugs/uploaded_media.ex107-------------------------------------------------------------------------------
Dlib/pleroma/plugs/user_enabled_plug.ex23-----------------------
Dlib/pleroma/plugs/user_fetcher_plug.ex21---------------------
Dlib/pleroma/plugs/user_is_admin_plug.ex24------------------------
Rlib/pleroma/reverse_proxy/reverse_proxy.ex -> lib/pleroma/reverse_proxy.ex0
Mlib/pleroma/tests/auth_test_controller.ex4++--
Mlib/pleroma/uploaders/uploader.ex2+-
Alib/pleroma/web.ex234+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/pleroma/web/activity_pub/activity_pub_controller.ex6+++---
Mlib/pleroma/web/admin_api/controllers/admin_api_controller.ex2+-
Mlib/pleroma/web/admin_api/controllers/chat_controller.ex2+-
Mlib/pleroma/web/admin_api/controllers/config_controller.ex2+-
Mlib/pleroma/web/admin_api/controllers/instance_document_controller.ex4++--
Mlib/pleroma/web/admin_api/controllers/invite_controller.ex2+-
Mlib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex2+-
Alib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dlib/pleroma/web/admin_api/controllers/oauth_app_controller.ex77-----------------------------------------------------------------------------
Mlib/pleroma/web/admin_api/controllers/relay_controller.ex2+-
Mlib/pleroma/web/admin_api/controllers/report_controller.ex2+-
Mlib/pleroma/web/admin_api/controllers/status_controller.ex2+-
Rlib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex -> lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex0
Mlib/pleroma/web/auth/pleroma_authenticator.ex2+-
Mlib/pleroma/web/auth/totp_authenticator.ex2+-
Rlib/pleroma/web/common_api/common_api.ex -> lib/pleroma/web/common_api.ex0
Mlib/pleroma/web/common_api/utils.ex2+-
Mlib/pleroma/web/endpoint.ex16++++++++--------
Alib/pleroma/web/fallback/redirect_controller.ex108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dlib/pleroma/web/fallback_redirect_controller.ex108-------------------------------------------------------------------------------
Rlib/pleroma/web/fed_sockets/fed_sockets.ex -> lib/pleroma/web/fed_sockets.ex0
Rlib/pleroma/web/federator/federator.ex -> lib/pleroma/web/federator.ex0
Mlib/pleroma/web/feed/user_controller.ex9++++-----
Mlib/pleroma/web/masto_fe_controller.ex4++--
Mlib/pleroma/web/mastodon_api/controllers/account_controller.ex6+++---
Mlib/pleroma/web/mastodon_api/controllers/app_controller.ex4++--
Mlib/pleroma/web/mastodon_api/controllers/auth_controller.ex2+-
Mlib/pleroma/web/mastodon_api/controllers/conversation_controller.ex2+-
Mlib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex2+-
Mlib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex2+-
Mlib/pleroma/web/mastodon_api/controllers/filter_controller.ex2+-
Mlib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex2+-
Mlib/pleroma/web/mastodon_api/controllers/instance_controller.ex2+-
Mlib/pleroma/web/mastodon_api/controllers/list_controller.ex2+-
Mlib/pleroma/web/mastodon_api/controllers/marker_controller.ex2+-
Mlib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex2+-
Mlib/pleroma/web/mastodon_api/controllers/media_controller.ex2+-
Mlib/pleroma/web/mastodon_api/controllers/notification_controller.ex2+-
Mlib/pleroma/web/mastodon_api/controllers/poll_controller.ex2+-
Mlib/pleroma/web/mastodon_api/controllers/report_controller.ex4+---
Mlib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex2+-
Mlib/pleroma/web/mastodon_api/controllers/search_controller.ex4++--
Mlib/pleroma/web/mastodon_api/controllers/status_controller.ex10+++++++---
Mlib/pleroma/web/mastodon_api/controllers/subscription_controller.ex2+-
Mlib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex2+-
Mlib/pleroma/web/mastodon_api/controllers/timeline_controller.ex6+++---
Rlib/pleroma/web/media_proxy/media_proxy.ex -> lib/pleroma/web/media_proxy.ex0
Rlib/pleroma/web/media_proxy/invalidations/http.ex -> lib/pleroma/web/media_proxy/invalidation/http.ex0
Rlib/pleroma/web/media_proxy/invalidations/script.ex -> lib/pleroma/web/media_proxy/invalidation/script.ex0
Rlib/pleroma/web/metadata/feed.ex -> lib/pleroma/web/metadata/providers/feed.ex0
Rlib/pleroma/web/metadata/opengraph.ex -> lib/pleroma/web/metadata/providers/open_graph.ex0
Rlib/pleroma/web/metadata/provider.ex -> lib/pleroma/web/metadata/providers/provider.ex0
Rlib/pleroma/web/metadata/rel_me.ex -> lib/pleroma/web/metadata/providers/rel_me.ex0
Rlib/pleroma/web/metadata/restrict_indexing.ex -> lib/pleroma/web/metadata/providers/restrict_indexing.ex0
Rlib/pleroma/web/metadata/twitter_card.ex -> lib/pleroma/web/metadata/providers/twitter_card.ex0
Alib/pleroma/web/mongoose_im/mongoose_im_controller.ex46++++++++++++++++++++++++++++++++++++++++++++++
Dlib/pleroma/web/mongooseim/mongoose_im_controller.ex46----------------------------------------------
Rlib/pleroma/web/oauth.ex -> lib/pleroma/web/o_auth.ex0
Rlib/pleroma/web/oauth/app.ex -> lib/pleroma/web/o_auth/app.ex0
Rlib/pleroma/web/oauth/authorization.ex -> lib/pleroma/web/o_auth/authorization.ex0
Rlib/pleroma/web/oauth/fallback_controller.ex -> lib/pleroma/web/o_auth/fallback_controller.ex0
Rlib/pleroma/web/oauth/mfa_controller.ex -> lib/pleroma/web/o_auth/mfa_controller.ex0
Rlib/pleroma/web/oauth/mfa_view.ex -> lib/pleroma/web/o_auth/mfa_view.ex0
Alib/pleroma/web/o_auth/o_auth_controller.ex613+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rlib/pleroma/web/oauth/oauth_view.ex -> lib/pleroma/web/o_auth/o_auth_view.ex0
Alib/pleroma/web/o_auth/scopes.ex76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rlib/pleroma/web/oauth/token.ex -> lib/pleroma/web/o_auth/token.ex0
Rlib/pleroma/web/oauth/token/query.ex -> lib/pleroma/web/o_auth/token/query.ex0
Rlib/pleroma/web/oauth/token/strategy/refresh_token.ex -> lib/pleroma/web/o_auth/token/strategy/refresh_token.ex0
Rlib/pleroma/web/oauth/token/strategy/revoke.ex -> lib/pleroma/web/o_auth/token/strategy/revoke.ex0
Rlib/pleroma/web/oauth/token/utils.ex -> lib/pleroma/web/o_auth/token/utils.ex0
Alib/pleroma/web/o_status/o_status_controller.ex151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dlib/pleroma/web/oauth/oauth_controller.ex610------------------------------------------------------------------------------
Dlib/pleroma/web/oauth/scopes.ex76----------------------------------------------------------------------------
Dlib/pleroma/web/ostatus/ostatus_controller.ex151------------------------------------------------------------------------------
Mlib/pleroma/web/pleroma_api/controllers/account_controller.ex6+++---
Mlib/pleroma/web/pleroma_api/controllers/chat_controller.ex2+-
Mlib/pleroma/web/pleroma_api/controllers/conversation_controller.ex2+-
Mlib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex2+-
Mlib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex9++++++---
Mlib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex2+-
Mlib/pleroma/web/pleroma_api/controllers/mascot_controller.ex2+-
Mlib/pleroma/web/pleroma_api/controllers/notification_controller.ex8++++++--
Mlib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex2+-
Mlib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex2+-
Mlib/pleroma/web/pleroma_api/controllers/user_import_controller.ex2+-
Alib/pleroma/web/plug.ex8++++++++
Alib/pleroma/web/plugs/admin_secret_authentication_plug.ex60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/plugs/authentication_plug.ex79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/plugs/basic_auth_decoder_plug.ex25+++++++++++++++++++++++++
Alib/pleroma/web/plugs/cache.ex136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rlib/pleroma/plugs/digest.ex -> lib/pleroma/web/plugs/digest_plug.ex0
Alib/pleroma/web/plugs/ensure_authenticated_plug.ex41+++++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex35+++++++++++++++++++++++++++++++++++
Alib/pleroma/web/plugs/ensure_user_key_plug.ex18++++++++++++++++++
Alib/pleroma/web/plugs/expect_authenticated_check_plug.ex20++++++++++++++++++++
Alib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex21+++++++++++++++++++++
Alib/pleroma/web/plugs/federating_plug.ex32++++++++++++++++++++++++++++++++
Alib/pleroma/web/plugs/frontend_static.ex55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/plugs/http_security_plug.ex225+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rlib/pleroma/plugs/http_signature.ex -> lib/pleroma/web/plugs/http_signature_plug.ex0
Alib/pleroma/web/plugs/idempotency_plug.ex84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/plugs/instance_static.ex53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/plugs/legacy_authentication_plug.ex41+++++++++++++++++++++++++++++++++++++++++
Rlib/pleroma/plugs/mapped_signature_to_identity_plug.ex -> lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex0
Alib/pleroma/web/plugs/o_auth_plug.ex120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/plugs/o_auth_scopes_plug.ex77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/plugs/plug_helper.ex40++++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/plugs/rate_limiter.ex267+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/plugs/rate_limiter/supervisor.ex20++++++++++++++++++++
Alib/pleroma/web/plugs/remote_ip.ex48++++++++++++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/plugs/session_authentication_plug.ex21+++++++++++++++++++++
Alib/pleroma/web/plugs/set_format_plug.ex24++++++++++++++++++++++++
Alib/pleroma/web/plugs/set_locale_plug.ex63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/plugs/set_user_session_id_plug.ex19+++++++++++++++++++
Alib/pleroma/web/plugs/static_fe_plug.ex26++++++++++++++++++++++++++
Alib/pleroma/web/plugs/trailing_format_plug.ex42++++++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/plugs/uploaded_media.ex107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/plugs/user_enabled_plug.ex23+++++++++++++++++++++++
Alib/pleroma/web/plugs/user_fetcher_plug.ex21+++++++++++++++++++++
Alib/pleroma/web/plugs/user_is_admin_plug.ex24++++++++++++++++++++++++
Dlib/pleroma/web/preload/instance.ex59-----------------------------------------------------------
Alib/pleroma/web/preload/providers/instance.ex59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rlib/pleroma/web/preload/provider.ex -> lib/pleroma/web/preload/providers/provider.ex0
Rlib/pleroma/web/preload/timelines.ex -> lib/pleroma/web/preload/providers/timelines.ex0
Rlib/pleroma/web/preload/user.ex -> lib/pleroma/web/preload/providers/user.ex0
Rlib/pleroma/web/push/push.ex -> lib/pleroma/web/push.ex0
Alib/pleroma/web/rich_media/parser/ttl.ex7+++++++
Alib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex50++++++++++++++++++++++++++++++++++++++++++++++++++
Rlib/pleroma/web/rich_media/parsers/oembed_parser.ex -> lib/pleroma/web/rich_media/parsers/o_embed.ex0
Dlib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex50--------------------------------------------------
Dlib/pleroma/web/rich_media/parsers/ttl/ttl.ex7-------
Mlib/pleroma/web/router.ex46+++++++++++++++++++++++-----------------------
Mlib/pleroma/web/static_fe/static_fe_controller.ex4++--
Alib/pleroma/web/streamer.ex331+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dlib/pleroma/web/streamer/streamer.ex331-------------------------------------------------------------------------------
Alib/pleroma/web/twitter_api/controller.ex100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex5++---
Mlib/pleroma/web/twitter_api/controllers/util_controller.ex4++--
Dlib/pleroma/web/twitter_api/twitter_api_controller.ex100-------------------------------------------------------------------------------
Dlib/pleroma/web/web.ex239-------------------------------------------------------------------------------
Rlib/pleroma/web/web_finger/web_finger.ex -> lib/pleroma/web/web_finger.ex0
Mlib/pleroma/web/web_finger/web_finger_controller.ex4++--
Rlib/pleroma/web/xml/xml.ex -> lib/pleroma/web/xml.ex0
Rlib/xml_builder.ex -> lib/pleroma/xml_builder.ex0
Apriv/repo/migrations/20200919182636_remoteip_plug_rename.exs19+++++++++++++++++++
Dtest/application_requirements_test.exs146-------------------------------------------------------------------------------
Dtest/config/deprecation_warnings_test.exs140-------------------------------------------------------------------------------
Mtest/fixtures/modules/runtime_module.ex2+-
Dtest/mfa/backup_codes_test.exs15---------------
Dtest/mfa/totp_test.exs21---------------------
Dtest/mfa_test.exs52----------------------------------------------------
Dtest/migration_helper/notification_backfill_test.exs56--------------------------------------------------------
Dtest/migrations/20200716195806_autolinker_to_linkify_test.exs72------------------------------------------------------------------------
Dtest/migrations/20200722185515_fix_malformed_formatter_config_test.exs70----------------------------------------------------------------------
Dtest/migrations/20200724133313_move_welcome_settings_test.exs144-------------------------------------------------------------------------------
Dtest/migrations/20200802170532_fix_legacy_tags_test.exs28----------------------------
Atest/mix/pleroma_test.exs50++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/mix/tasks/pleroma/app_test.exs65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/mix/tasks/pleroma/config_test.exs189+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/mix/tasks/pleroma/count_statuses_test.exs39+++++++++++++++++++++++++++++++++++++++
Atest/mix/tasks/pleroma/database_test.exs175+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/mix/tasks/pleroma/digest_test.exs60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/mix/tasks/pleroma/ecto/migrate_test.exs20++++++++++++++++++++
Atest/mix/tasks/pleroma/ecto/rollback_test.exs20++++++++++++++++++++
Atest/mix/tasks/pleroma/ecto_test.exs15+++++++++++++++
Atest/mix/tasks/pleroma/email_test.exs127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/mix/tasks/pleroma/emoji_test.exs243+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/mix/tasks/pleroma/frontend_test.exs85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/mix/tasks/pleroma/instance_test.exs99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/mix/tasks/pleroma/refresh_counter_cache_test.exs43+++++++++++++++++++++++++++++++++++++++++++
Atest/mix/tasks/pleroma/relay_test.exs180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/mix/tasks/pleroma/robots_txt_test.exs45+++++++++++++++++++++++++++++++++++++++++++++
Atest/mix/tasks/pleroma/uploads_test.exs56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/mix/tasks/pleroma/user_test.exs614+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtest/moderation_log_test.exs297-------------------------------------------------------------------------------
Dtest/notification_test.exs1144-------------------------------------------------------------------------------
Dtest/object/containment_test.exs125-------------------------------------------------------------------------------
Dtest/object/fetcher_test.exs245-------------------------------------------------------------------------------
Dtest/object_test.exs402-------------------------------------------------------------------------------
Dtest/otp_version_test.exs42------------------------------------------
Dtest/pagination_test.exs92-------------------------------------------------------------------------------
Rtest/activity/ir/topics_test.exs -> test/pleroma/activity/ir/topics_test.exs0
Rtest/activity_test.exs -> test/pleroma/activity_test.exs0
Atest/pleroma/application_requirements_test.exs149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rtest/bbs/handler_test.exs -> test/pleroma/bbs/handler_test.exs0
Rtest/bookmark_test.exs -> test/pleroma/bookmark_test.exs0
Rtest/captcha_test.exs -> test/pleroma/captcha_test.exs0
Rtest/chat/message_reference_test.exs -> test/pleroma/chat/message_reference_test.exs0
Rtest/chat_test.exs -> test/pleroma/chat_test.exs0
Atest/pleroma/config/deprecation_warnings_test.exs140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rtest/config/holder_test.exs -> test/pleroma/config/holder_test.exs0
Rtest/config/loader_test.exs -> test/pleroma/config/loader_test.exs0
Rtest/config/transfer_task_test.exs -> test/pleroma/config/transfer_task_test.exs0
Rtest/config/config_db_test.exs -> test/pleroma/config_db_test.exs0
Rtest/config_test.exs -> test/pleroma/config_test.exs0
Rtest/conversation/participation_test.exs -> test/pleroma/conversation/participation_test.exs0
Rtest/conversation_test.exs -> test/pleroma/conversation_test.exs0
Rtest/docs/generator_test.exs -> test/pleroma/docs/generator_test.exs0
Rtest/earmark_renderer_test.exs -> test/pleroma/earmark_renderer_test.exs0
Atest/pleroma/ecto_type/activity_pub/object_validators/date_time_test.exs36++++++++++++++++++++++++++++++++++++
Atest/pleroma/ecto_type/activity_pub/object_validators/object_id_test.exs41+++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/ecto_type/activity_pub/object_validators/recipients_test.exs31+++++++++++++++++++++++++++++++
Atest/pleroma/ecto_type/activity_pub/object_validators/safe_text_test.exs30++++++++++++++++++++++++++++++
Rtest/emails/admin_email_test.exs -> test/pleroma/emails/admin_email_test.exs0
Rtest/emails/mailer_test.exs -> test/pleroma/emails/mailer_test.exs0
Rtest/emails/user_email_test.exs -> test/pleroma/emails/user_email_test.exs0
Rtest/emoji/formatter_test.exs -> test/pleroma/emoji/formatter_test.exs0
Rtest/emoji/loader_test.exs -> test/pleroma/emoji/loader_test.exs0
Rtest/emoji/pack_test.exs -> test/pleroma/emoji/pack_test.exs0
Rtest/emoji_test.exs -> test/pleroma/emoji_test.exs0
Rtest/filter_test.exs -> test/pleroma/filter_test.exs0
Rtest/following_relationship_test.exs -> test/pleroma/following_relationship_test.exs0
Rtest/formatter_test.exs -> test/pleroma/formatter_test.exs0
Rtest/gun/conneciton_pool_test.exs -> test/pleroma/gun/connection_pool_test.exs0
Rtest/healthcheck_test.exs -> test/pleroma/healthcheck_test.exs0
Rtest/html_test.exs -> test/pleroma/html_test.exs0
Rtest/http/adapter_helper/gun_test.exs -> test/pleroma/http/adapter_helper/gun_test.exs0
Rtest/http/adapter_helper/hackney_test.exs -> test/pleroma/http/adapter_helper/hackney_test.exs0
Rtest/http/adapter_helper_test.exs -> test/pleroma/http/adapter_helper_test.exs0
Rtest/http/ex_aws_test.exs -> test/pleroma/http/ex_aws_test.exs0
Rtest/http/request_builder_test.exs -> test/pleroma/http/request_builder_test.exs0
Rtest/http/tzdata_test.exs -> test/pleroma/http/tzdata_test.exs0
Rtest/http_test.exs -> test/pleroma/http_test.exs0
Atest/pleroma/instances/instance_test.exs152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/instances_test.exs124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rtest/federation/federation_test.exs -> test/pleroma/integration/federation_test.exs0
Rtest/integration/mastodon_websocket_test.exs -> test/pleroma/integration/mastodon_websocket_test.exs0
Rtest/job_queue_monitor_test.exs -> test/pleroma/job_queue_monitor_test.exs0
Rtest/keys_test.exs -> test/pleroma/keys_test.exs0
Rtest/list_test.exs -> test/pleroma/list_test.exs0
Rtest/marker_test.exs -> test/pleroma/marker_test.exs0
Atest/pleroma/mfa/backup_codes_test.exs15+++++++++++++++
Atest/pleroma/mfa/totp_test.exs21+++++++++++++++++++++
Atest/pleroma/mfa_test.exs52++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/migration_helper/notification_backfill_test.exs56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/moderation_log_test.exs297+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/notification_test.exs1144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/object/containment_test.exs125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/object/fetcher_test.exs245+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/object_test.exs402+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/otp_version_test.exs42++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/pagination_test.exs92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/registration_test.exs59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/repo/migrations/autolinker_to_linkify_test.exs72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/repo/migrations/fix_legacy_tags_test.exs28++++++++++++++++++++++++++++
Atest/pleroma/repo/migrations/fix_malformed_formatter_config_test.exs70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/repo/migrations/move_welcome_settings_test.exs144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/repo_test.exs80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/report_note_test.exs16++++++++++++++++
Atest/pleroma/reverse_proxy_test.exs336+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/runtime_test.exs12++++++++++++
Atest/pleroma/safe_jsonb_set_test.exs16++++++++++++++++
Atest/pleroma/scheduled_activity_test.exs105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/signature_test.exs134+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/stats_test.exs122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/upload/filter/anonymize_filename_test.exs42++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/upload/filter/dedupe_test.exs32++++++++++++++++++++++++++++++++
Atest/pleroma/upload/filter/exiftool_test.exs42++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/upload/filter/mogrifun_test.exs44++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/upload/filter/mogrify_test.exs41+++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/upload/filter_test.exs33+++++++++++++++++++++++++++++++++
Atest/pleroma/upload_test.exs287+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/uploaders/local_test.exs55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/uploaders/s3_test.exs88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/user/import_test.exs76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/user/notification_setting_test.exs21+++++++++++++++++++++
Atest/pleroma/user/query_test.exs37+++++++++++++++++++++++++++++++++++++
Atest/pleroma/user/welcome_chat_message_test.exs35+++++++++++++++++++++++++++++++++++
Atest/pleroma/user/welcome_email_test.exs61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/user/welcome_message_test.exs34++++++++++++++++++++++++++++++++++
Atest/pleroma/user_invite_token_test.exs96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/user_relationship_test.exs130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/user_search_test.exs362+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/user_test.exs2124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/utils_test.exs15+++++++++++++++
Atest/pleroma/web/activity_pub/activity_pub_controller_test.exs1550+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/activity_pub_test.exs2260+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/activity_expiration_policy_test.exs84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/anti_followbot_policy_test.exs72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/anti_link_spam_policy_test.exs166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/ensure_re_prepended_test.exs92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/hellthread_policy_test.exs92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/keyword_policy_test.exs225+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/media_proxy_warming_policy_test.exs53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/mention_policy_test.exs96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/no_placeholder_text_policy_test.exs37+++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/normalize_markup_test.exs42++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/object_age_policy_test.exs148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/reject_non_public_test.exs100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/simple_policy_test.exs539+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/subchain_policy_test.exs33+++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/tag_policy_test.exs123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/user_allow_list_policy_test.exs31+++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf/vocabulary_policy_test.exs106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/mrf_test.exs90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/object_validators/accept_validation_test.exs56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/object_validators/announce_validation_test.exs106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/object_validators/article_note_validator_test.exs35+++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/object_validators/block_validation_test.exs39+++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/object_validators/chat_validation_test.exs212+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/object_validators/delete_validation_test.exs106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/object_validators/follow_validation_test.exs26++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/object_validators/like_validation_test.exs113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/object_validators/reject_validation_test.exs56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/object_validators/undo_handling_test.exs53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/object_validators/update_handling_test.exs44++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/pipeline_test.exs179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/publisher_test.exs365+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/relay_test.exs168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/side_effects_test.exs639+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier/accept_handling_test.exs91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier/article_handling_test.exs75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier/audio_handling_test.exs83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier/block_handling_test.exs63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier/chat_message_test.exs171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier/delete_handling_test.exs114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier/event_handling_test.exs40++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier/follow_handling_test.exs208+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs176+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier/reject_handling_test.exs67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier/undo_handling_test.exs185+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier/user_update_handling_test.exs159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier/video_handling_test.exs93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/transmogrifier_test.exs1220++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/utils_test.exs548+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/views/object_view_test.exs84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/views/user_view_test.exs180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/activity_pub/visibility_test.exs230+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/admin_api/controllers/admin_api_controller_test.exs2034+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/admin_api/controllers/chat_controller_test.exs219+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/admin_api/controllers/config_controller_test.exs1465+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/admin_api/controllers/instance_document_controller_test.exs106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/admin_api/controllers/invite_controller_test.exs281+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/admin_api/controllers/media_proxy_cache_controller_test.exs167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs220+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/admin_api/controllers/relay_controller_test.exs99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/admin_api/controllers/report_controller_test.exs372+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/admin_api/controllers/status_controller_test.exs202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/admin_api/search_test.exs190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/admin_api/views/report_view_test.exs146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/api_spec/schema_examples_test.exs43+++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/auth/auth_controller_test.exs242+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/auth/authenticator_test.exs42++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/auth/basic_auth_test.exs46++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/auth/pleroma_authenticator_test.exs48++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/auth/totp_authenticator_test.exs51+++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/chat_channel_test.exs41+++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/common_api/utils_test.exs593+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/common_api_test.exs1244+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/fallback_test.exs80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/fed_sockets/fed_registry_test.exs124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/fed_sockets/fetch_registry_test.exs67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/fed_sockets/socket_info_test.exs118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/federator_test.exs173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/feed/tag_controller_test.exs197+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/feed/user_controller_test.exs265+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/account_controller_test.exs1536+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/app_controller_test.exs60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/auth_controller_test.exs159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/conversation_controller_test.exs209+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/custom_emoji_controller_test.exs23+++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/domain_block_controller_test.exs79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/filter_controller_test.exs152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/follow_request_controller_test.exs74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/instance_controller_test.exs87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/list_controller_test.exs176+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/marker_controller_test.exs131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/media_controller_test.exs146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/notification_controller_test.exs626+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/poll_controller_test.exs171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/report_controller_test.exs95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/scheduled_activity_controller_test.exs139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/search_controller_test.exs413+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/status_controller_test.exs1743+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/subscription_controller_test.exs199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/suggestion_controller_test.exs18++++++++++++++++++
Atest/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs560+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/masto_fe_controller_test.exs85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/mastodon_api_controller_test.exs34++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/mastodon_api_test.exs103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/update_credentials_test.exs529+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/views/account_view_test.exs575+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/views/conversation_view_test.exs44++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/views/list_view_test.exs32++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/views/marker_view_test.exs29+++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/views/notification_view_test.exs231+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/views/poll_view_test.exs167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/views/status_view_test.exs664+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mastodon_api/views/subscription_view_test.exs23+++++++++++++++++++++++
Atest/pleroma/web/media_proxy/invalidation/http_test.exs43+++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/media_proxy/invalidation/script_test.exs30++++++++++++++++++++++++++++++
Atest/pleroma/web/media_proxy/invalidation_test.exs68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/media_proxy/media_proxy_controller_test.exs342+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/media_proxy_test.exs234+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/metadata/player_view_test.exs33+++++++++++++++++++++++++++++++++
Atest/pleroma/web/metadata/providers/feed_test.exs18++++++++++++++++++
Atest/pleroma/web/metadata/providers/open_graph_test.exs96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/metadata/providers/rel_me_test.exs21+++++++++++++++++++++
Atest/pleroma/web/metadata/providers/restrict_indexing_test.exs27+++++++++++++++++++++++++++
Atest/pleroma/web/metadata/providers/twitter_card_test.exs150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/metadata/utils_test.exs32++++++++++++++++++++++++++++++++
Atest/pleroma/web/metadata_test.exs49+++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/mongoose_im_controller_test.exs81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/node_info_test.exs188+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/o_auth/app_test.exs44++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/o_auth/authorization_test.exs77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/o_auth/ldap_authorization_test.exs135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/o_auth/mfa_controller_test.exs306+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/o_auth/o_auth_controller_test.exs1232+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/o_auth/token/utils_test.exs53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/o_auth/token_test.exs72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/o_status/o_status_controller_test.exs338+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/pleroma_api/controllers/account_controller_test.exs284+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/pleroma_api/controllers/chat_controller_test.exs410+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/pleroma_api/controllers/conversation_controller_test.exs136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs357+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs604++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/pleroma_api/controllers/notification_controller_test.exs68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs264+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/pleroma_api/controllers/user_import_controller_test.exs235+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/pleroma_api/views/chat_view_test.exs49+++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/pleroma_api/views/scrobble_view_test.exs20++++++++++++++++++++
Atest/pleroma/web/plugs/admin_secret_authentication_plug_test.exs75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/authentication_plug_test.exs125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/basic_auth_decoder_plug_test.exs35+++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/cache_control_test.exs20++++++++++++++++++++
Atest/pleroma/web/plugs/cache_test.exs186+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/ensure_authenticated_plug_test.exs96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/ensure_public_or_authenticated_plug_test.exs48++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/ensure_user_key_plug_test.exs29+++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/federating_plug_test.exs31+++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/frontend_static_plug_test.exs56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/http_security_plug_test.exs140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/http_signature_plug_test.exs89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/idempotency_plug_test.exs110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/instance_static_test.exs65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/legacy_authentication_plug_test.exs82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/o_auth_plug_test.exs80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/o_auth_scopes_plug_test.exs210+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/plug_helper_test.exs91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/rate_limiter_test.exs263+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/remote_ip_test.exs108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/session_authentication_plug_test.exs63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/set_format_plug_test.exs38++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/set_locale_plug_test.exs46++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/set_user_session_id_plug_test.exs45+++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/uploaded_media_plug_test.exs43+++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/user_enabled_plug_test.exs59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/user_fetcher_plug_test.exs41+++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/plugs/user_is_admin_plug_test.exs37+++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/preload/providers/instance_test.exs56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/preload/providers/timeline_test.exs56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/preload/providers/user_test.exs33+++++++++++++++++++++++++++++++++
Atest/pleroma/web/push/impl_test.exs344+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/rel_me_test.exs48++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/rich_media/helpers_test.exs85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/rich_media/parser/ttl/aws_signed_url_test.exs82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/rich_media/parser_test.exs176+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/rich_media/parsers/twitter_card_test.exs127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/static_fe/static_fe_controller_test.exs196+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/streamer_test.exs767+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/twitter_api/controller_test.exs138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/twitter_api/password_controller_test.exs81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/twitter_api/remote_follow_controller_test.exs350+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/twitter_api/twitter_api_test.exs432+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/twitter_api/util_controller_test.exs437+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/uploader_controller_test.exs43+++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/views/error_view_test.exs36++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/web_finger/web_finger_controller_test.exs94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/web/web_finger_test.exs116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/workers/cron/digest_emails_worker_test.exs54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/workers/cron/new_users_digest_worker_test.exs45+++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/workers/purge_expired_activity_test.exs59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/workers/purge_expired_token_test.exs51+++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/workers/scheduled_activity_worker_test.exs49+++++++++++++++++++++++++++++++++++++++++++++++++
Atest/pleroma/xml_builder_test.exs65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtest/plugs/admin_secret_authentication_plug_test.exs75---------------------------------------------------------------------------
Dtest/plugs/authentication_plug_test.exs125-------------------------------------------------------------------------------
Dtest/plugs/basic_auth_decoder_plug_test.exs35-----------------------------------
Dtest/plugs/cache_control_test.exs20--------------------
Dtest/plugs/cache_test.exs186-------------------------------------------------------------------------------
Dtest/plugs/ensure_authenticated_plug_test.exs96-------------------------------------------------------------------------------
Dtest/plugs/ensure_public_or_authenticated_plug_test.exs48------------------------------------------------
Dtest/plugs/ensure_user_key_plug_test.exs29-----------------------------
Dtest/plugs/frontend_static_test.exs57---------------------------------------------------------
Dtest/plugs/http_security_plug_test.exs140-------------------------------------------------------------------------------
Dtest/plugs/http_signature_plug_test.exs89-------------------------------------------------------------------------------
Dtest/plugs/idempotency_plug_test.exs110-------------------------------------------------------------------------------
Dtest/plugs/instance_static_test.exs65-----------------------------------------------------------------
Dtest/plugs/legacy_authentication_plug_test.exs82-------------------------------------------------------------------------------
Dtest/plugs/mapped_identity_to_signature_plug_test.exs59-----------------------------------------------------------
Dtest/plugs/oauth_plug_test.exs80-------------------------------------------------------------------------------
Dtest/plugs/oauth_scopes_plug_test.exs210-------------------------------------------------------------------------------
Dtest/plugs/rate_limiter_test.exs263-------------------------------------------------------------------------------
Dtest/plugs/remote_ip_test.exs108-------------------------------------------------------------------------------
Dtest/plugs/session_authentication_plug_test.exs63---------------------------------------------------------------
Dtest/plugs/set_format_plug_test.exs38--------------------------------------
Dtest/plugs/set_locale_plug_test.exs46----------------------------------------------
Dtest/plugs/set_user_session_id_plug_test.exs45---------------------------------------------
Dtest/plugs/uploaded_media_plug_test.exs43-------------------------------------------
Dtest/plugs/user_enabled_plug_test.exs59-----------------------------------------------------------
Dtest/plugs/user_fetcher_plug_test.exs41-----------------------------------------
Dtest/plugs/user_is_admin_plug_test.exs37-------------------------------------
Dtest/registration_test.exs59-----------------------------------------------------------
Dtest/repo_test.exs80-------------------------------------------------------------------------------
Dtest/report_note_test.exs16----------------
Dtest/reverse_proxy/reverse_proxy_test.exs336-------------------------------------------------------------------------------
Dtest/runtime_test.exs11-----------
Dtest/safe_jsonb_set_test.exs16----------------
Dtest/scheduled_activity_test.exs105-------------------------------------------------------------------------------
Dtest/signature_test.exs134-------------------------------------------------------------------------------
Dtest/stats_test.exs122-------------------------------------------------------------------------------
Atest/support/captcha/mock.ex28++++++++++++++++++++++++++++
Dtest/support/captcha_mock.ex28----------------------------
Dtest/tasks/app_test.exs65-----------------------------------------------------------------
Dtest/tasks/config_test.exs189-------------------------------------------------------------------------------
Dtest/tasks/count_statuses_test.exs39---------------------------------------
Dtest/tasks/database_test.exs175-------------------------------------------------------------------------------
Dtest/tasks/digest_test.exs60------------------------------------------------------------
Dtest/tasks/ecto/ecto_test.exs15---------------
Dtest/tasks/ecto/migrate_test.exs20--------------------
Dtest/tasks/ecto/rollback_test.exs20--------------------
Dtest/tasks/email_test.exs127-------------------------------------------------------------------------------
Dtest/tasks/emoji_test.exs243-------------------------------------------------------------------------------
Dtest/tasks/frontend_test.exs85-------------------------------------------------------------------------------
Dtest/tasks/instance_test.exs99-------------------------------------------------------------------------------
Dtest/tasks/pleroma_test.exs50--------------------------------------------------
Dtest/tasks/refresh_counter_cache_test.exs43-------------------------------------------
Dtest/tasks/relay_test.exs180-------------------------------------------------------------------------------
Dtest/tasks/robots_txt_test.exs45---------------------------------------------
Dtest/tasks/uploads_test.exs56--------------------------------------------------------
Dtest/tasks/user_test.exs614-------------------------------------------------------------------------------
Dtest/upload/filter/anonymize_filename_test.exs42------------------------------------------
Dtest/upload/filter/dedupe_test.exs32--------------------------------
Dtest/upload/filter/exiftool_test.exs42------------------------------------------
Dtest/upload/filter/mogrifun_test.exs44--------------------------------------------
Dtest/upload/filter/mogrify_test.exs41-----------------------------------------
Dtest/upload/filter_test.exs33---------------------------------
Dtest/upload_test.exs287-------------------------------------------------------------------------------
Dtest/uploaders/local_test.exs55-------------------------------------------------------
Dtest/uploaders/s3_test.exs88-------------------------------------------------------------------------------
Dtest/user/import_test.exs76----------------------------------------------------------------------------
Dtest/user/notification_setting_test.exs21---------------------
Dtest/user/query_test.exs37-------------------------------------
Dtest/user/welcome_chat_massage_test.exs35-----------------------------------
Dtest/user/welcome_email_test.exs61-------------------------------------------------------------
Dtest/user/welcome_message_test.exs34----------------------------------
Dtest/user_invite_token_test.exs96-------------------------------------------------------------------------------
Dtest/user_relationship_test.exs130-------------------------------------------------------------------------------
Dtest/user_search_test.exs362-------------------------------------------------------------------------------
Dtest/user_test.exs2124-------------------------------------------------------------------------------
Dtest/utils_test.exs15---------------
Dtest/web/activity_pub/activity_pub_controller_test.exs1550-------------------------------------------------------------------------------
Dtest/web/activity_pub/activity_pub_test.exs2260-------------------------------------------------------------------------------
Dtest/web/activity_pub/mrf/activity_expiration_policy_test.exs84-------------------------------------------------------------------------------
Dtest/web/activity_pub/mrf/anti_followbot_policy_test.exs72------------------------------------------------------------------------
Dtest/web/activity_pub/mrf/anti_link_spam_policy_test.exs166-------------------------------------------------------------------------------
Dtest/web/activity_pub/mrf/ensure_re_prepended_test.exs92-------------------------------------------------------------------------------
Dtest/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs60------------------------------------------------------------
Dtest/web/activity_pub/mrf/hellthread_policy_test.exs92-------------------------------------------------------------------------------
Dtest/web/activity_pub/mrf/keyword_policy_test.exs225-------------------------------------------------------------------------------
Dtest/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs53-----------------------------------------------------
Dtest/web/activity_pub/mrf/mention_policy_test.exs96-------------------------------------------------------------------------------
Dtest/web/activity_pub/mrf/mrf_test.exs90-------------------------------------------------------------------------------
Dtest/web/activity_pub/mrf/no_placeholder_text_policy_test.exs37-------------------------------------
Dtest/web/activity_pub/mrf/normalize_markup_test.exs42------------------------------------------
Dtest/web/activity_pub/mrf/object_age_policy_test.exs148-------------------------------------------------------------------------------
Dtest/web/activity_pub/mrf/reject_non_public_test.exs100-------------------------------------------------------------------------------
Dtest/web/activity_pub/mrf/simple_policy_test.exs539-------------------------------------------------------------------------------
Dtest/web/activity_pub/mrf/steal_emoji_policy_test.exs68--------------------------------------------------------------------
Dtest/web/activity_pub/mrf/subchain_policy_test.exs33---------------------------------
Dtest/web/activity_pub/mrf/tag_policy_test.exs123-------------------------------------------------------------------------------
Dtest/web/activity_pub/mrf/user_allowlist_policy_test.exs31-------------------------------
Dtest/web/activity_pub/mrf/vocabulary_policy_test.exs106-------------------------------------------------------------------------------
Dtest/web/activity_pub/object_validators/accept_validation_test.exs56--------------------------------------------------------
Dtest/web/activity_pub/object_validators/announce_validation_test.exs106-------------------------------------------------------------------------------
Dtest/web/activity_pub/object_validators/article_note_validator_test.exs35-----------------------------------
Dtest/web/activity_pub/object_validators/attachment_validator_test.exs74--------------------------------------------------------------------------
Dtest/web/activity_pub/object_validators/block_validation_test.exs39---------------------------------------
Dtest/web/activity_pub/object_validators/chat_validation_test.exs212-------------------------------------------------------------------------------
Dtest/web/activity_pub/object_validators/delete_validation_test.exs106-------------------------------------------------------------------------------
Dtest/web/activity_pub/object_validators/emoji_react_validation_test.exs53-----------------------------------------------------
Dtest/web/activity_pub/object_validators/follow_validation_test.exs26--------------------------
Dtest/web/activity_pub/object_validators/like_validation_test.exs113-------------------------------------------------------------------------------
Dtest/web/activity_pub/object_validators/reject_validation_test.exs56--------------------------------------------------------
Dtest/web/activity_pub/object_validators/types/date_time_test.exs36------------------------------------
Dtest/web/activity_pub/object_validators/types/object_id_test.exs41-----------------------------------------
Dtest/web/activity_pub/object_validators/types/recipients_test.exs31-------------------------------
Dtest/web/activity_pub/object_validators/types/safe_text_test.exs30------------------------------
Dtest/web/activity_pub/object_validators/undo_validation_test.exs53-----------------------------------------------------
Dtest/web/activity_pub/object_validators/update_validation_test.exs44--------------------------------------------
Dtest/web/activity_pub/pipeline_test.exs179-------------------------------------------------------------------------------
Dtest/web/activity_pub/publisher_test.exs365-------------------------------------------------------------------------------
Dtest/web/activity_pub/relay_test.exs168-------------------------------------------------------------------------------
Dtest/web/activity_pub/side_effects_test.exs639-------------------------------------------------------------------------------
Dtest/web/activity_pub/transmogrifier/accept_handling_test.exs91-------------------------------------------------------------------------------
Dtest/web/activity_pub/transmogrifier/announce_handling_test.exs172-------------------------------------------------------------------------------
Dtest/web/activity_pub/transmogrifier/answer_handling_test.exs78------------------------------------------------------------------------------
Dtest/web/activity_pub/transmogrifier/article_handling_test.exs75---------------------------------------------------------------------------
Dtest/web/activity_pub/transmogrifier/audio_handling_test.exs83-------------------------------------------------------------------------------
Dtest/web/activity_pub/transmogrifier/block_handling_test.exs63---------------------------------------------------------------
Dtest/web/activity_pub/transmogrifier/chat_message_test.exs171-------------------------------------------------------------------------------
Dtest/web/activity_pub/transmogrifier/delete_handling_test.exs114-------------------------------------------------------------------------------
Dtest/web/activity_pub/transmogrifier/emoji_react_handling_test.exs61-------------------------------------------------------------
Dtest/web/activity_pub/transmogrifier/event_handling_test.exs40----------------------------------------
Dtest/web/activity_pub/transmogrifier/follow_handling_test.exs208-------------------------------------------------------------------------------
Dtest/web/activity_pub/transmogrifier/like_handling_test.exs78------------------------------------------------------------------------------
Dtest/web/activity_pub/transmogrifier/question_handling_test.exs176-------------------------------------------------------------------------------
Dtest/web/activity_pub/transmogrifier/reject_handling_test.exs67-------------------------------------------------------------------
Dtest/web/activity_pub/transmogrifier/undo_handling_test.exs185-------------------------------------------------------------------------------
Dtest/web/activity_pub/transmogrifier/user_update_handling_test.exs159-------------------------------------------------------------------------------
Dtest/web/activity_pub/transmogrifier/video_handling_test.exs93-------------------------------------------------------------------------------
Dtest/web/activity_pub/transmogrifier_test.exs1220------------------------------------------------------------------------------
Dtest/web/activity_pub/utils_test.exs548-------------------------------------------------------------------------------
Dtest/web/activity_pub/views/object_view_test.exs84-------------------------------------------------------------------------------
Dtest/web/activity_pub/views/user_view_test.exs180-------------------------------------------------------------------------------
Dtest/web/activity_pub/visibilty_test.exs230-------------------------------------------------------------------------------
Dtest/web/admin_api/controllers/admin_api_controller_test.exs2034-------------------------------------------------------------------------------
Dtest/web/admin_api/controllers/chat_controller_test.exs219-------------------------------------------------------------------------------
Dtest/web/admin_api/controllers/config_controller_test.exs1465-------------------------------------------------------------------------------
Dtest/web/admin_api/controllers/instance_document_controller_test.exs106-------------------------------------------------------------------------------
Dtest/web/admin_api/controllers/invite_controller_test.exs281-------------------------------------------------------------------------------
Dtest/web/admin_api/controllers/media_proxy_cache_controller_test.exs167-------------------------------------------------------------------------------
Dtest/web/admin_api/controllers/oauth_app_controller_test.exs220-------------------------------------------------------------------------------
Dtest/web/admin_api/controllers/relay_controller_test.exs99-------------------------------------------------------------------------------
Dtest/web/admin_api/controllers/report_controller_test.exs372-------------------------------------------------------------------------------
Dtest/web/admin_api/controllers/status_controller_test.exs202-------------------------------------------------------------------------------
Dtest/web/admin_api/search_test.exs190-------------------------------------------------------------------------------
Dtest/web/admin_api/views/report_view_test.exs146-------------------------------------------------------------------------------
Dtest/web/api_spec/schema_examples_test.exs43-------------------------------------------
Dtest/web/auth/auth_test_controller_test.exs242-------------------------------------------------------------------------------
Dtest/web/auth/authenticator_test.exs42------------------------------------------
Dtest/web/auth/basic_auth_test.exs46----------------------------------------------
Dtest/web/auth/pleroma_authenticator_test.exs48------------------------------------------------
Dtest/web/auth/totp_authenticator_test.exs51---------------------------------------------------
Dtest/web/chat_channel_test.exs41-----------------------------------------
Dtest/web/common_api/common_api_test.exs1244-------------------------------------------------------------------------------
Dtest/web/common_api/common_api_utils_test.exs593-------------------------------------------------------------------------------
Dtest/web/fallback_test.exs80-------------------------------------------------------------------------------
Dtest/web/fed_sockets/fed_registry_test.exs124-------------------------------------------------------------------------------
Dtest/web/fed_sockets/fetch_registry_test.exs67-------------------------------------------------------------------
Dtest/web/fed_sockets/socket_info_test.exs118-------------------------------------------------------------------------------
Dtest/web/federator_test.exs173-------------------------------------------------------------------------------
Dtest/web/feed/tag_controller_test.exs197-------------------------------------------------------------------------------
Dtest/web/feed/user_controller_test.exs265-------------------------------------------------------------------------------
Dtest/web/instances/instance_test.exs152-------------------------------------------------------------------------------
Dtest/web/instances/instances_test.exs124-------------------------------------------------------------------------------
Dtest/web/masto_fe_controller_test.exs85-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/account_controller/update_credentials_test.exs529-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/account_controller_test.exs1536-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/app_controller_test.exs60------------------------------------------------------------
Dtest/web/mastodon_api/controllers/auth_controller_test.exs159-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/conversation_controller_test.exs209-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/custom_emoji_controller_test.exs23-----------------------
Dtest/web/mastodon_api/controllers/domain_block_controller_test.exs79-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/filter_controller_test.exs152-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/follow_request_controller_test.exs74--------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/instance_controller_test.exs87-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/list_controller_test.exs176-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/marker_controller_test.exs131-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/media_controller_test.exs146-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/notification_controller_test.exs626-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/poll_controller_test.exs171-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/report_controller_test.exs95-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/scheduled_activity_controller_test.exs139-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/search_controller_test.exs413-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/status_controller_test.exs1743-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/subscription_controller_test.exs199-------------------------------------------------------------------------------
Dtest/web/mastodon_api/controllers/suggestion_controller_test.exs18------------------
Dtest/web/mastodon_api/controllers/timeline_controller_test.exs560-------------------------------------------------------------------------------
Dtest/web/mastodon_api/mastodon_api_controller_test.exs34----------------------------------
Dtest/web/mastodon_api/mastodon_api_test.exs103-------------------------------------------------------------------------------
Dtest/web/mastodon_api/views/account_view_test.exs575-------------------------------------------------------------------------------
Dtest/web/mastodon_api/views/conversation_view_test.exs44--------------------------------------------
Dtest/web/mastodon_api/views/list_view_test.exs32--------------------------------
Dtest/web/mastodon_api/views/marker_view_test.exs29-----------------------------
Dtest/web/mastodon_api/views/notification_view_test.exs231-------------------------------------------------------------------------------
Dtest/web/mastodon_api/views/poll_view_test.exs167-------------------------------------------------------------------------------
Dtest/web/mastodon_api/views/scheduled_activity_view_test.exs68--------------------------------------------------------------------
Dtest/web/mastodon_api/views/status_view_test.exs664-------------------------------------------------------------------------------
Dtest/web/mastodon_api/views/subscription_view_test.exs23-----------------------
Dtest/web/media_proxy/invalidation_test.exs68--------------------------------------------------------------------
Dtest/web/media_proxy/invalidations/http_test.exs43-------------------------------------------
Dtest/web/media_proxy/invalidations/script_test.exs30------------------------------
Dtest/web/media_proxy/media_proxy_controller_test.exs342-------------------------------------------------------------------------------
Dtest/web/media_proxy/media_proxy_test.exs234-------------------------------------------------------------------------------
Dtest/web/metadata/feed_test.exs18------------------
Dtest/web/metadata/metadata_test.exs49-------------------------------------------------
Dtest/web/metadata/opengraph_test.exs96-------------------------------------------------------------------------------
Dtest/web/metadata/player_view_test.exs33---------------------------------
Dtest/web/metadata/rel_me_test.exs21---------------------
Dtest/web/metadata/restrict_indexing_test.exs27---------------------------
Dtest/web/metadata/twitter_card_test.exs150-------------------------------------------------------------------------------
Dtest/web/metadata/utils_test.exs32--------------------------------
Dtest/web/mongooseim/mongoose_im_controller_test.exs81-------------------------------------------------------------------------------
Dtest/web/node_info_test.exs188-------------------------------------------------------------------------------
Dtest/web/oauth/app_test.exs44--------------------------------------------
Dtest/web/oauth/authorization_test.exs77-----------------------------------------------------------------------------
Dtest/web/oauth/ldap_authorization_test.exs135-------------------------------------------------------------------------------
Dtest/web/oauth/mfa_controller_test.exs306-------------------------------------------------------------------------------
Dtest/web/oauth/oauth_controller_test.exs1232-------------------------------------------------------------------------------
Dtest/web/oauth/token/utils_test.exs53-----------------------------------------------------
Dtest/web/oauth/token_test.exs72------------------------------------------------------------------------
Dtest/web/ostatus/ostatus_controller_test.exs338-------------------------------------------------------------------------------
Dtest/web/pleroma_api/controllers/account_controller_test.exs284-------------------------------------------------------------------------------
Dtest/web/pleroma_api/controllers/chat_controller_test.exs410-------------------------------------------------------------------------------
Dtest/web/pleroma_api/controllers/conversation_controller_test.exs136-------------------------------------------------------------------------------
Dtest/web/pleroma_api/controllers/emoji_file_controller_test.exs357-------------------------------------------------------------------------------
Dtest/web/pleroma_api/controllers/emoji_pack_controller_test.exs604------------------------------------------------------------------------------
Dtest/web/pleroma_api/controllers/emoji_reaction_controller_test.exs149-------------------------------------------------------------------------------
Dtest/web/pleroma_api/controllers/mascot_controller_test.exs73-------------------------------------------------------------------------
Dtest/web/pleroma_api/controllers/notification_controller_test.exs68--------------------------------------------------------------------
Dtest/web/pleroma_api/controllers/scrobble_controller_test.exs60------------------------------------------------------------
Dtest/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs264-------------------------------------------------------------------------------
Dtest/web/pleroma_api/controllers/user_import_controller_test.exs235-------------------------------------------------------------------------------
Dtest/web/pleroma_api/views/chat/message_reference_view_test.exs72------------------------------------------------------------------------
Dtest/web/pleroma_api/views/chat_view_test.exs49-------------------------------------------------
Dtest/web/pleroma_api/views/scrobble_view_test.exs20--------------------
Dtest/web/plugs/federating_plug_test.exs31-------------------------------
Dtest/web/plugs/plug_test.exs91-------------------------------------------------------------------------------
Dtest/web/preload/instance_test.exs56--------------------------------------------------------
Dtest/web/preload/timeline_test.exs56--------------------------------------------------------
Dtest/web/preload/user_test.exs33---------------------------------
Dtest/web/push/impl_test.exs344-------------------------------------------------------------------------------
Dtest/web/rel_me_test.exs48------------------------------------------------
Dtest/web/rich_media/aws_signed_url_test.exs82-------------------------------------------------------------------------------
Dtest/web/rich_media/helpers_test.exs86-------------------------------------------------------------------------------
Dtest/web/rich_media/parser_test.exs176-------------------------------------------------------------------------------
Dtest/web/rich_media/parsers/twitter_card_test.exs127-------------------------------------------------------------------------------
Dtest/web/static_fe/static_fe_controller_test.exs196-------------------------------------------------------------------------------
Dtest/web/streamer/streamer_test.exs767-------------------------------------------------------------------------------
Dtest/web/twitter_api/password_controller_test.exs81-------------------------------------------------------------------------------
Dtest/web/twitter_api/remote_follow_controller_test.exs350-------------------------------------------------------------------------------
Dtest/web/twitter_api/twitter_api_controller_test.exs138-------------------------------------------------------------------------------
Dtest/web/twitter_api/twitter_api_test.exs432-------------------------------------------------------------------------------
Dtest/web/twitter_api/util_controller_test.exs437-------------------------------------------------------------------------------
Dtest/web/uploader_controller_test.exs43-------------------------------------------
Dtest/web/views/error_view_test.exs36------------------------------------
Dtest/web/web_finger/web_finger_controller_test.exs94-------------------------------------------------------------------------------
Dtest/web/web_finger/web_finger_test.exs116-------------------------------------------------------------------------------
Dtest/workers/cron/digest_emails_worker_test.exs54------------------------------------------------------
Dtest/workers/cron/new_users_digest_worker_test.exs45---------------------------------------------
Dtest/workers/purge_expired_activity_test.exs59-----------------------------------------------------------
Dtest/workers/purge_expired_token_test.exs51---------------------------------------------------
Dtest/workers/scheduled_activity_worker_test.exs49-------------------------------------------------
Dtest/xml_builder_test.exs65-----------------------------------------------------------------
818 files changed, 57981 insertions(+), 57773 deletions(-)

diff --git a/.credo.exs b/.credo.exs @@ -25,7 +25,7 @@ # # If you create your own checks, you must specify the source files for # them here, so they can be loaded by Credo before running the analysis. - requires: [], + requires: ["./lib/credo/check/consistency/file_location.ex"], # # Credo automatically checks for updates, like e.g. Hex does. # You can disable this behaviour below: @@ -71,7 +71,6 @@ # set this value to 0 (zero). {Credo.Check.Design.TagTODO, exit_status: 0}, {Credo.Check.Design.TagFIXME, exit_status: 0}, - {Credo.Check.Readability.FunctionNames}, {Credo.Check.Readability.LargeNumbers}, {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 100}, @@ -91,7 +90,6 @@ {Credo.Check.Readability.VariableNames}, {Credo.Check.Readability.Semicolons}, {Credo.Check.Readability.SpaceAfterCommas}, - {Credo.Check.Refactor.DoubleBooleanNegation}, {Credo.Check.Refactor.CondStatements}, {Credo.Check.Refactor.CyclomaticComplexity}, @@ -102,7 +100,6 @@ {Credo.Check.Refactor.Nesting}, {Credo.Check.Refactor.PipeChainStart}, {Credo.Check.Refactor.UnlessWithElse}, - {Credo.Check.Warning.BoolOperationOnSameValues}, {Credo.Check.Warning.IExPry}, {Credo.Check.Warning.IoInspect}, @@ -131,6 +128,7 @@ # Custom checks can be created using `mix credo.gen.check`. # + {Credo.Check.Consistency.FileLocation} ] } ] diff --git a/config/config.exs b/config/config.exs @@ -677,7 +677,7 @@ config :pleroma, :rate_limit, config :pleroma, Pleroma.Workers.PurgeExpiredActivity, enabled: true, min_lifetime: 600 -config :pleroma, Pleroma.Plugs.RemoteIp, +config :pleroma, Pleroma.Web.Plugs.RemoteIp, enabled: true, headers: ["x-forwarded-for"], proxies: [], diff --git a/config/description.exs b/config/description.exs @@ -3250,10 +3250,10 @@ config :pleroma, :config_description, [ }, %{ group: :pleroma, - key: Pleroma.Plugs.RemoteIp, + key: Pleroma.Web.Plugs.RemoteIp, type: :group, description: """ - `Pleroma.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. + `Pleroma.Web.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. **If your instance is not behind at least one reverse proxy, you should not enable this plug.** """, children: [ diff --git a/config/test.exs b/config/test.exs @@ -113,7 +113,7 @@ config :pleroma, Pleroma.Gun, Pleroma.GunMock config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: true -config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false +config :pleroma, Pleroma.Web.Plugs.RemoteIp, enabled: false config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true diff --git a/coveralls.json b/coveralls.json @@ -1,6 +1,7 @@ { "skip_files": [ "test/support", - "lib/mix/tasks/pleroma/benchmark.ex" + "lib/mix/tasks/pleroma/benchmark.ex", + "lib/credo/check/consistency/file_location.ex" ] } \ No newline at end of file diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md @@ -113,7 +113,7 @@ To add configuration to your config file, you can copy it from the base config. * `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)). * `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)). * `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)). - * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.ActivityExpiration` to be enabled for processing the scheduled delections. + * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.Workers.PurgeExpiredActivity` to be enabled for processing the scheduled delections. * `Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy`: Makes all bot posts to disappear from public timelines. * `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo). * `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. @@ -219,12 +219,6 @@ config :pleroma, :mrf_user_allowlist, %{ * `total_user_limit`: the number of scheduled activities a user is allowed to create in total (Default: `300`) * `enabled`: whether scheduled activities are sent to the job queue to be executed -## Pleroma.ActivityExpiration - -Enables the worker which processes posts scheduled for deletion. Pinned posts are exempt from expiration. - -* `enabled`: whether expired activities will be sent to the job queue to be deleted - ## FedSockets FedSockets is an experimental feature allowing for Pleroma backends to federate using a persistant websocket connection as opposed to making each federation a seperate http connection. This feature is currently off by default. It is configurable throught he following options. @@ -416,12 +410,12 @@ This will make Pleroma listen on `127.0.0.1` port `8080` and generate urls start * ``referrer_policy``: The referrer policy to use, either `"same-origin"` or `"no-referrer"`. * ``report_uri``: Adds the specified url to `report-uri` and `report-to` group in CSP header. -### Pleroma.Plugs.RemoteIp +### Pleroma.Web.Plugs.RemoteIp !!! warning If your instance is not behind at least one reverse proxy, you should not enable this plug. -`Pleroma.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. +`Pleroma.Web.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. Available options: @@ -434,7 +428,7 @@ Available options: ### :rate_limit !!! note - If your instance is behind a reverse proxy ensure [`Pleroma.Plugs.RemoteIp`](#pleroma-plugs-remoteip) is enabled (it is enabled by default). + If your instance is behind a reverse proxy ensure [`Pleroma.Web.Plugs.RemoteIp`](#pleroma-plugs-remoteip) is enabled (it is enabled by default). A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where: diff --git a/docs/dev.md b/docs/dev.md @@ -6,7 +6,7 @@ This document contains notes and guidelines for Pleroma developers. * Pleroma supports hierarchical OAuth scopes, just like Mastodon but with added granularity of admin scopes. For a reference, see [Mastodon OAuth scopes](https://docs.joinmastodon.org/api/oauth-scopes/). -* It is important to either define OAuth scope restrictions or explicitly mark OAuth scope check as skipped, for every controller action. To define scopes, call `plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: [...]})`. To explicitly set OAuth scopes check skipped, call `plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug <when ...>)`. +* It is important to either define OAuth scope restrictions or explicitly mark OAuth scope check as skipped, for every controller action. To define scopes, call `plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: [...]})`. To explicitly set OAuth scopes check skipped, call `plug(:skip_plug, Pleroma.Web.Plugs.OAuthScopesPlug <when ...>)`. * In controllers, `use Pleroma.Web, :controller` will result in `action/2` (see `Pleroma.Web.controller/0` for definition) be called prior to actual controller action, and it'll perform security / privacy checks before passing control to actual controller action. @@ -16,7 +16,7 @@ This document contains notes and guidelines for Pleroma developers. ## [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) -* With HTTP Basic Auth, OAuth scopes check is _not_ performed for any action (since password is provided during the auth, requester is able to obtain a token with full permissions anyways). `Pleroma.Plugs.AuthenticationPlug` and `Pleroma.Plugs.LegacyAuthenticationPlug` both call `Pleroma.Plugs.OAuthScopesPlug.skip_plug(conn)` when password is provided. +* With HTTP Basic Auth, OAuth scopes check is _not_ performed for any action (since password is provided during the auth, requester is able to obtain a token with full permissions anyways). `Pleroma.Web.Plugs.AuthenticationPlug` and `Pleroma.Web.Plugs.LegacyAuthenticationPlug` both call `Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug(conn)` when password is provided. ## Auth-related configuration, OAuth consumer mode etc. diff --git a/lib/credo/check/consistency/file_location.ex b/lib/credo/check/consistency/file_location.ex @@ -0,0 +1,166 @@ +# Pleroma: A lightweight social networking server +# Originally taken from +# https://github.com/VeryBigThings/elixir_common/blob/master/lib/vbt/credo/check/consistency/file_location.ex +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Credo.Check.Consistency.FileLocation do + @moduledoc false + + # credo:disable-for-this-file Credo.Check.Readability.Specs + + @checkdoc """ + File location should follow the namespace hierarchy of the module it defines. + + Examples: + + - `lib/my_system.ex` should define the `MySystem` module + - `lib/my_system/accounts.ex` should define the `MySystem.Accounts` module + """ + @explanation [warning: @checkdoc] + + @special_namespaces [ + "controllers", + "views", + "operations", + "channels" + ] + + # `use Credo.Check` required that module attributes are already defined, so we need + # to place these attributes + # before use/alias expressions. + # credo:disable-for-next-line VBT.Credo.Check.Consistency.ModuleLayout + use Credo.Check, category: :warning, base_priority: :high + + alias Credo.Code + + def run(source_file, params \\ []) do + case verify(source_file, params) do + :ok -> + [] + + {:error, module, expected_file} -> + error(IssueMeta.for(source_file, params), module, expected_file) + end + end + + defp verify(source_file, params) do + source_file.filename + |> Path.relative_to_cwd() + |> verify(Code.ast(source_file), params) + end + + @doc false + def verify(relative_path, ast, params) do + if verify_path?(relative_path, params), + do: ast |> main_module() |> verify_module(relative_path, params), + else: :ok + end + + defp verify_path?(relative_path, params) do + case Path.split(relative_path) do + ["lib" | _] -> not exclude?(relative_path, params) + ["test", "support" | _] -> false + ["test", "test_helper.exs"] -> false + ["test" | _] -> not exclude?(relative_path, params) + _ -> false + end + end + + defp exclude?(relative_path, params) do + params + |> Keyword.get(:exclude, []) + |> Enum.any?(&String.starts_with?(relative_path, &1)) + end + + defp main_module(ast) do + {_ast, modules} = Macro.prewalk(ast, [], &traverse/2) + Enum.at(modules, -1) + end + + defp traverse({:defmodule, _meta, args}, modules) do + [{:__aliases__, _, name_parts}, _module_body] = args + {args, [Module.concat(name_parts) | modules]} + end + + defp traverse(ast, state), do: {ast, state} + + # empty file - shouldn't really happen, but we'll let it through + defp verify_module(nil, _relative_path, _params), do: :ok + + defp verify_module(main_module, relative_path, params) do + parsed_path = parsed_path(relative_path, params) + + expected_file = + expected_file_base(parsed_path.root, main_module) <> + Path.extname(parsed_path.allowed) + + cond do + expected_file == parsed_path.allowed -> + :ok + + special_namespaces?(parsed_path.allowed) -> + original_path = parsed_path.allowed + + namespace = + Enum.find(@special_namespaces, original_path, fn namespace -> + String.contains?(original_path, namespace) + end) + + allowed = String.replace(original_path, "/" <> namespace, "") + + if expected_file == allowed, + do: :ok, + else: {:error, main_module, expected_file} + + true -> + {:error, main_module, expected_file} + end + end + + defp special_namespaces?(path), do: String.contains?(path, @special_namespaces) + + defp parsed_path(relative_path, params) do + parts = Path.split(relative_path) + + allowed = + Keyword.get(params, :ignore_folder_namespace, %{}) + |> Stream.flat_map(fn {root, folders} -> Enum.map(folders, &Path.join([root, &1])) end) + |> Stream.map(&Path.split/1) + |> Enum.find(&List.starts_with?(parts, &1)) + |> case do + nil -> + relative_path + + ignore_parts -> + Stream.drop(ignore_parts, -1) + |> Enum.concat(Stream.drop(parts, length(ignore_parts))) + |> Path.join() + end + + %{root: hd(parts), allowed: allowed} + end + + defp expected_file_base(root_folder, module) do + {parent_namespace, module_name} = module |> Module.split() |> Enum.split(-1) + + relative_path = + if parent_namespace == [], + do: "", + else: parent_namespace |> Module.concat() |> Macro.underscore() + + file_name = module_name |> Module.concat() |> Macro.underscore() + + Path.join([root_folder, relative_path, file_name]) + end + + defp error(issue_meta, module, expected_file) do + format_issue(issue_meta, + message: + "Mismatch between file name and main module #{inspect(module)}. " <> + "Expected file path to be #{expected_file}. " <> + "Either move the file or rename the module.", + line_no: 1 + ) + end +end diff --git a/lib/mix/tasks/pleroma/ecto/ecto.ex b/lib/mix/tasks/pleroma/ecto.ex diff --git a/lib/mix/tasks/pleroma/robotstxt.ex b/lib/mix/tasks/pleroma/robots_txt.ex diff --git a/lib/transports.ex b/lib/phoenix/transports/web_socket/raw.ex diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex @@ -52,7 +52,7 @@ defmodule Pleroma.Application do Pleroma.HTML.compile_scrubbers() Pleroma.Config.Oban.warn() Config.DeprecationWarnings.warn() - Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled() + Pleroma.Web.Plugs.HTTPSecurityPlug.warn_if_disabled() Pleroma.ApplicationRequirements.verify!() setup_instrumenters() load_custom_modules() @@ -88,7 +88,7 @@ defmodule Pleroma.Application do Pleroma.Repo, Config.TransferTask, Pleroma.Emoji, - Pleroma.Plugs.RateLimiter.Supervisor + Pleroma.Web.Plugs.RateLimiter.Supervisor ] ++ cachex_children() ++ http_children(adapter, @env) ++ diff --git a/lib/pleroma/bbs/authenticator.ex b/lib/pleroma/bbs/authenticator.ex @@ -4,8 +4,8 @@ defmodule Pleroma.BBS.Authenticator do use Sshd.PasswordAuthenticator - alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.User + alias Pleroma.Web.Plugs.AuthenticationPlug def authenticate(username, password) do username = to_string(username) diff --git a/lib/pleroma/captcha/captcha.ex b/lib/pleroma/captcha.ex diff --git a/lib/pleroma/captcha/captcha_service.ex b/lib/pleroma/captcha/service.ex diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex @@ -39,7 +39,8 @@ defmodule Pleroma.Config.DeprecationWarnings do :ok <- check_media_proxy_whitelist_config(), :ok <- check_welcome_message_config(), :ok <- check_gun_pool_options(), - :ok <- check_activity_expiration_config() do + :ok <- check_activity_expiration_config(), + :ok <- check_remote_ip_plug_name() do :ok else _ -> @@ -176,4 +177,20 @@ defmodule Pleroma.Config.DeprecationWarnings do warning_preface ) end + + @spec check_remote_ip_plug_name() :: :ok | nil + def check_remote_ip_plug_name do + warning_preface = """ + !!!DEPRECATION WARNING!!! + Your config is using old namespace for RemoteIp Plug. Setting should work for now, but you are advised to change to new namespace to prevent possible issues later: + """ + + move_namespace_and_warn( + [ + {Pleroma.Plugs.RemoteIp, Pleroma.Web.Plugs.RemoteIp, + "\n* `config :pleroma, Pleroma.Plugs.RemoteIp` is now `config :pleroma, Pleroma.Web.Plugs.RemoteIp`"} + ], + warning_preface + ) + end end diff --git a/lib/pleroma/config/config_db.ex b/lib/pleroma/config_db.ex diff --git a/lib/pleroma/conversation/participation_recipient_ship.ex b/lib/pleroma/conversation/participation/recipient_ship.ex diff --git a/lib/pleroma/gun/gun.ex b/lib/pleroma/gun.ex diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http.ex diff --git a/lib/pleroma/plugs/admin_secret_authentication_plug.ex b/lib/pleroma/plugs/admin_secret_authentication_plug.ex @@ -1,60 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do - import Plug.Conn - - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.RateLimiter - alias Pleroma.User - - def init(options) do - options - end - - def secret_token do - case Pleroma.Config.get(:admin_token) do - blank when blank in [nil, ""] -> nil - token -> token - end - end - - def call(%{assigns: %{user: %User{}}} = conn, _), do: conn - - def call(conn, _) do - if secret_token() do - authenticate(conn) - else - conn - end - end - - def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do - if admin_token == secret_token() do - assign_admin_user(conn) - else - handle_bad_token(conn) - end - end - - def authenticate(conn) do - token = secret_token() - - case get_req_header(conn, "x-admin-token") do - blank when blank in [[], [""]] -> conn - [^token] -> assign_admin_user(conn) - _ -> handle_bad_token(conn) - end - end - - defp assign_admin_user(conn) do - conn - |> assign(:user, %User{is_admin: true}) - |> OAuthScopesPlug.skip_plug() - end - - defp handle_bad_token(conn) do - RateLimiter.call(conn, name: :authentication) - end -end diff --git a/lib/pleroma/plugs/authentication_plug.ex b/lib/pleroma/plugs/authentication_plug.ex @@ -1,80 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.AuthenticationPlug do - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.User - - import Plug.Conn - - require Logger - - def init(options), do: options - - def checkpw(password, "$6" <> _ = password_hash) do - :crypt.crypt(password, password_hash) == password_hash - end - - def checkpw(password, "$2" <> _ = password_hash) do - # Handle bcrypt passwords for Mastodon migration - Bcrypt.verify_pass(password, password_hash) - end - - def checkpw(password, "$pbkdf2" <> _ = password_hash) do - Pbkdf2.verify_pass(password, password_hash) - end - - def checkpw(_password, _password_hash) do - Logger.error("Password hash not recognized") - false - end - - def maybe_update_password(%User{password_hash: "$2" <> _} = user, password) do - do_update_password(user, password) - end - - def maybe_update_password(%User{password_hash: "$6" <> _} = user, password) do - do_update_password(user, password) - end - - def maybe_update_password(user, _), do: {:ok, user} - - defp do_update_password(user, password) do - user - |> User.password_update_changeset(%{ - "password" => password, - "password_confirmation" => password - }) - |> Pleroma.Repo.update() - end - - def call(%{assigns: %{user: %User{}}} = conn, _), do: conn - - def call( - %{ - assigns: %{ - auth_user: %{password_hash: password_hash} = auth_user, - auth_credentials: %{password: password} - } - } = conn, - _ - ) do - if checkpw(password, password_hash) do - {:ok, auth_user} = maybe_update_password(auth_user, password) - - conn - |> assign(:user, auth_user) - |> OAuthScopesPlug.skip_plug() - else - conn - end - end - - def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do - Pbkdf2.no_user_verify() - conn - end - - def call(conn, _), do: conn -end diff --git a/lib/pleroma/plugs/basic_auth_decoder_plug.ex b/lib/pleroma/plugs/basic_auth_decoder_plug.ex @@ -1,25 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.BasicAuthDecoderPlug do - import Plug.Conn - - def init(options) do - options - end - - def call(conn, _opts) do - with ["Basic " <> header] <- get_req_header(conn, "authorization"), - {:ok, userinfo} <- Base.decode64(header), - [username, password] <- String.split(userinfo, ":", parts: 2) do - conn - |> assign(:auth_credentials, %{ - username: username, - password: password - }) - else - _ -> conn - end - end -end diff --git a/lib/pleroma/plugs/cache.ex b/lib/pleroma/plugs/cache.ex @@ -1,136 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.Cache do - @moduledoc """ - Caches successful GET responses. - - To enable the cache add the plug to a router pipeline or controller: - - plug(Pleroma.Plugs.Cache) - - ## Configuration - - To configure the plug you need to pass settings as the second argument to the `plug/2` macro: - - plug(Pleroma.Plugs.Cache, [ttl: nil, query_params: true]) - - Available options: - - - `ttl`: An expiration time (time-to-live). This value should be in milliseconds or `nil` to disable expiration. Defaults to `nil`. - - `query_params`: Take URL query string into account (`true`), ignore it (`false`) or limit to specific params only (list). Defaults to `true`. - - `tracking_fun`: A function that is called on successfull responses, no matter if the request is cached or not. It should accept a conn as the first argument and the value assigned to `tracking_fun_data` as the second. - - Additionally, you can overwrite the TTL inside a controller action by assigning `cache_ttl` to the connection struct: - - def index(conn, _params) do - ttl = 60_000 # one minute - - conn - |> assign(:cache_ttl, ttl) - |> render("index.html") - end - - """ - - import Phoenix.Controller, only: [current_path: 1, json: 2] - import Plug.Conn - - @behaviour Plug - - @defaults %{ttl: nil, query_params: true} - - @impl true - def init([]), do: @defaults - - def init(opts) do - opts = Map.new(opts) - Map.merge(@defaults, opts) - end - - @impl true - def call(%{method: "GET"} = conn, opts) do - key = cache_key(conn, opts) - - case Cachex.get(:web_resp_cache, key) do - {:ok, nil} -> - cache_resp(conn, opts) - - {:ok, {content_type, body, tracking_fun_data}} -> - conn = opts.tracking_fun.(conn, tracking_fun_data) - - send_cached(conn, {content_type, body}) - - {:ok, record} -> - send_cached(conn, record) - - {atom, message} when atom in [:ignore, :error] -> - render_error(conn, message) - end - end - - def call(conn, _), do: conn - - # full path including query params - defp cache_key(conn, %{query_params: true}), do: current_path(conn) - - # request path without query params - defp cache_key(conn, %{query_params: false}), do: conn.request_path - - # request path with specific query params - defp cache_key(conn, %{query_params: query_params}) when is_list(query_params) do - query_string = - conn.params - |> Map.take(query_params) - |> URI.encode_query() - - conn.request_path <> "?" <> query_string - end - - defp cache_resp(conn, opts) do - register_before_send(conn, fn - %{status: 200, resp_body: body} = conn -> - ttl = Map.get(conn.assigns, :cache_ttl, opts.ttl) - key = cache_key(conn, opts) - content_type = content_type(conn) - - conn = - unless opts[:tracking_fun] do - Cachex.put(:web_resp_cache, key, {content_type, body}, ttl: ttl) - conn - else - tracking_fun_data = Map.get(conn.assigns, :tracking_fun_data, nil) - Cachex.put(:web_resp_cache, key, {content_type, body, tracking_fun_data}, ttl: ttl) - - opts.tracking_fun.(conn, tracking_fun_data) - end - - put_resp_header(conn, "x-cache", "MISS from Pleroma") - - conn -> - conn - end) - end - - defp content_type(conn) do - conn - |> Plug.Conn.get_resp_header("content-type") - |> hd() - end - - defp send_cached(conn, {content_type, body}) do - conn - |> put_resp_content_type(content_type, nil) - |> put_resp_header("x-cache", "HIT from Pleroma") - |> send_resp(:ok, body) - |> halt() - end - - defp render_error(conn, message) do - conn - |> put_status(:internal_server_error) - |> json(%{error: message}) - |> halt() - end -end diff --git a/lib/pleroma/plugs/ensure_authenticated_plug.ex b/lib/pleroma/plugs/ensure_authenticated_plug.ex @@ -1,41 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do - import Plug.Conn - import Pleroma.Web.TranslationHelpers - - alias Pleroma.User - - use Pleroma.Web, :plug - - def init(options) do - options - end - - @impl true - def perform( - %{ - assigns: %{ - auth_credentials: %{password: _}, - user: %User{multi_factor_authentication_settings: %{enabled: true}} - } - } = conn, - _ - ) do - conn - |> render_error(:forbidden, "Two-factor authentication enabled, you must use a access token.") - |> halt() - end - - def perform(%{assigns: %{user: %User{}}} = conn, _) do - conn - end - - def perform(conn, _) do - conn - |> render_error(:forbidden, "Invalid credentials.") - |> halt() - end -end diff --git a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex @@ -1,35 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do - import Pleroma.Web.TranslationHelpers - import Plug.Conn - - alias Pleroma.Config - alias Pleroma.User - - use Pleroma.Web, :plug - - def init(options) do - options - end - - @impl true - def perform(conn, _) do - public? = Config.get!([:instance, :public]) - - case {public?, conn} do - {true, _} -> - conn - - {false, %{assigns: %{user: %User{}}}} -> - conn - - {false, _} -> - conn - |> render_error(:forbidden, "This resource requires authentication.") - |> halt - end - end -end diff --git a/lib/pleroma/plugs/ensure_user_key_plug.ex b/lib/pleroma/plugs/ensure_user_key_plug.ex @@ -1,18 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.EnsureUserKeyPlug do - import Plug.Conn - - def init(opts) do - opts - end - - def call(%{assigns: %{user: _}} = conn, _), do: conn - - def call(conn, _) do - conn - |> assign(:user, nil) - end -end diff --git a/lib/pleroma/plugs/expect_authenticated_check_plug.ex b/lib/pleroma/plugs/expect_authenticated_check_plug.ex @@ -1,20 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.ExpectAuthenticatedCheckPlug do - @moduledoc """ - Marks `Pleroma.Plugs.EnsureAuthenticatedPlug` as expected to be executed later in plug chain. - - No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`). - """ - - use Pleroma.Web, :plug - - def init(options), do: options - - @impl true - def perform(conn, _) do - conn - end -end diff --git a/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex b/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex @@ -1,21 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug do - @moduledoc """ - Marks `Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug` as expected to be executed later in plug - chain. - - No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`). - """ - - use Pleroma.Web, :plug - - def init(options), do: options - - @impl true - def perform(conn, _) do - conn - end -end diff --git a/lib/pleroma/plugs/federating_plug.ex b/lib/pleroma/plugs/federating_plug.ex @@ -1,32 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.FederatingPlug do - import Plug.Conn - - def init(options) do - options - end - - def call(conn, _opts) do - if federating?() do - conn - else - fail(conn) - end - end - - def federating?, do: Pleroma.Config.get([:instance, :federating]) - - # Definition for the use in :if_func / :unless_func plug options - def federating?(_conn), do: federating?() - - defp fail(conn) do - conn - |> put_status(404) - |> Phoenix.Controller.put_view(Pleroma.Web.ErrorView) - |> Phoenix.Controller.render("404.json") - |> halt() - end -end diff --git a/lib/pleroma/plugs/frontend_static.ex b/lib/pleroma/plugs/frontend_static.ex @@ -1,55 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.FrontendStatic do - require Pleroma.Constants - - @moduledoc """ - This is a shim to call `Plug.Static` but with runtime `from` configuration`. It dispatches to the different frontends. - """ - @behaviour Plug - - def file_path(path, frontend_type \\ :primary) do - if configuration = Pleroma.Config.get([:frontends, frontend_type]) do - instance_static_path = Pleroma.Config.get([:instance, :static_dir], "instance/static") - - Path.join([ - instance_static_path, - "frontends", - configuration["name"], - configuration["ref"], - path - ]) - else - nil - end - end - - def init(opts) do - opts - |> Keyword.put(:from, "__unconfigured_frontend_static_plug") - |> Plug.Static.init() - |> Map.put(:frontend_type, opts[:frontend_type]) - end - - def call(conn, opts) do - frontend_type = Map.get(opts, :frontend_type, :primary) - path = file_path("", frontend_type) - - if path do - conn - |> call_static(opts, path) - else - conn - end - end - - defp call_static(conn, opts, from) do - opts = - opts - |> Map.put(:from, from) - - Plug.Static.call(conn, opts) - end -end diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex @@ -1,225 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.HTTPSecurityPlug do - alias Pleroma.Config - import Plug.Conn - - require Logger - - def init(opts), do: opts - - def call(conn, _options) do - if Config.get([:http_security, :enabled]) do - conn - |> merge_resp_headers(headers()) - |> maybe_send_sts_header(Config.get([:http_security, :sts])) - else - conn - end - end - - defp headers do - referrer_policy = Config.get([:http_security, :referrer_policy]) - report_uri = Config.get([:http_security, :report_uri]) - - headers = [ - {"x-xss-protection", "1; mode=block"}, - {"x-permitted-cross-domain-policies", "none"}, - {"x-frame-options", "DENY"}, - {"x-content-type-options", "nosniff"}, - {"referrer-policy", referrer_policy}, - {"x-download-options", "noopen"}, - {"content-security-policy", csp_string()} - ] - - if report_uri do - report_group = %{ - "group" => "csp-endpoint", - "max-age" => 10_886_400, - "endpoints" => [ - %{"url" => report_uri} - ] - } - - [{"reply-to", Jason.encode!(report_group)} | headers] - else - headers - end - end - - static_csp_rules = [ - "default-src 'none'", - "base-uri 'self'", - "frame-ancestors 'none'", - "style-src 'self' 'unsafe-inline'", - "font-src 'self'", - "manifest-src 'self'" - ] - - @csp_start [Enum.join(static_csp_rules, ";") <> ";"] - - defp csp_string do - scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme] - static_url = Pleroma.Web.Endpoint.static_url() - websocket_url = Pleroma.Web.Endpoint.websocket_url() - report_uri = Config.get([:http_security, :report_uri]) - - img_src = "img-src 'self' data: blob:" - media_src = "media-src 'self'" - - # Strict multimedia CSP enforcement only when MediaProxy is enabled - {img_src, media_src} = - if Config.get([:media_proxy, :enabled]) && - !Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do - sources = build_csp_multimedia_source_list() - {[img_src, sources], [media_src, sources]} - else - {[img_src, " https:"], [media_src, " https:"]} - end - - connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url] - - connect_src = - if Config.get(:env) == :dev do - [connect_src, " http://localhost:3035/"] - else - connect_src - end - - script_src = - if Config.get(:env) == :dev do - "script-src 'self' 'unsafe-eval'" - else - "script-src 'self'" - end - - report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"] - insecure = if scheme == "https", do: "upgrade-insecure-requests" - - @csp_start - |> add_csp_param(img_src) - |> add_csp_param(media_src) - |> add_csp_param(connect_src) - |> add_csp_param(script_src) - |> add_csp_param(insecure) - |> add_csp_param(report) - |> :erlang.iolist_to_binary() - end - - defp build_csp_from_whitelist([], acc), do: acc - - defp build_csp_from_whitelist([last], acc) do - [build_csp_param_from_whitelist(last) | acc] - end - - defp build_csp_from_whitelist([head | tail], acc) do - build_csp_from_whitelist(tail, [[?\s, build_csp_param_from_whitelist(head)] | acc]) - end - - # TODO: use `build_csp_param/1` after removing support bare domains for media proxy whitelist - defp build_csp_param_from_whitelist("http" <> _ = url) do - build_csp_param(url) - end - - defp build_csp_param_from_whitelist(url), do: url - - defp build_csp_multimedia_source_list do - media_proxy_whitelist = - [:media_proxy, :whitelist] - |> Config.get() - |> build_csp_from_whitelist([]) - - captcha_method = Config.get([Pleroma.Captcha, :method]) - captcha_endpoint = Config.get([captcha_method, :endpoint]) - - base_endpoints = - [ - [:media_proxy, :base_url], - [Pleroma.Upload, :base_url], - [Pleroma.Uploaders.S3, :public_endpoint] - ] - |> Enum.map(&Config.get/1) - - [captcha_endpoint | base_endpoints] - |> Enum.map(&build_csp_param/1) - |> Enum.reduce([], &add_source(&2, &1)) - |> add_source(media_proxy_whitelist) - end - - defp add_source(iodata, nil), do: iodata - defp add_source(iodata, []), do: iodata - defp add_source(iodata, source), do: [[?\s, source] | iodata] - - defp add_csp_param(csp_iodata, nil), do: csp_iodata - - defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata] - - defp build_csp_param(nil), do: nil - - defp build_csp_param(url) when is_binary(url) do - %{host: host, scheme: scheme} = URI.parse(url) - - if scheme do - [scheme, "://", host] - end - end - - def warn_if_disabled do - unless Config.get([:http_security, :enabled]) do - Logger.warn(" - .i;;;;i. - iYcviii;vXY: - .YXi .i1c. - .YC. . in7. - .vc. ...... ;1c. - i7, .. .;1; - i7, .. ... .Y1i - ,7v .6MMM@; .YX, - .7;. ..IMMMMMM1 :t7. - .;Y. ;$MMMMMM9. :tc. - vY. .. .nMMM@MMU. ;1v. - i7i ... .#MM@M@C. .....:71i - it: .... $MMM@9;.,i;;;i,;tti - :t7. ..... 0MMMWv.,iii:::,,;St. - .nC. ..... IMMMQ..,::::::,.,czX. - .ct: ....... .ZMMMI..,:::::::,,:76Y. - c2: ......,i..Y$M@t..:::::::,,..inZY - vov ......:ii..c$MBc..,,,,,,,,,,..iI9i - i9Y ......iii:..7@MA,..,,,,,,,,,....;AA: - iIS. ......:ii::..;@MI....,............;Ez. - .I9. ......:i::::...8M1..................C0z. - .z9; ......:i::::,.. .i:...................zWX. - vbv ......,i::::,,. ................. :AQY - c6Y. .,...,::::,,..:t0@@QY. ................ :8bi - :6S. ..,,...,:::,,,..EMMMMMMI. ............... .;bZ, - :6o, .,,,,..:::,,,..i#MMMMMM#v................. YW2. - .n8i ..,,,,,,,::,,,,.. tMMMMM@C:.................. .1Wn - 7Uc. .:::,,,,,::,,,,.. i1t;,..................... .UEi - 7C...::::::::::::,,,,.. .................... vSi. - ;1;...,,::::::,......... .................. Yz: - v97,......... .voC. - izAotX7777777777777777777777777777777777777777Y7n92: - .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov. - -HTTP Security is disabled. Please re-enable it to prevent users from attacking -your instance and your users via malicious posts: - - config :pleroma, :http_security, enabled: true - ") - end - end - - defp maybe_send_sts_header(conn, true) do - max_age_sts = Config.get([:http_security, :sts_max_age]) - max_age_ct = Config.get([:http_security, :ct_max_age]) - - merge_resp_headers(conn, [ - {"strict-transport-security", "max-age=#{max_age_sts}; includeSubDomains"}, - {"expect-ct", "enforce, max-age=#{max_age_ct}"} - ]) - end - - defp maybe_send_sts_header(conn, _), do: conn -end diff --git a/lib/pleroma/plugs/idempotency_plug.ex b/lib/pleroma/plugs/idempotency_plug.ex @@ -1,84 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.IdempotencyPlug do - import Phoenix.Controller, only: [json: 2] - import Plug.Conn - - @behaviour Plug - - @impl true - def init(opts), do: opts - - # Sending idempotency keys in `GET` and `DELETE` requests has no effect - # and should be avoided, as these requests are idempotent by definition. - - @impl true - def call(%{method: method} = conn, _) when method in ["POST", "PUT", "PATCH"] do - case get_req_header(conn, "idempotency-key") do - [key] -> process_request(conn, key) - _ -> conn - end - end - - def call(conn, _), do: conn - - def process_request(conn, key) do - case Cachex.get(:idempotency_cache, key) do - {:ok, nil} -> - cache_resposnse(conn, key) - - {:ok, record} -> - send_cached(conn, key, record) - - {atom, message} when atom in [:ignore, :error] -> - render_error(conn, message) - end - end - - defp cache_resposnse(conn, key) do - register_before_send(conn, fn conn -> - [request_id] = get_resp_header(conn, "x-request-id") - content_type = get_content_type(conn) - - record = {request_id, content_type, conn.status, conn.resp_body} - {:ok, _} = Cachex.put(:idempotency_cache, key, record) - - conn - |> put_resp_header("idempotency-key", key) - |> put_resp_header("x-original-request-id", request_id) - end) - end - - defp send_cached(conn, key, record) do - {request_id, content_type, status, body} = record - - conn - |> put_resp_header("idempotency-key", key) - |> put_resp_header("idempotent-replayed", "true") - |> put_resp_header("x-original-request-id", request_id) - |> put_resp_content_type(content_type) - |> send_resp(status, body) - |> halt() - end - - defp render_error(conn, message) do - conn - |> put_status(:unprocessable_entity) - |> json(%{error: message}) - |> halt() - end - - defp get_content_type(conn) do - [content_type] = get_resp_header(conn, "content-type") - - if String.contains?(content_type, ";") do - content_type - |> String.split(";") - |> hd() - else - content_type - end - end -end diff --git a/lib/pleroma/plugs/instance_static.ex b/lib/pleroma/plugs/instance_static.ex @@ -1,53 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.InstanceStatic do - require Pleroma.Constants - - @moduledoc """ - This is a shim to call `Plug.Static` but with runtime `from` configuration. - - Mountpoints are defined directly in the module to avoid calling the configuration for every request including non-static ones. - """ - @behaviour Plug - - def file_path(path) do - instance_path = - Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path) - - frontend_path = Pleroma.Plugs.FrontendStatic.file_path(path, :primary) - - (File.exists?(instance_path) && instance_path) || - (frontend_path && File.exists?(frontend_path) && frontend_path) || - Path.join(Application.app_dir(:pleroma, "priv/static/"), path) - end - - def init(opts) do - opts - |> Keyword.put(:from, "__unconfigured_instance_static_plug") - |> Plug.Static.init() - end - - for only <- Pleroma.Constants.static_only_files() do - def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do - call_static( - conn, - opts, - Pleroma.Config.get([:instance, :static_dir], "instance/static") - ) - end - end - - def call(conn, _) do - conn - end - - defp call_static(conn, opts, from) do - opts = - opts - |> Map.put(:from, from) - - Plug.Static.call(conn, opts) - end -end diff --git a/lib/pleroma/plugs/legacy_authentication_plug.ex b/lib/pleroma/plugs/legacy_authentication_plug.ex @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.LegacyAuthenticationPlug do - import Plug.Conn - - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.User - - def init(options) do - options - end - - def call(%{assigns: %{user: %User{}}} = conn, _), do: conn - - def call( - %{ - assigns: %{ - auth_user: %{password_hash: "$6$" <> _ = password_hash} = auth_user, - auth_credentials: %{password: password} - } - } = conn, - _ - ) do - with ^password_hash <- :crypt.crypt(password, password_hash), - {:ok, user} <- - User.reset_password(auth_user, %{password: password, password_confirmation: password}) do - conn - |> assign(:auth_user, user) - |> assign(:user, user) - |> OAuthScopesPlug.skip_plug() - else - _ -> - conn - end - end - - def call(conn, _) do - conn - end -end diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex @@ -1,120 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.OAuthPlug do - import Plug.Conn - import Ecto.Query - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Token - - @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") - - def init(options), do: options - - def call(%{assigns: %{user: %User{}}} = conn, _), do: conn - - def call(%{params: %{"access_token" => access_token}} = conn, _) do - with {:ok, user, token_record} <- fetch_user_and_token(access_token) do - conn - |> assign(:token, token_record) - |> assign(:user, user) - else - _ -> - # token found, but maybe only with app - with {:ok, app, token_record} <- fetch_app_and_token(access_token) do - conn - |> assign(:token, token_record) - |> assign(:app, app) - else - _ -> conn - end - end - end - - def call(conn, _) do - case fetch_token_str(conn) do - {:ok, token} -> - with {:ok, user, token_record} <- fetch_user_and_token(token) do - conn - |> assign(:token, token_record) - |> assign(:user, user) - else - _ -> - # token found, but maybe only with app - with {:ok, app, token_record} <- fetch_app_and_token(token) do - conn - |> assign(:token, token_record) - |> assign(:app, app) - else - _ -> conn - end - end - - _ -> - conn - end - end - - # Gets user by token - # - @spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil - defp fetch_user_and_token(token) do - query = - from(t in Token, - where: t.token == ^token, - join: user in assoc(t, :user), - preload: [user: user] - ) - - # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength - with %Token{user: user} = token_record <- Repo.one(query) do - {:ok, user, token_record} - end - end - - @spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil - defp fetch_app_and_token(token) do - query = - from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app]) - - with %Token{app: app} = token_record <- Repo.one(query) do - {:ok, app, token_record} - end - end - - # Gets token from session by :oauth_token key - # - @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} - defp fetch_token_from_session(conn) do - case get_session(conn, :oauth_token) do - nil -> :no_token_found - token -> {:ok, token} - end - end - - # Gets token from headers - # - @spec fetch_token_str(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} - defp fetch_token_str(%Plug.Conn{} = conn) do - headers = get_req_header(conn, "authorization") - - with :no_token_found <- fetch_token_str(headers), - do: fetch_token_from_session(conn) - end - - @spec fetch_token_str(Keyword.t()) :: :no_token_found | {:ok, String.t()} - defp fetch_token_str([]), do: :no_token_found - - defp fetch_token_str([token | tail]) do - trimmed_token = String.trim(token) - - case Regex.run(@realm_reg, trimmed_token) do - [_, match] -> {:ok, String.trim(match)} - _ -> fetch_token_str(tail) - end - end -end diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex @@ -1,77 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.OAuthScopesPlug do - import Plug.Conn - import Pleroma.Web.Gettext - - alias Pleroma.Config - - use Pleroma.Web, :plug - - def init(%{scopes: _} = options), do: options - - @impl true - def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do - op = options[:op] || :| - token = assigns[:token] - - scopes = transform_scopes(scopes, options) - matched_scopes = (token && filter_descendants(scopes, token.scopes)) || [] - - cond do - token && op == :| && Enum.any?(matched_scopes) -> - conn - - token && op == :& && matched_scopes == scopes -> - conn - - options[:fallback] == :proceed_unauthenticated -> - drop_auth_info(conn) - - true -> - missing_scopes = scopes -- matched_scopes - permissions = Enum.join(missing_scopes, " #{op} ") - - error_message = - dgettext("errors", "Insufficient permissions: %{permissions}.", permissions: permissions) - - conn - |> put_resp_content_type("application/json") - |> send_resp(:forbidden, Jason.encode!(%{error: error_message})) - |> halt() - end - end - - @doc "Drops authentication info from connection" - def drop_auth_info(conn) do - # To simplify debugging, setting a private variable on `conn` if auth info is dropped - conn - |> put_private(:authentication_ignored, true) - |> assign(:user, nil) - |> assign(:token, nil) - end - - @doc "Keeps those of `scopes` which are descendants of `supported_scopes`" - def filter_descendants(scopes, supported_scopes) do - Enum.filter( - scopes, - fn scope -> - Enum.find( - supported_scopes, - &(scope == &1 || String.starts_with?(scope, &1 <> ":")) - ) - end - ) - end - - @doc "Transforms scopes by applying supported options (e.g. :admin)" - def transform_scopes(scopes, options) do - if options[:admin] do - Config.oauth_admin_scopes(scopes) - else - scopes - end - end -end diff --git a/lib/pleroma/plugs/plug_helper.ex b/lib/pleroma/plugs/plug_helper.ex @@ -1,40 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.PlugHelper do - @moduledoc "Pleroma Plug helper" - - @called_plugs_list_id :called_plugs - def called_plugs_list_id, do: @called_plugs_list_id - - @skipped_plugs_list_id :skipped_plugs - def skipped_plugs_list_id, do: @skipped_plugs_list_id - - @doc "Returns `true` if specified plug was called." - def plug_called?(conn, plug_module) do - contained_in_private_list?(conn, @called_plugs_list_id, plug_module) - end - - @doc "Returns `true` if specified plug was explicitly marked as skipped." - def plug_skipped?(conn, plug_module) do - contained_in_private_list?(conn, @skipped_plugs_list_id, plug_module) - end - - @doc "Returns `true` if specified plug was either called or explicitly marked as skipped." - def plug_called_or_skipped?(conn, plug_module) do - plug_called?(conn, plug_module) || plug_skipped?(conn, plug_module) - end - - # Appends plug to known list (skipped, called). Intended to be used from within plug code only. - def append_to_private_list(conn, list_id, value) do - list = conn.private[list_id] || [] - modified_list = Enum.uniq(list ++ [value]) - Plug.Conn.put_private(conn, list_id, modified_list) - end - - defp contained_in_private_list?(conn, private_variable, value) do - list = conn.private[private_variable] || [] - value in list - end -end diff --git a/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex b/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex @@ -1,54 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do - use DynamicSupervisor - - import Cachex.Spec - - def start_link(init_arg) do - DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__) - end - - def add_or_return_limiter(limiter_name, expiration) do - result = - DynamicSupervisor.start_child( - __MODULE__, - %{ - id: String.to_atom("rl_#{limiter_name}"), - start: - {Cachex, :start_link, - [ - limiter_name, - [ - expiration: - expiration( - default: expiration, - interval: check_interval(expiration), - lazy: true - ) - ] - ]} - } - ) - - case result do - {:ok, _pid} = result -> result - {:error, {:already_started, pid}} -> {:ok, pid} - _ -> result - end - end - - @impl true - def init(_init_arg) do - DynamicSupervisor.init(strategy: :one_for_one) - end - - defp check_interval(exp) do - (exp / 2) - |> Kernel.trunc() - |> Kernel.min(5000) - |> Kernel.max(1) - end -end diff --git a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex @@ -1,267 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.RateLimiter do - @moduledoc """ - - ## Configuration - - A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. - The basic configuration is a tuple where: - - * The first element: `scale` (Integer). The time scale in milliseconds. - * The second element: `limit` (Integer). How many requests to limit in the time scale provided. - - It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a - list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated. - - To disable a limiter set its value to `nil`. - - ### Example - - config :pleroma, :rate_limit, - one: {1000, 10}, - two: [{10_000, 10}, {10_000, 50}], - foobar: nil - - Here we have three limiters: - - * `one` which is not over 10req/1s - * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users - * `foobar` which is disabled - - ## Usage - - AllowedSyntax: - - plug(Pleroma.Plugs.RateLimiter, name: :limiter_name) - plug(Pleroma.Plugs.RateLimiter, options) # :name is a required option - - Allowed options: - - * `name` required, always used to fetch the limit values from the config - * `bucket_name` overrides name for counting purposes (e.g. to have a separate limit for a set of actions) - * `params` appends values of specified request params (e.g. ["id"]) to bucket name - - Inside a controller: - - plug(Pleroma.Plugs.RateLimiter, [name: :one] when action == :one) - plug(Pleroma.Plugs.RateLimiter, [name: :two] when action in [:two, :three]) - - plug( - Pleroma.Plugs.RateLimiter, - [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]] - when action in ~w(fav_status unfav_status)a - ) - - or inside a router pipeline: - - pipeline :api do - ... - plug(Pleroma.Plugs.RateLimiter, name: :one) - ... - end - """ - import Pleroma.Web.TranslationHelpers - import Plug.Conn - - alias Pleroma.Config - alias Pleroma.Plugs.RateLimiter.LimiterSupervisor - alias Pleroma.User - - require Logger - - @doc false - def init(plug_opts) do - plug_opts - end - - def call(conn, plug_opts) do - if disabled?(conn) do - handle_disabled(conn) - else - action_settings = action_settings(plug_opts) - handle(conn, action_settings) - end - end - - defp handle_disabled(conn) do - Logger.warn( - "Rate limiter disabled due to forwarded IP not being found. Please ensure your reverse proxy is providing the X-Forwarded-For header or disable the RemoteIP plug/rate limiter." - ) - - conn - end - - defp handle(conn, nil), do: conn - - defp handle(conn, action_settings) do - action_settings - |> incorporate_conn_info(conn) - |> check_rate() - |> case do - {:ok, _count} -> - conn - - {:error, _count} -> - render_throttled_error(conn) - end - end - - def disabled?(conn) do - if Map.has_key?(conn.assigns, :remote_ip_found), - do: !conn.assigns.remote_ip_found, - else: false - end - - @inspect_bucket_not_found {:error, :not_found} - - def inspect_bucket(conn, bucket_name_root, plug_opts) do - with %{name: _} = action_settings <- action_settings(plug_opts) do - action_settings = incorporate_conn_info(action_settings, conn) - bucket_name = make_bucket_name(%{action_settings | name: bucket_name_root}) - key_name = make_key_name(action_settings) - limit = get_limits(action_settings) - - case Cachex.get(bucket_name, key_name) do - {:error, :no_cache} -> - @inspect_bucket_not_found - - {:ok, nil} -> - {0, limit} - - {:ok, value} -> - {value, limit - value} - end - else - _ -> @inspect_bucket_not_found - end - end - - def action_settings(plug_opts) do - with limiter_name when is_atom(limiter_name) <- plug_opts[:name], - limits when not is_nil(limits) <- Config.get([:rate_limit, limiter_name]) do - bucket_name_root = Keyword.get(plug_opts, :bucket_name, limiter_name) - - %{ - name: bucket_name_root, - limits: limits, - opts: plug_opts - } - end - end - - defp check_rate(action_settings) do - bucket_name = make_bucket_name(action_settings) - key_name = make_key_name(action_settings) - limit = get_limits(action_settings) - - case Cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do - {:commit, value} -> - {:ok, value} - - {:ignore, value} -> - {:error, value} - - {:error, :no_cache} -> - initialize_buckets!(action_settings) - check_rate(action_settings) - end - end - - defp increment_value(nil, _limit), do: {:commit, 1} - - defp increment_value(val, limit) when val >= limit, do: {:ignore, val} - - defp increment_value(val, _limit), do: {:commit, val + 1} - - defp incorporate_conn_info(action_settings, %{ - assigns: %{user: %User{id: user_id}}, - params: params - }) do - Map.merge(action_settings, %{ - mode: :user, - conn_params: params, - conn_info: "#{user_id}" - }) - end - - defp incorporate_conn_info(action_settings, %{params: params} = conn) do - Map.merge(action_settings, %{ - mode: :anon, - conn_params: params, - conn_info: "#{ip(conn)}" - }) - end - - defp ip(%{remote_ip: remote_ip}) do - remote_ip - |> Tuple.to_list() - |> Enum.join(".") - end - - defp render_throttled_error(conn) do - conn - |> render_error(:too_many_requests, "Throttled") - |> halt() - end - - defp make_key_name(action_settings) do - "" - |> attach_selected_params(action_settings) - |> attach_identity(action_settings) - end - - defp get_scale(_, {scale, _}), do: scale - - defp get_scale(:anon, [{scale, _}, {_, _}]), do: scale - - defp get_scale(:user, [{_, _}, {scale, _}]), do: scale - - defp get_limits(%{limits: {_scale, limit}}), do: limit - - defp get_limits(%{mode: :user, limits: [_, {_, limit}]}), do: limit - - defp get_limits(%{limits: [{_, limit}, _]}), do: limit - - defp make_bucket_name(%{mode: :user, name: bucket_name_root}), - do: user_bucket_name(bucket_name_root) - - defp make_bucket_name(%{mode: :anon, name: bucket_name_root}), - do: anon_bucket_name(bucket_name_root) - - defp attach_selected_params(input, %{conn_params: conn_params, opts: plug_opts}) do - params_string = - plug_opts - |> Keyword.get(:params, []) - |> Enum.sort() - |> Enum.map(&Map.get(conn_params, &1, "")) - |> Enum.join(":") - - [input, params_string] - |> Enum.join(":") - |> String.replace_leading(":", "") - end - - defp initialize_buckets!(%{name: _name, limits: nil}), do: :ok - - defp initialize_buckets!(%{name: name, limits: limits}) do - {:ok, _pid} = - LimiterSupervisor.add_or_return_limiter(anon_bucket_name(name), get_scale(:anon, limits)) - - {:ok, _pid} = - LimiterSupervisor.add_or_return_limiter(user_bucket_name(name), get_scale(:user, limits)) - - :ok - end - - defp attach_identity(base, %{mode: :user, conn_info: conn_info}), - do: "user:#{base}:#{conn_info}" - - defp attach_identity(base, %{mode: :anon, conn_info: conn_info}), - do: "ip:#{base}:#{conn_info}" - - defp user_bucket_name(bucket_name_root), do: "user:#{bucket_name_root}" |> String.to_atom() - defp anon_bucket_name(bucket_name_root), do: "anon:#{bucket_name_root}" |> String.to_atom() -end diff --git a/lib/pleroma/plugs/rate_limiter/supervisor.ex b/lib/pleroma/plugs/rate_limiter/supervisor.ex @@ -1,20 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.RateLimiter.Supervisor do - use Supervisor - - def start_link(opts) do - Supervisor.start_link(__MODULE__, opts, name: __MODULE__) - end - - def init(_args) do - children = [ - Pleroma.Plugs.RateLimiter.LimiterSupervisor - ] - - opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor] - Supervisor.init(children, opts) - end -end diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex @@ -1,48 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.RemoteIp do - @moduledoc """ - This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. - """ - - alias Pleroma.Config - import Plug.Conn - - @behaviour Plug - - def init(_), do: nil - - def call(%{remote_ip: original_remote_ip} = conn, _) do - if Config.get([__MODULE__, :enabled]) do - %{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts()) - assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip) - else - conn - end - end - - defp remote_ip_opts do - headers = Config.get([__MODULE__, :headers], []) |> MapSet.new() - reserved = Config.get([__MODULE__, :reserved], []) - - proxies = - Config.get([__MODULE__, :proxies], []) - |> Enum.concat(reserved) - |> Enum.map(&maybe_add_cidr/1) - - {headers, proxies} - end - - defp maybe_add_cidr(proxy) when is_binary(proxy) do - proxy = - cond do - "/" in String.codepoints(proxy) -> proxy - InetCidr.v4?(InetCidr.parse_address!(proxy)) -> proxy <> "/32" - InetCidr.v6?(InetCidr.parse_address!(proxy)) -> proxy <> "/128" - end - - InetCidr.parse(proxy, true) - end -end diff --git a/lib/pleroma/plugs/session_authentication_plug.ex b/lib/pleroma/plugs/session_authentication_plug.ex @@ -1,21 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.SessionAuthenticationPlug do - import Plug.Conn - - def init(options) do - options - end - - def call(conn, _) do - with saved_user_id <- get_session(conn, :user_id), - %{auth_user: %{id: ^saved_user_id}} <- conn.assigns do - conn - |> assign(:user, conn.assigns.auth_user) - else - _ -> conn - end - end -end diff --git a/lib/pleroma/plugs/set_format_plug.ex b/lib/pleroma/plugs/set_format_plug.ex @@ -1,24 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.SetFormatPlug do - import Plug.Conn, only: [assign: 3, fetch_query_params: 1] - - def init(_), do: nil - - def call(conn, _) do - case get_format(conn) do - nil -> conn - format -> assign(conn, :format, format) - end - end - - defp get_format(conn) do - conn.private[:phoenix_format] || - case fetch_query_params(conn) do - %{query_params: %{"_format" => format}} -> format - _ -> nil - end - end -end diff --git a/lib/pleroma/plugs/set_locale_plug.ex b/lib/pleroma/plugs/set_locale_plug.ex @@ -1,63 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -# NOTE: this module is based on https://github.com/smeevil/set_locale -defmodule Pleroma.Plugs.SetLocalePlug do - import Plug.Conn, only: [get_req_header: 2, assign: 3] - - def init(_), do: nil - - def call(conn, _) do - locale = get_locale_from_header(conn) || Gettext.get_locale() - Gettext.put_locale(locale) - assign(conn, :locale, locale) - end - - defp get_locale_from_header(conn) do - conn - |> extract_accept_language() - |> Enum.find(&supported_locale?/1) - end - - defp extract_accept_language(conn) do - case get_req_header(conn, "accept-language") do - [value | _] -> - value - |> String.split(",") - |> Enum.map(&parse_language_option/1) - |> Enum.sort(&(&1.quality > &2.quality)) - |> Enum.map(& &1.tag) - |> Enum.reject(&is_nil/1) - |> ensure_language_fallbacks() - - _ -> - [] - end - end - - defp supported_locale?(locale) do - Pleroma.Web.Gettext - |> Gettext.known_locales() - |> Enum.member?(locale) - end - - defp parse_language_option(string) do - captures = Regex.named_captures(~r/^\s?(?<tag>[\w\-]+)(?:;q=(?<quality>[\d\.]+))?$/i, string) - - quality = - case Float.parse(captures["quality"] || "1.0") do - {val, _} -> val - :error -> 1.0 - end - - %{tag: captures["tag"], quality: quality} - end - - defp ensure_language_fallbacks(tags) do - Enum.flat_map(tags, fn tag -> - [language | _] = String.split(tag, "-") - if Enum.member?(tags, language), do: [tag], else: [tag, language] - end) - end -end diff --git a/lib/pleroma/plugs/set_user_session_id_plug.ex b/lib/pleroma/plugs/set_user_session_id_plug.ex @@ -1,19 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.SetUserSessionIdPlug do - import Plug.Conn - alias Pleroma.User - - def init(opts) do - opts - end - - def call(%{assigns: %{user: %User{id: id}}} = conn, _) do - conn - |> put_session(:user_id, id) - end - - def call(conn, _), do: conn -end diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex @@ -1,26 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.StaticFEPlug do - import Plug.Conn - alias Pleroma.Web.StaticFE.StaticFEController - - def init(options), do: options - - def call(conn, _) do - if enabled?() and requires_html?(conn) do - conn - |> StaticFEController.call(:show) - |> halt() - else - conn - end - end - - defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false) - - defp requires_html?(conn) do - Phoenix.Controller.get_format(conn) == "html" - end -end diff --git a/lib/pleroma/plugs/trailing_format_plug.ex b/lib/pleroma/plugs/trailing_format_plug.ex @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.TrailingFormatPlug do - @moduledoc "Calls TrailingFormatPlug for specific paths. Ideally we would just do this in the router, but TrailingFormatPlug needs to be called before Plug.Parsers." - - @behaviour Plug - @paths [ - "/api/statusnet", - "/api/statuses", - "/api/qvitter", - "/api/search", - "/api/account", - "/api/friends", - "/api/mutes", - "/api/media", - "/api/favorites", - "/api/blocks", - "/api/friendships", - "/api/users", - "/users", - "/nodeinfo", - "/api/help", - "/api/externalprofile", - "/notice", - "/api/pleroma/emoji", - "/api/oauth_tokens" - ] - - def init(opts) do - TrailingFormatPlug.init(opts) - end - - for path <- @paths do - def call(%{request_path: unquote(path) <> _} = conn, opts) do - TrailingFormatPlug.call(conn, opts) - end - end - - def call(conn, _opts), do: conn -end diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex @@ -1,107 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.UploadedMedia do - @moduledoc """ - """ - - import Plug.Conn - import Pleroma.Web.Gettext - require Logger - - alias Pleroma.Web.MediaProxy - - @behaviour Plug - # no slashes - @path "media" - - @default_cache_control_header "public, max-age=1209600" - - def init(_opts) do - static_plug_opts = - [ - headers: %{"cache-control" => @default_cache_control_header}, - cache_control_for_etags: @default_cache_control_header - ] - |> Keyword.put(:from, "__unconfigured_media_plug") - |> Keyword.put(:at, "/__unconfigured_media_plug") - |> Plug.Static.init() - - %{static_plug_opts: static_plug_opts} - end - - def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do - conn = - case fetch_query_params(conn) do - %{query_params: %{"name" => name}} = conn -> - name = String.replace(name, "\"", "\\\"") - - put_resp_header(conn, "content-disposition", "filename=\"#{name}\"") - - conn -> - conn - end - |> merge_resp_headers([{"content-security-policy", "sandbox"}]) - - config = Pleroma.Config.get(Pleroma.Upload) - - with uploader <- Keyword.fetch!(config, :uploader), - proxy_remote = Keyword.get(config, :proxy_remote, false), - {:ok, get_method} <- uploader.get_file(file), - false <- media_is_banned(conn, get_method) do - get_media(conn, get_method, proxy_remote, opts) - else - _ -> - conn - |> send_resp(:internal_server_error, dgettext("errors", "Failed")) - |> halt() - end - end - - def call(conn, _opts), do: conn - - defp media_is_banned(%{request_path: path} = _conn, {:static_dir, _}) do - MediaProxy.in_banned_urls(Pleroma.Web.base_url() <> path) - end - - defp media_is_banned(_, {:url, url}), do: MediaProxy.in_banned_urls(url) - - defp media_is_banned(_, _), do: false - - defp get_media(conn, {:static_dir, directory}, _, opts) do - static_opts = - Map.get(opts, :static_plug_opts) - |> Map.put(:at, [@path]) - |> Map.put(:from, directory) - - conn = Plug.Static.call(conn, static_opts) - - if conn.halted do - conn - else - conn - |> send_resp(:not_found, dgettext("errors", "Not found")) - |> halt() - end - end - - defp get_media(conn, {:url, url}, true, _) do - conn - |> Pleroma.ReverseProxy.call(url, Pleroma.Config.get([Pleroma.Upload, :proxy_opts], [])) - end - - defp get_media(conn, {:url, url}, _, _) do - conn - |> Phoenix.Controller.redirect(external: url) - |> halt() - end - - defp get_media(conn, unknown, _, _) do - Logger.error("#{__MODULE__}: Unknown get startegy: #{inspect(unknown)}") - - conn - |> send_resp(:internal_server_error, dgettext("errors", "Internal Error")) - |> halt() - end -end diff --git a/lib/pleroma/plugs/user_enabled_plug.ex b/lib/pleroma/plugs/user_enabled_plug.ex @@ -1,23 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.UserEnabledPlug do - import Plug.Conn - alias Pleroma.User - - def init(options) do - options - end - - def call(%{assigns: %{user: %User{} = user}} = conn, _) do - case User.account_status(user) do - :active -> conn - _ -> assign(conn, :user, nil) - end - end - - def call(conn, _) do - conn - end -end diff --git a/lib/pleroma/plugs/user_fetcher_plug.ex b/lib/pleroma/plugs/user_fetcher_plug.ex @@ -1,21 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.UserFetcherPlug do - alias Pleroma.User - import Plug.Conn - - def init(options) do - options - end - - def call(conn, _options) do - with %{auth_credentials: %{username: username}} <- conn.assigns, - %User{} = user <- User.get_by_nickname_or_email(username) do - assign(conn, :auth_user, user) - else - _ -> conn - end - end -end diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex @@ -1,24 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.UserIsAdminPlug do - import Pleroma.Web.TranslationHelpers - import Plug.Conn - - alias Pleroma.User - - def init(options) do - options - end - - def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do - conn - end - - def call(conn, _) do - conn - |> render_error(:forbidden, "User is not an admin.") - |> halt() - end -end diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex diff --git a/lib/pleroma/tests/auth_test_controller.ex b/lib/pleroma/tests/auth_test_controller.ex @@ -8,9 +8,9 @@ defmodule Pleroma.Tests.AuthTestController do use Pleroma.Web, :controller - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug # Serves only with proper OAuth token (:api and :authenticated_api) # Skipping EnsurePublicOrAuthenticatedPlug has no effect in this case diff --git a/lib/pleroma/uploaders/uploader.ex b/lib/pleroma/uploaders/uploader.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Uploaders.Uploader do @doc """ Instructs how to get the file from the backend. - Used by `Pleroma.Plugs.UploadedMedia`. + Used by `Pleroma.Web.Plugs.UploadedMedia`. """ @type get_method :: {:static_dir, directory :: String.t()} | {:url, url :: String.t()} @callback get_file(file :: String.t()) :: {:ok, get_method()} diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex @@ -0,0 +1,234 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web do + @moduledoc """ + A module that keeps using definitions for controllers, + views and so on. + + This can be used in your application as: + + use Pleroma.Web, :controller + use Pleroma.Web, :view + + The definitions below will be executed for every view, + controller, etc, so keep them short and clean, focused + on imports, uses and aliases. + + Do NOT define functions inside the quoted expressions + below. + """ + + alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug + alias Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.PlugHelper + + def controller do + quote do + use Phoenix.Controller, namespace: Pleroma.Web + + import Plug.Conn + + import Pleroma.Web.Gettext + import Pleroma.Web.Router.Helpers + import Pleroma.Web.TranslationHelpers + + plug(:set_put_layout) + + defp set_put_layout(conn, _) do + put_layout(conn, Pleroma.Config.get(:app_layout, "app.html")) + end + + # Marks plugs intentionally skipped and blocks their execution if present in plugs chain + defp skip_plug(conn, plug_modules) do + plug_modules + |> List.wrap() + |> Enum.reduce( + conn, + fn plug_module, conn -> + try do + plug_module.skip_plug(conn) + rescue + UndefinedFunctionError -> + raise "`#{plug_module}` is not skippable. Append `use Pleroma.Web, :plug` to its code." + end + end + ) + end + + # Executed just before actual controller action, invokes before-action hooks (callbacks) + defp action(conn, params) do + with %{halted: false} = conn <- maybe_drop_authentication_if_oauth_check_ignored(conn), + %{halted: false} = conn <- maybe_perform_public_or_authenticated_check(conn), + %{halted: false} = conn <- maybe_perform_authenticated_check(conn), + %{halted: false} = conn <- maybe_halt_on_missing_oauth_scopes_check(conn) do + super(conn, params) + end + end + + # For non-authenticated API actions, drops auth info if OAuth scopes check was ignored + # (neither performed nor explicitly skipped) + defp maybe_drop_authentication_if_oauth_check_ignored(conn) do + if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and + not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do + OAuthScopesPlug.drop_auth_info(conn) + else + conn + end + end + + # Ensures instance is public -or- user is authenticated if such check was scheduled + defp maybe_perform_public_or_authenticated_check(conn) do + if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) do + EnsurePublicOrAuthenticatedPlug.call(conn, %{}) + else + conn + end + end + + # Ensures user is authenticated if such check was scheduled + # Note: runs prior to action even if it was already executed earlier in plug chain + # (since OAuthScopesPlug has option of proceeding unauthenticated) + defp maybe_perform_authenticated_check(conn) do + if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) do + EnsureAuthenticatedPlug.call(conn, %{}) + else + conn + end + end + + # Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check + defp maybe_halt_on_missing_oauth_scopes_check(conn) do + if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and + not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do + conn + |> render_error( + :forbidden, + "Security violation: OAuth scopes check was neither handled nor explicitly skipped." + ) + |> halt() + else + conn + end + end + end + end + + def view do + quote do + use Phoenix.View, + root: "lib/pleroma/web/templates", + namespace: Pleroma.Web + + # Import convenience functions from controllers + import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] + + import Pleroma.Web.ErrorHelpers + import Pleroma.Web.Gettext + import Pleroma.Web.Router.Helpers + + require Logger + + @doc "Same as `render/3` but wrapped in a rescue block" + def safe_render(view, template, assigns \\ %{}) do + Phoenix.View.render(view, template, assigns) + rescue + error -> + Logger.error( + "#{__MODULE__} failed to render #{inspect({view, template})}\n" <> + Exception.format(:error, error, __STACKTRACE__) + ) + + nil + end + + @doc """ + Same as `render_many/4` but wrapped in rescue block. + """ + def safe_render_many(collection, view, template, assigns \\ %{}) do + Enum.map(collection, fn resource -> + as = Map.get(assigns, :as) || view.__resource__ + assigns = Map.put(assigns, as, resource) + safe_render(view, template, assigns) + end) + |> Enum.filter(& &1) + end + end + end + + def router do + quote do + use Phoenix.Router + # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse + import Plug.Conn + import Phoenix.Controller + end + end + + def channel do + quote do + # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse + use Phoenix.Channel + import Pleroma.Web.Gettext + end + end + + def plug do + quote do + @behaviour Pleroma.Web.Plug + @behaviour Plug + + @doc """ + Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain. + """ + def skip_plug(conn) do + PlugHelper.append_to_private_list( + conn, + PlugHelper.skipped_plugs_list_id(), + __MODULE__ + ) + end + + @impl Plug + @doc """ + Before-plug hook that + * ensures the plug is not skipped + * processes `:if_func` / `:unless_func` functional pre-run conditions + * adds plug to the list of called plugs and calls `perform/2` if checks are passed + + Note: multiple invocations of the same plug (with different or same options) are allowed. + """ + def call(%Plug.Conn{} = conn, options) do + if PlugHelper.plug_skipped?(conn, __MODULE__) || + (options[:if_func] && !options[:if_func].(conn)) || + (options[:unless_func] && options[:unless_func].(conn)) do + conn + else + conn = + PlugHelper.append_to_private_list( + conn, + PlugHelper.called_plugs_list_id(), + __MODULE__ + ) + + apply(__MODULE__, :perform, [conn, options]) + end + end + end + end + + @doc """ + When used, dispatch to the appropriate controller/view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end + + def base_url do + Pleroma.Web.Endpoint.url() + end +end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -9,7 +9,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Delivery alias Pleroma.Object alias Pleroma.Object.Fetcher - alias Pleroma.Plugs.EnsureAuthenticatedPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder @@ -23,8 +22,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ControllerHelper alias Pleroma.Web.Endpoint - alias Pleroma.Web.FederatingPlug alias Pleroma.Web.Federator + alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug + alias Pleroma.Web.Plugs.FederatingPlug require Logger @@ -46,7 +46,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do ) plug( - Pleroma.Plugs.Cache, + Pleroma.Web.Plugs.Cache, [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2] when action in [:activity, :object] ) diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -10,7 +10,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Config alias Pleroma.MFA alias Pleroma.ModerationLog - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Stats alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -21,6 +20,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.AdminAPI.ModerationLogView alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.Endpoint + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Router @users_page_size 50 diff --git a/lib/pleroma/web/admin_api/controllers/chat_controller.ex b/lib/pleroma/web/admin_api/controllers/chat_controller.ex @@ -10,10 +10,10 @@ defmodule Pleroma.Web.AdminAPI.ChatController do alias Pleroma.Chat.MessageReference alias Pleroma.ModerationLog alias Pleroma.Pagination - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.AdminAPI alias Pleroma.Web.CommonAPI alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView + alias Pleroma.Web.Plugs.OAuthScopesPlug require Logger diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do alias Pleroma.Config alias Pleroma.ConfigDB - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update) diff --git a/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do use Pleroma.Web, :controller - alias Pleroma.Plugs.InstanceStatic - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.InstanceDocument + alias Pleroma.Web.Plugs.InstanceStatic + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/admin_api/controllers/invite_controller.ex b/lib/pleroma/web/admin_api/controllers/invite_controller.ex @@ -8,8 +8,8 @@ defmodule Pleroma.Web.AdminAPI.InviteController do import Pleroma.Web.ControllerHelper, only: [json_response: 3] alias Pleroma.Config - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.UserInviteToken + alias Pleroma.Web.Plugs.OAuthScopesPlug require Logger diff --git a/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex b/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do use Pleroma.Web, :controller - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.ApiSpec.Admin, as: Spec alias Pleroma.Web.MediaProxy + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex @@ -0,0 +1,77 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.OAuthAppController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [json_response: 3] + + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.Plugs.OAuthScopesPlug + + require Logger + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(:put_view, Pleroma.Web.MastodonAPI.AppView) + + plug( + OAuthScopesPlug, + %{scopes: ["write"], admin: true} + when action in [:create, :index, :update, :delete] + ) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.OAuthAppOperation + + def index(conn, params) do + search_params = + params + |> Map.take([:client_id, :page, :page_size, :trusted]) + |> Map.put(:client_name, params[:name]) + + with {:ok, apps, count} <- App.search(search_params) do + render(conn, "index.json", + apps: apps, + count: count, + page_size: params.page_size, + admin: true + ) + end + end + + def create(%{body_params: params} = conn, _) do + params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) + + case App.create(params) do + {:ok, app} -> + render(conn, "show.json", app: app, admin: true) + + {:error, changeset} -> + json(conn, App.errors(changeset)) + end + end + + def update(%{body_params: params} = conn, %{id: id}) do + params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) + + with {:ok, app} <- App.update(id, params) do + render(conn, "show.json", app: app, admin: true) + else + {:error, changeset} -> + json(conn, App.errors(changeset)) + + nil -> + json_response(conn, :bad_request, "") + end + end + + def delete(conn, params) do + with {:ok, _app} <- App.destroy(params.id) do + json_response(conn, :no_content, "") + else + _ -> json_response(conn, :bad_request, "") + end + end +end diff --git a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex @@ -1,77 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.OAuthAppController do - use Pleroma.Web, :controller - - import Pleroma.Web.ControllerHelper, only: [json_response: 3] - - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Web.OAuth.App - - require Logger - - plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(:put_view, Pleroma.Web.MastodonAPI.AppView) - - plug( - OAuthScopesPlug, - %{scopes: ["write"], admin: true} - when action in [:create, :index, :update, :delete] - ) - - action_fallback(Pleroma.Web.AdminAPI.FallbackController) - - defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.OAuthAppOperation - - def index(conn, params) do - search_params = - params - |> Map.take([:client_id, :page, :page_size, :trusted]) - |> Map.put(:client_name, params[:name]) - - with {:ok, apps, count} <- App.search(search_params) do - render(conn, "index.json", - apps: apps, - count: count, - page_size: params.page_size, - admin: true - ) - end - end - - def create(%{body_params: params} = conn, _) do - params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) - - case App.create(params) do - {:ok, app} -> - render(conn, "show.json", app: app, admin: true) - - {:error, changeset} -> - json(conn, App.errors(changeset)) - end - end - - def update(%{body_params: params} = conn, %{id: id}) do - params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) - - with {:ok, app} <- App.update(id, params) do - render(conn, "show.json", app: app, admin: true) - else - {:error, changeset} -> - json(conn, App.errors(changeset)) - - nil -> - json_response(conn, :bad_request, "") - end - end - - def delete(conn, params) do - with {:ok, _app} <- App.destroy(params.id) do - json_response(conn, :no_content, "") - else - _ -> json_response(conn, :bad_request, "") - end - end -end diff --git a/lib/pleroma/web/admin_api/controllers/relay_controller.ex b/lib/pleroma/web/admin_api/controllers/relay_controller.ex @@ -6,8 +6,8 @@ defmodule Pleroma.Web.AdminAPI.RelayController do use Pleroma.Web, :controller alias Pleroma.ModerationLog - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.ActivityPub.Relay + alias Pleroma.Web.Plugs.OAuthScopesPlug require Logger diff --git a/lib/pleroma/web/admin_api/controllers/report_controller.ex b/lib/pleroma/web/admin_api/controllers/report_controller.ex @@ -9,12 +9,12 @@ defmodule Pleroma.Web.AdminAPI.ReportController do alias Pleroma.Activity alias Pleroma.ModerationLog - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.ReportNote alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.Report alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug require Logger diff --git a/lib/pleroma/web/admin_api/controllers/status_controller.ex b/lib/pleroma/web/admin_api/controllers/status_controller.ex @@ -7,10 +7,10 @@ defmodule Pleroma.Web.AdminAPI.StatusController do alias Pleroma.Activity alias Pleroma.ModerationLog - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug require Logger diff --git a/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -3,10 +3,10 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Auth.PleromaAuthenticator do - alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.Registration alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Web.Plugs.AuthenticationPlug import Pleroma.Web.Auth.Authenticator, only: [fetch_credentials: 1, fetch_user: 1] diff --git a/lib/pleroma/web/auth/totp_authenticator.ex b/lib/pleroma/web/auth/totp_authenticator.ex @@ -5,8 +5,8 @@ defmodule Pleroma.Web.Auth.TOTPAuthenticator do alias Pleroma.MFA alias Pleroma.MFA.TOTP - alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.User + alias Pleroma.Web.Plugs.AuthenticationPlug @doc "Verify code or check backup code." @spec verify(String.t(), User.t()) :: diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api.ex diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex @@ -12,12 +12,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do alias Pleroma.Conversation.Participation alias Pleroma.Formatter alias Pleroma.Object - alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.MediaProxy + alias Pleroma.Web.Plugs.AuthenticationPlug require Logger require Pleroma.Constants diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex @@ -9,17 +9,17 @@ defmodule Pleroma.Web.Endpoint do socket("/socket", Pleroma.Web.UserSocket) - plug(Pleroma.Plugs.SetLocalePlug) + plug(Pleroma.Web.Plugs.SetLocalePlug) plug(CORSPlug) - plug(Pleroma.Plugs.HTTPSecurityPlug) - plug(Pleroma.Plugs.UploadedMedia) + plug(Pleroma.Web.Plugs.HTTPSecurityPlug) + plug(Pleroma.Web.Plugs.UploadedMedia) @static_cache_control "public, no-cache" # InstanceStatic needs to be before Plug.Static to be able to override shipped-static files # If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well # Cache-control headers are duplicated in case we turn off etags in the future - plug(Pleroma.Plugs.InstanceStatic, + plug(Pleroma.Web.Plugs.InstanceStatic, at: "/", gzip: true, cache_control_for_etags: @static_cache_control, @@ -29,7 +29,7 @@ defmodule Pleroma.Web.Endpoint do ) # Careful! No `only` restriction here, as we don't know what frontends contain. - plug(Pleroma.Plugs.FrontendStatic, + plug(Pleroma.Web.Plugs.FrontendStatic, at: "/", frontend_type: :primary, gzip: true, @@ -41,7 +41,7 @@ defmodule Pleroma.Web.Endpoint do plug(Plug.Static.IndexHtml, at: "/pleroma/admin/") - plug(Pleroma.Plugs.FrontendStatic, + plug(Pleroma.Web.Plugs.FrontendStatic, at: "/pleroma/admin", frontend_type: :admin, gzip: true, @@ -79,7 +79,7 @@ defmodule Pleroma.Web.Endpoint do plug(Phoenix.CodeReloader) end - plug(Pleroma.Plugs.TrailingFormatPlug) + plug(Pleroma.Web.Plugs.TrailingFormatPlug) plug(Plug.RequestId) plug(Plug.Logger, log: :debug) @@ -122,7 +122,7 @@ defmodule Pleroma.Web.Endpoint do extra: extra ) - plug(Pleroma.Plugs.RemoteIp) + plug(Pleroma.Web.Plugs.RemoteIp) defmodule Instrumenter do use Prometheus.PhoenixInstrumenter diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex @@ -0,0 +1,108 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Fallback.RedirectController do + use Pleroma.Web, :controller + + require Logger + + alias Pleroma.User + alias Pleroma.Web.Metadata + alias Pleroma.Web.Preload + + def api_not_implemented(conn, _params) do + conn + |> put_status(404) + |> json(%{error: "Not implemented"}) + end + + def redirector(conn, _params, code \\ 200) do + conn + |> put_resp_content_type("text/html") + |> send_file(code, index_file_path()) + end + + def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do + with %User{} = user <- User.get_cached_by_nickname_or_id(maybe_nickname_or_id) do + redirector_with_meta(conn, %{user: user}) + else + nil -> + redirector(conn, params) + end + end + + def redirector_with_meta(conn, params) do + {:ok, index_content} = File.read(index_file_path()) + + tags = build_tags(conn, params) + preloads = preload_data(conn, params) + + response = + index_content + |> String.replace("<!--server-generated-meta-->", tags <> preloads) + + conn + |> put_resp_content_type("text/html") + |> send_resp(200, response) + end + + def redirector_with_preload(conn, %{"path" => ["pleroma", "admin"]}) do + redirect(conn, to: "/pleroma/admin/") + end + + def redirector_with_preload(conn, params) do + {:ok, index_content} = File.read(index_file_path()) + preloads = preload_data(conn, params) + + response = + index_content + |> String.replace("<!--server-generated-meta-->", preloads) + + conn + |> put_resp_content_type("text/html") + |> send_resp(200, response) + end + + def registration_page(conn, params) do + redirector(conn, params) + end + + def empty(conn, _params) do + conn + |> put_status(204) + |> text("") + end + + defp index_file_path do + Pleroma.Web.Plugs.InstanceStatic.file_path("index.html") + end + + defp build_tags(conn, params) do + try do + Metadata.build_tags(params) + rescue + e -> + Logger.error( + "Metadata rendering for #{conn.request_path} failed.\n" <> + Exception.format(:error, e, __STACKTRACE__) + ) + + "" + end + end + + defp preload_data(conn, params) do + try do + Preload.build_tags(conn, params) + rescue + e -> + Logger.error( + "Preloading for #{conn.request_path} failed.\n" <> + Exception.format(:error, e, __STACKTRACE__) + ) + + "" + end + end +end diff --git a/lib/pleroma/web/fallback_redirect_controller.ex b/lib/pleroma/web/fallback_redirect_controller.ex @@ -1,108 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Fallback.RedirectController do - use Pleroma.Web, :controller - - require Logger - - alias Pleroma.User - alias Pleroma.Web.Metadata - alias Pleroma.Web.Preload - - def api_not_implemented(conn, _params) do - conn - |> put_status(404) - |> json(%{error: "Not implemented"}) - end - - def redirector(conn, _params, code \\ 200) do - conn - |> put_resp_content_type("text/html") - |> send_file(code, index_file_path()) - end - - def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do - with %User{} = user <- User.get_cached_by_nickname_or_id(maybe_nickname_or_id) do - redirector_with_meta(conn, %{user: user}) - else - nil -> - redirector(conn, params) - end - end - - def redirector_with_meta(conn, params) do - {:ok, index_content} = File.read(index_file_path()) - - tags = build_tags(conn, params) - preloads = preload_data(conn, params) - - response = - index_content - |> String.replace("<!--server-generated-meta-->", tags <> preloads) - - conn - |> put_resp_content_type("text/html") - |> send_resp(200, response) - end - - def redirector_with_preload(conn, %{"path" => ["pleroma", "admin"]}) do - redirect(conn, to: "/pleroma/admin/") - end - - def redirector_with_preload(conn, params) do - {:ok, index_content} = File.read(index_file_path()) - preloads = preload_data(conn, params) - - response = - index_content - |> String.replace("<!--server-generated-meta-->", preloads) - - conn - |> put_resp_content_type("text/html") - |> send_resp(200, response) - end - - def registration_page(conn, params) do - redirector(conn, params) - end - - def empty(conn, _params) do - conn - |> put_status(204) - |> text("") - end - - defp index_file_path do - Pleroma.Plugs.InstanceStatic.file_path("index.html") - end - - defp build_tags(conn, params) do - try do - Metadata.build_tags(params) - rescue - e -> - Logger.error( - "Metadata rendering for #{conn.request_path} failed.\n" <> - Exception.format(:error, e, __STACKTRACE__) - ) - - "" - end - end - - defp preload_data(conn, params) do - try do - Preload.build_tags(conn, params) - rescue - e -> - Logger.error( - "Preloading for #{conn.request_path} failed.\n" <> - Exception.format(:error, e, __STACKTRACE__) - ) - - "" - end - end -end diff --git a/lib/pleroma/web/fed_sockets/fed_sockets.ex b/lib/pleroma/web/fed_sockets.ex diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator.ex diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex @@ -5,27 +5,26 @@ defmodule Pleroma.Web.Feed.UserController do use Pleroma.Web, :controller - alias Fallback.RedirectController alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.Feed.FeedView - plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect]) + plug(Pleroma.Web.Plugs.SetFormatPlug when action in [:feed_redirect]) action_fallback(:errors) def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do - RedirectController.redirector_with_meta(conn, %{user: user}) + Pleroma.Web.Fallback.RedirectController.redirector_with_meta(conn, %{user: user}) end end def feed_redirect(%{assigns: %{format: format}} = conn, _params) when format in ["json", "activity+json"] do with %{halted: false} = conn <- - Pleroma.Plugs.EnsureAuthenticatedPlug.call(conn, - unless_func: &Pleroma.Web.FederatingPlug.federating?/1 + Pleroma.Web.Plugs.EnsureAuthenticatedPlug.call(conn, + unless_func: &Pleroma.Web.Plugs.FederatingPlug.federating?/1 ) do ActivityPubController.call(conn, :user) end diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.MastoFEController do use Pleroma.Web, :controller - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :put_settings) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -15,9 +15,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do ] alias Pleroma.Maps - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.RateLimiter alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder @@ -29,6 +26,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.OAuth.OAuthController alias Pleroma.Web.OAuth.OAuthView + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Web.TwitterAPI.TwitterAPI plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex @@ -5,12 +5,12 @@ defmodule Pleroma.Web.MastodonAPI.AppController do use Pleroma.Web, :controller - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Repo alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Scopes alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug action_fallback(Pleroma.Web.MastodonAPI.FallbackController) diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex @@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do action_fallback(Pleroma.Web.MastodonAPI.FallbackController) - plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset) + plug(Pleroma.Web.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset) @local_mastodon_name "Mastodon-Local" diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex @@ -8,8 +8,8 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] alias Pleroma.Conversation.Participation - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Repo + alias Pleroma.Web.Plugs.OAuthScopesPlug action_fallback(Pleroma.Web.MastodonAPI.FallbackController) diff --git a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do plug( :skip_plug, - [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] + [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug] when action == :index ) diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex @@ -5,8 +5,8 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do use Pleroma.Web, :controller - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do use Pleroma.Web, :controller alias Pleroma.Filter - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug @oauth_read_actions [:show, :index] diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do use Pleroma.Web, :controller - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do plug( :skip_plug, - [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] + [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug] when action in [:show, :peers] ) diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.MastodonAPI.ListController do use Pleroma.Web, :controller - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.Plugs.OAuthScopesPlug @oauth_read_actions [:index, :show, :list_accounts] diff --git a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do use Pleroma.Web, :controller - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do plug( :skip_plug, - [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] + [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug] when action in [:empty_array, :empty_object] ) diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -6,9 +6,9 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do use Pleroma.Web, :controller alias Pleroma.Object - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.Plugs.OAuthScopesPlug action_fallback(Pleroma.Web.MastodonAPI.FallbackController) plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -8,8 +8,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] alias Pleroma.Notification - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.MastodonAPI.MastodonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug @oauth_read_actions [:show, :index] diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex @@ -9,9 +9,9 @@ defmodule Pleroma.Web.MastodonAPI.PollController do alias Pleroma.Activity alias Pleroma.Object - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug action_fallback(Pleroma.Web.MastodonAPI.FallbackController) diff --git a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex @@ -3,14 +3,12 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MastodonAPI.ReportController do - alias Pleroma.Plugs.OAuthScopesPlug - use Pleroma.Web, :controller action_fallback(Pleroma.Web.MastodonAPI.FallbackController) plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create) + plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex @@ -7,9 +7,9 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.ScheduledActivity alias Pleroma.Web.MastodonAPI.MastodonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug @oauth_read_actions [:show, :index] diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -6,14 +6,14 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do use Pleroma.Web, :controller alias Pleroma.Activity - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web alias Pleroma.Web.ControllerHelper alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.RateLimiter require Logger diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -13,8 +13,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do alias Pleroma.Activity alias Pleroma.Bookmark alias Pleroma.Object - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.ScheduledActivity alias Pleroma.User @@ -23,9 +21,15 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.ScheduledActivityView + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.RateLimiter plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(:skip_plug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show]) + + plug( + :skip_plug, + Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show] + ) @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []} diff --git a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(:restrict_push_enabled) - plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]}) + plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["push"]}) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SubscriptionOperation diff --git a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.SuggestionController do require Logger plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action == :index) + plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action == :index) def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -10,11 +10,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do alias Pleroma.Config alias Pleroma.Pagination - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.RateLimiter alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.RateLimiter plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:public, :hashtag]) diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy.ex diff --git a/lib/pleroma/web/media_proxy/invalidations/http.ex b/lib/pleroma/web/media_proxy/invalidation/http.ex diff --git a/lib/pleroma/web/media_proxy/invalidations/script.ex b/lib/pleroma/web/media_proxy/invalidation/script.ex diff --git a/lib/pleroma/web/metadata/feed.ex b/lib/pleroma/web/metadata/providers/feed.ex diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/providers/open_graph.ex diff --git a/lib/pleroma/web/metadata/provider.ex b/lib/pleroma/web/metadata/providers/provider.ex diff --git a/lib/pleroma/web/metadata/rel_me.ex b/lib/pleroma/web/metadata/providers/rel_me.ex diff --git a/lib/pleroma/web/metadata/restrict_indexing.ex b/lib/pleroma/web/metadata/providers/restrict_indexing.ex diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/providers/twitter_card.ex diff --git a/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex b/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex @@ -0,0 +1,46 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MongooseIM.MongooseIMController do + use Pleroma.Web, :controller + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.Plugs.AuthenticationPlug + alias Pleroma.Web.Plugs.RateLimiter + + plug(RateLimiter, [name: :authentication] when action in [:user_exists, :check_password]) + plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password) + + def user_exists(conn, %{"user" => username}) do + with %User{} <- Repo.get_by(User, nickname: username, local: true, deactivated: false) do + conn + |> json(true) + else + _ -> + conn + |> put_status(:not_found) + |> json(false) + end + end + + def check_password(conn, %{"user" => username, "pass" => password}) do + with %User{password_hash: password_hash, deactivated: false} <- + Repo.get_by(User, nickname: username, local: true), + true <- AuthenticationPlug.checkpw(password, password_hash) do + conn + |> json(true) + else + false -> + conn + |> put_status(:forbidden) + |> json(false) + + _ -> + conn + |> put_status(:not_found) + |> json(false) + end + end +end diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex @@ -1,46 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MongooseIM.MongooseIMController do - use Pleroma.Web, :controller - - alias Pleroma.Plugs.AuthenticationPlug - alias Pleroma.Plugs.RateLimiter - alias Pleroma.Repo - alias Pleroma.User - - plug(RateLimiter, [name: :authentication] when action in [:user_exists, :check_password]) - plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password) - - def user_exists(conn, %{"user" => username}) do - with %User{} <- Repo.get_by(User, nickname: username, local: true, deactivated: false) do - conn - |> json(true) - else - _ -> - conn - |> put_status(:not_found) - |> json(false) - end - end - - def check_password(conn, %{"user" => username, "pass" => password}) do - with %User{password_hash: password_hash, deactivated: false} <- - Repo.get_by(User, nickname: username, local: true), - true <- AuthenticationPlug.checkpw(password, password_hash) do - conn - |> json(true) - else - false -> - conn - |> put_status(:forbidden) - |> json(false) - - _ -> - conn - |> put_status(:not_found) - |> json(false) - end - end -end diff --git a/lib/pleroma/web/oauth.ex b/lib/pleroma/web/o_auth.ex diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/o_auth/app.ex diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/o_auth/authorization.ex diff --git a/lib/pleroma/web/oauth/fallback_controller.ex b/lib/pleroma/web/o_auth/fallback_controller.ex diff --git a/lib/pleroma/web/oauth/mfa_controller.ex b/lib/pleroma/web/o_auth/mfa_controller.ex diff --git a/lib/pleroma/web/oauth/mfa_view.ex b/lib/pleroma/web/o_auth/mfa_view.ex diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex @@ -0,0 +1,613 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.OAuthController do + use Pleroma.Web, :controller + + alias Pleroma.Helpers.UriHelper + alias Pleroma.Maps + alias Pleroma.MFA + alias Pleroma.Registration + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.Auth.Authenticator + alias Pleroma.Web.ControllerHelper + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.MFAController + alias Pleroma.Web.OAuth.MFAView + alias Pleroma.Web.OAuth.OAuthView + alias Pleroma.Web.OAuth.Scopes + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken + alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken + alias Pleroma.Web.Plugs.RateLimiter + + require Logger + + if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) + + plug(:fetch_session) + plug(:fetch_flash) + + plug(:skip_plug, [ + Pleroma.Web.Plugs.OAuthScopesPlug, + Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug + ]) + + plug(RateLimiter, [name: :authentication] when action == :create_authorization) + + action_fallback(Pleroma.Web.OAuth.FallbackController) + + @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob" + + # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg + def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do + {auth_attrs, params} = Map.pop(params, "authorization") + authorize(conn, Map.merge(params, auth_attrs)) + end + + def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do + if ControllerHelper.truthy_param?(params["force_login"]) do + do_authorize(conn, params) + else + handle_existing_authorization(conn, params) + end + end + + # Note: the token is set in oauth_plug, but the token and client do not always go together. + # For example, MastodonFE's token is set if user requests with another client, + # after user already authorized to MastodonFE. + # So we have to check client and token. + def authorize( + %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, + %{"client_id" => client_id} = params + ) do + with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app), + ^client_id <- t.app.client_id do + handle_existing_authorization(conn, params) + else + _ -> do_authorize(conn, params) + end + end + + def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params) + + defp do_authorize(%Plug.Conn{} = conn, params) do + app = Repo.get_by(App, client_id: params["client_id"]) + available_scopes = (app && app.scopes) || [] + scopes = Scopes.fetch_scopes(params, available_scopes) + + scopes = + if scopes == [] do + available_scopes + else + scopes + end + + # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template + render(conn, Authenticator.auth_template(), %{ + response_type: params["response_type"], + client_id: params["client_id"], + available_scopes: available_scopes, + scopes: scopes, + redirect_uri: params["redirect_uri"], + state: params["state"], + params: params + }) + end + + defp handle_existing_authorization( + %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, + %{"redirect_uri" => @oob_token_redirect_uri} + ) do + render(conn, "oob_token_exists.html", %{token: token}) + end + + defp handle_existing_authorization( + %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, + %{} = params + ) do + app = Repo.preload(token, :app).app + + redirect_uri = + if is_binary(params["redirect_uri"]) do + params["redirect_uri"] + else + default_redirect_uri(app) + end + + if redirect_uri in String.split(app.redirect_uris) do + redirect_uri = redirect_uri(conn, redirect_uri) + url_params = %{access_token: token.token} + url_params = Maps.put_if_present(url_params, :state, params["state"]) + url = UriHelper.modify_uri_params(redirect_uri, url_params) + redirect(conn, external: url) + else + conn + |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) + |> redirect(external: redirect_uri(conn, redirect_uri)) + end + end + + def create_authorization( + %Plug.Conn{} = conn, + %{"authorization" => _} = params, + opts \\ [] + ) do + with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]), + {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do + after_create_authorization(conn, auth, params) + else + error -> + handle_create_authorization_error(conn, error, params) + end + end + + def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ + "authorization" => %{"redirect_uri" => @oob_token_redirect_uri} + }) do + # Enforcing the view to reuse the template when calling from other controllers + conn + |> put_view(OAuthView) + |> render("oob_authorization_created.html", %{auth: auth}) + end + + def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ + "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs + }) do + app = Repo.preload(auth, :app).app + + # An extra safety measure before we redirect (also done in `do_create_authorization/2`) + if redirect_uri in String.split(app.redirect_uris) do + redirect_uri = redirect_uri(conn, redirect_uri) + url_params = %{code: auth.token} + url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"]) + url = UriHelper.modify_uri_params(redirect_uri, url_params) + redirect(conn, external: url) + else + conn + |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) + |> redirect(external: redirect_uri(conn, redirect_uri)) + end + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:error, scopes_issue}, + %{"authorization" => _} = params + ) + when scopes_issue in [:unsupported_scopes, :missing_scopes] do + # Per https://github.com/tootsuite/mastodon/blob/ + # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 + conn + |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes")) + |> put_status(:unauthorized) + |> authorize(params) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:account_status, :confirmation_pending}, + %{"authorization" => _} = params + ) do + conn + |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address")) + |> put_status(:forbidden) + |> authorize(params) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:mfa_required, user, auth, _}, + params + ) do + {:ok, token} = MFA.Token.create(user, auth) + + data = %{ + "mfa_token" => token.token, + "redirect_uri" => params["authorization"]["redirect_uri"], + "state" => params["authorization"]["state"] + } + + MFAController.show(conn, data) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:account_status, :password_reset_pending}, + %{"authorization" => _} = params + ) do + conn + |> put_flash(:error, dgettext("errors", "Password reset is required")) + |> put_status(:forbidden) + |> authorize(params) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:account_status, :deactivated}, + %{"authorization" => _} = params + ) do + conn + |> put_flash(:error, dgettext("errors", "Your account is currently disabled")) + |> put_status(:forbidden) + |> authorize(params) + end + + defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do + Authenticator.handle_error(conn, error) + end + + @doc "Renew access_token with refresh_token" + def token_exchange( + %Plug.Conn{} = conn, + %{"grant_type" => "refresh_token", "refresh_token" => token} = _params + ) do + with {:ok, app} <- Token.Utils.fetch_app(conn), + {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), + {:ok, token} <- RefreshToken.grant(token) do + json(conn, OAuthView.render("token.json", %{user: user, token: token})) + else + _error -> render_invalid_credentials_error(conn) + end + end + + def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do + with {:ok, app} <- Token.Utils.fetch_app(conn), + fixed_token = Token.Utils.fix_padding(params["code"]), + {:ok, auth} <- Authorization.get_by_token(app, fixed_token), + %User{} = user <- User.get_cached_by_id(auth.user_id), + {:ok, token} <- Token.exchange_token(app, auth) do + json(conn, OAuthView.render("token.json", %{user: user, token: token})) + else + error -> + handle_token_exchange_error(conn, error) + end + end + + def token_exchange( + %Plug.Conn{} = conn, + %{"grant_type" => "password"} = params + ) do + with {:ok, %User{} = user} <- Authenticator.get_user(conn), + {:ok, app} <- Token.Utils.fetch_app(conn), + requested_scopes <- Scopes.fetch_scopes(params, app.scopes), + {:ok, token} <- login(user, app, requested_scopes) do + json(conn, OAuthView.render("token.json", %{user: user, token: token})) + else + error -> + handle_token_exchange_error(conn, error) + end + end + + def token_exchange( + %Plug.Conn{} = conn, + %{"grant_type" => "password", "name" => name, "password" => _password} = params + ) do + params = + params + |> Map.delete("name") + |> Map.put("username", name) + + token_exchange(conn, params) + end + + def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do + with {:ok, app} <- Token.Utils.fetch_app(conn), + {:ok, auth} <- Authorization.create_authorization(app, %User{}), + {:ok, token} <- Token.exchange_token(app, auth) do + json(conn, OAuthView.render("token.json", %{token: token})) + else + _error -> + handle_token_exchange_error(conn, :invalid_credentails) + end + end + + # Bad request + def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params) + + defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do + conn + |> put_status(:forbidden) + |> json(build_and_response_mfa_token(user, auth)) + end + + defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do + render_error( + conn, + :forbidden, + "Your account is currently disabled", + %{}, + "account_is_disabled" + ) + end + + defp handle_token_exchange_error( + %Plug.Conn{} = conn, + {:account_status, :password_reset_pending} + ) do + render_error( + conn, + :forbidden, + "Password reset is required", + %{}, + "password_reset_required" + ) + end + + defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do + render_error( + conn, + :forbidden, + "Your login is missing a confirmed e-mail address", + %{}, + "missing_confirmed_email" + ) + end + + defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do + render_error( + conn, + :forbidden, + "Your account is awaiting approval.", + %{}, + "awaiting_approval" + ) + end + + defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do + render_invalid_credentials_error(conn) + end + + def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do + with {:ok, app} <- Token.Utils.fetch_app(conn), + {:ok, _token} <- RevokeToken.revoke(app, params) do + json(conn, %{}) + else + _error -> + # RFC 7009: invalid tokens [in the request] do not cause an error response + json(conn, %{}) + end + end + + def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params) + + # Response for bad request + defp bad_request(%Plug.Conn{} = conn, _) do + render_error(conn, :internal_server_error, "Bad request") + end + + @doc "Prepares OAuth request to provider for Ueberauth" + def prepare_request(%Plug.Conn{} = conn, %{ + "provider" => provider, + "authorization" => auth_attrs + }) do + scope = + auth_attrs + |> Scopes.fetch_scopes([]) + |> Scopes.to_string() + + state = + auth_attrs + |> Map.delete("scopes") + |> Map.put("scope", scope) + |> Jason.encode!() + + params = + auth_attrs + |> Map.drop(~w(scope scopes client_id redirect_uri)) + |> Map.put("state", state) + + # Handing the request to Ueberauth + redirect(conn, to: o_auth_path(conn, :request, provider, params)) + end + + def request(%Plug.Conn{} = conn, params) do + message = + if params["provider"] do + dgettext("errors", "Unsupported OAuth provider: %{provider}.", + provider: params["provider"] + ) + else + dgettext("errors", "Bad OAuth request.") + end + + conn + |> put_flash(:error, message) + |> redirect(to: "/") + end + + def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do + params = callback_params(params) + messages = for e <- Map.get(failure, :errors, []), do: e.message + message = Enum.join(messages, "; ") + + conn + |> put_flash( + :error, + dgettext("errors", "Failed to authenticate: %{message}.", message: message) + ) + |> redirect(external: redirect_uri(conn, params["redirect_uri"])) + end + + def callback(%Plug.Conn{} = conn, params) do + params = callback_params(params) + + with {:ok, registration} <- Authenticator.get_registration(conn) do + auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state)) + + case Repo.get_assoc(registration, :user) do + {:ok, user} -> + create_authorization(conn, %{"authorization" => auth_attrs}, user: user) + + _ -> + registration_params = + Map.merge(auth_attrs, %{ + "nickname" => Registration.nickname(registration), + "email" => Registration.email(registration) + }) + + conn + |> put_session_registration_id(registration.id) + |> registration_details(%{"authorization" => registration_params}) + end + else + error -> + Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns])) + + conn + |> put_flash(:error, dgettext("errors", "Failed to set up user account.")) + |> redirect(external: redirect_uri(conn, params["redirect_uri"])) + end + end + + defp callback_params(%{"state" => state} = params) do + Map.merge(params, Jason.decode!(state)) + end + + def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do + render(conn, "register.html", %{ + client_id: auth_attrs["client_id"], + redirect_uri: auth_attrs["redirect_uri"], + state: auth_attrs["state"], + scopes: Scopes.fetch_scopes(auth_attrs, []), + nickname: auth_attrs["nickname"], + email: auth_attrs["email"] + }) + end + + def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do + with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), + %Registration{} = registration <- Repo.get(Registration, registration_id), + {_, {:ok, auth, _user}} <- + {:create_authorization, do_create_authorization(conn, params)}, + %User{} = user <- Repo.preload(auth, :user).user, + {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do + conn + |> put_session_registration_id(nil) + |> after_create_authorization(auth, params) + else + {:create_authorization, error} -> + {:register, handle_create_authorization_error(conn, error, params)} + + _ -> + {:register, :generic_error} + end + end + + def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do + with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), + %Registration{} = registration <- Repo.get(Registration, registration_id), + {:ok, user} <- Authenticator.create_from_registration(conn, registration) do + conn + |> put_session_registration_id(nil) + |> create_authorization( + params, + user: user + ) + else + {:error, changeset} -> + message = + Enum.map(changeset.errors, fn {field, {error, _}} -> + "#{field} #{error}" + end) + |> Enum.join("; ") + + message = + String.replace( + message, + "ap_id has already been taken", + "nickname has already been taken" + ) + + conn + |> put_status(:forbidden) + |> put_flash(:error, "Error: #{message}.") + |> registration_details(params) + + _ -> + {:register, :generic_error} + end + end + + defp do_create_authorization(conn, auth_attrs, user \\ nil) + + defp do_create_authorization( + %Plug.Conn{} = conn, + %{ + "authorization" => + %{ + "client_id" => client_id, + "redirect_uri" => redirect_uri + } = auth_attrs + }, + user + ) do + with {_, {:ok, %User{} = user}} <- + {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, + %App{} = app <- Repo.get_by(App, client_id: client_id), + true <- redirect_uri in String.split(app.redirect_uris), + requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes), + {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do + {:ok, auth, user} + end + end + + defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes) + when is_list(requested_scopes) do + with {:account_status, :active} <- {:account_status, User.account_status(user)}, + {:ok, scopes} <- validate_scopes(app, requested_scopes), + {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do + {:ok, auth} + end + end + + # Note: intended to be a private function but opened for AccountController that logs in on signup + @doc "If checks pass, creates authorization and token for given user, app and requested scopes." + def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do + with {:ok, auth} <- do_create_authorization(user, app, requested_scopes), + {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)}, + {:ok, token} <- Token.exchange_token(app, auth) do + {:ok, token} + end + end + + # Special case: Local MastodonFE + defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login) + + defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri + + defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id) + + defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), + do: put_session(conn, :registration_id, registration_id) + + defp build_and_response_mfa_token(user, auth) do + with {:ok, token} <- MFA.Token.create(user, auth) do + MFAView.render("mfa_response.json", %{token: token, user: user}) + end + end + + @spec validate_scopes(App.t(), map() | list()) :: + {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} + defp validate_scopes(%App{} = app, params) when is_map(params) do + requested_scopes = Scopes.fetch_scopes(params, app.scopes) + validate_scopes(app, requested_scopes) + end + + defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do + Scopes.validate(requested_scopes, app.scopes) + end + + def default_redirect_uri(%App{} = app) do + app.redirect_uris + |> String.split() + |> Enum.at(0) + end + + defp render_invalid_credentials_error(conn) do + render_error(conn, :bad_request, "Invalid credentials") + end +end diff --git a/lib/pleroma/web/oauth/oauth_view.ex b/lib/pleroma/web/o_auth/o_auth_view.ex diff --git a/lib/pleroma/web/o_auth/scopes.ex b/lib/pleroma/web/o_auth/scopes.ex @@ -0,0 +1,76 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Scopes do + @moduledoc """ + Functions for dealing with scopes. + """ + + alias Pleroma.Web.Plugs.OAuthScopesPlug + + @doc """ + Fetch scopes from request params. + + Note: `scopes` is used by Mastodon — supporting it but sticking to + OAuth's standard `scope` wherever we control it + """ + @spec fetch_scopes(map() | struct(), list()) :: list() + + def fetch_scopes(params, default) do + parse_scopes(params["scope"] || params["scopes"] || params[:scopes], default) + end + + def parse_scopes(scopes, _default) when is_list(scopes) do + Enum.filter(scopes, &(&1 not in [nil, ""])) + end + + def parse_scopes(scopes, default) when is_binary(scopes) do + scopes + |> to_list + |> parse_scopes(default) + end + + def parse_scopes(_, default) do + default + end + + @doc """ + Convert scopes string to list + """ + @spec to_list(binary()) :: [binary()] + def to_list(nil), do: [] + + def to_list(str) do + str + |> String.trim() + |> String.split(~r/[\s,]+/) + end + + @doc """ + Convert scopes list to string + """ + @spec to_string(list()) :: binary() + def to_string(scopes), do: Enum.join(scopes, " ") + + @doc """ + Validates scopes. + """ + @spec validate(list() | nil, list()) :: + {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} + def validate(blank_scopes, _app_scopes) when blank_scopes in [nil, []], + do: {:error, :missing_scopes} + + def validate(scopes, app_scopes) do + case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do + ^scopes -> {:ok, scopes} + _ -> {:error, :unsupported_scopes} + end + end + + def contains_admin_scopes?(scopes) do + scopes + |> OAuthScopesPlug.filter_descendants(["admin"]) + |> Enum.any?() + end +end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/o_auth/token.ex diff --git a/lib/pleroma/web/oauth/token/query.ex b/lib/pleroma/web/o_auth/token/query.ex diff --git a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex b/lib/pleroma/web/o_auth/token/strategy/refresh_token.ex diff --git a/lib/pleroma/web/oauth/token/strategy/revoke.ex b/lib/pleroma/web/o_auth/token/strategy/revoke.ex diff --git a/lib/pleroma/web/oauth/token/utils.ex b/lib/pleroma/web/o_auth/token/utils.ex diff --git a/lib/pleroma/web/o_status/o_status_controller.ex b/lib/pleroma/web/o_status/o_status_controller.ex @@ -0,0 +1,151 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OStatus.OStatusController do + use Pleroma.Web, :controller + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPubController + alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.Endpoint + alias Pleroma.Web.Fallback.RedirectController + alias Pleroma.Web.Metadata.PlayerView + alias Pleroma.Web.Plugs.RateLimiter + alias Pleroma.Web.Router + + plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug, + unless_func: &Pleroma.Web.Plugs.FederatingPlug.federating?/1 + ) + + plug( + RateLimiter, + [name: :ap_routes, params: ["uuid"]] when action in [:object, :activity] + ) + + plug( + Pleroma.Web.Plugs.SetFormatPlug + when action in [:object, :activity, :notice] + ) + + action_fallback(:errors) + + def object(%{assigns: %{format: format}} = conn, _params) + when format in ["json", "activity+json"] do + ActivityPubController.call(conn, :object) + end + + def object(%{assigns: %{format: format}} = conn, _params) do + with id <- Endpoint.url() <> conn.request_path, + {_, %Activity{} = activity} <- + {:activity, Activity.get_create_by_object_ap_id_with_object(id)}, + {_, true} <- {:public?, Visibility.is_public?(activity)} do + case format do + _ -> redirect(conn, to: "/notice/#{activity.id}") + end + else + reason when reason in [{:public?, false}, {:activity, nil}] -> + {:error, :not_found} + + e -> + e + end + end + + def activity(%{assigns: %{format: format}} = conn, _params) + when format in ["json", "activity+json"] do + ActivityPubController.call(conn, :activity) + end + + def activity(%{assigns: %{format: format}} = conn, _params) do + with id <- Endpoint.url() <> conn.request_path, + {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, + {_, true} <- {:public?, Visibility.is_public?(activity)} do + case format do + _ -> redirect(conn, to: "/notice/#{activity.id}") + end + else + reason when reason in [{:public?, false}, {:activity, nil}] -> + {:error, :not_found} + + e -> + e + end + end + + def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do + with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)}, + {_, true} <- {:public?, Visibility.is_public?(activity)}, + %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do + cond do + format in ["json", "activity+json"] -> + if activity.local do + %{data: %{"id" => redirect_url}} = Object.normalize(activity) + redirect(conn, external: redirect_url) + else + {:error, :not_found} + end + + activity.data["type"] == "Create" -> + %Object{} = object = Object.normalize(activity) + + RedirectController.redirector_with_meta( + conn, + %{ + activity_id: activity.id, + object: object, + url: Router.Helpers.o_status_url(Endpoint, :notice, activity.id), + user: user + } + ) + + true -> + RedirectController.redirector(conn, nil) + end + else + reason when reason in [{:public?, false}, {:activity, nil}] -> + conn + |> put_status(404) + |> RedirectController.redirector(nil, 404) + + e -> + e + end + end + + # Returns an HTML embedded <audio> or <video> player suitable for embed iframes. + def notice_player(conn, %{"id" => id}) do + with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id), + true <- Visibility.is_public?(activity), + %Object{} = object <- Object.normalize(activity), + %{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object, + true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do + conn + |> put_layout(:metadata_player) + |> put_resp_header("x-frame-options", "ALLOW") + |> put_resp_header( + "content-security-policy", + "default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;" + ) + |> put_view(PlayerView) + |> render("player.html", url) + else + _error -> + conn + |> put_status(404) + |> RedirectController.redirector(nil, 404) + end + end + + defp errors(conn, {:error, :not_found}) do + render_error(conn, :not_found, "Not found") + end + + defp errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found}) + + defp errors(conn, _) do + render_error(conn, :internal_server_error, "Something went wrong") + end +end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex @@ -1,610 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.OAuthController do - use Pleroma.Web, :controller - - alias Pleroma.Helpers.UriHelper - alias Pleroma.Maps - alias Pleroma.MFA - alias Pleroma.Plugs.RateLimiter - alias Pleroma.Registration - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.Auth.Authenticator - alias Pleroma.Web.ControllerHelper - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.MFAController - alias Pleroma.Web.OAuth.MFAView - alias Pleroma.Web.OAuth.OAuthView - alias Pleroma.Web.OAuth.Scopes - alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken - alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken - - require Logger - - if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) - - plug(:fetch_session) - plug(:fetch_flash) - - plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]) - - plug(RateLimiter, [name: :authentication] when action == :create_authorization) - - action_fallback(Pleroma.Web.OAuth.FallbackController) - - @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob" - - # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg - def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do - {auth_attrs, params} = Map.pop(params, "authorization") - authorize(conn, Map.merge(params, auth_attrs)) - end - - def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do - if ControllerHelper.truthy_param?(params["force_login"]) do - do_authorize(conn, params) - else - handle_existing_authorization(conn, params) - end - end - - # Note: the token is set in oauth_plug, but the token and client do not always go together. - # For example, MastodonFE's token is set if user requests with another client, - # after user already authorized to MastodonFE. - # So we have to check client and token. - def authorize( - %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, - %{"client_id" => client_id} = params - ) do - with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app), - ^client_id <- t.app.client_id do - handle_existing_authorization(conn, params) - else - _ -> do_authorize(conn, params) - end - end - - def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params) - - defp do_authorize(%Plug.Conn{} = conn, params) do - app = Repo.get_by(App, client_id: params["client_id"]) - available_scopes = (app && app.scopes) || [] - scopes = Scopes.fetch_scopes(params, available_scopes) - - scopes = - if scopes == [] do - available_scopes - else - scopes - end - - # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template - render(conn, Authenticator.auth_template(), %{ - response_type: params["response_type"], - client_id: params["client_id"], - available_scopes: available_scopes, - scopes: scopes, - redirect_uri: params["redirect_uri"], - state: params["state"], - params: params - }) - end - - defp handle_existing_authorization( - %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, - %{"redirect_uri" => @oob_token_redirect_uri} - ) do - render(conn, "oob_token_exists.html", %{token: token}) - end - - defp handle_existing_authorization( - %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, - %{} = params - ) do - app = Repo.preload(token, :app).app - - redirect_uri = - if is_binary(params["redirect_uri"]) do - params["redirect_uri"] - else - default_redirect_uri(app) - end - - if redirect_uri in String.split(app.redirect_uris) do - redirect_uri = redirect_uri(conn, redirect_uri) - url_params = %{access_token: token.token} - url_params = Maps.put_if_present(url_params, :state, params["state"]) - url = UriHelper.modify_uri_params(redirect_uri, url_params) - redirect(conn, external: url) - else - conn - |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) - |> redirect(external: redirect_uri(conn, redirect_uri)) - end - end - - def create_authorization( - %Plug.Conn{} = conn, - %{"authorization" => _} = params, - opts \\ [] - ) do - with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]), - {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do - after_create_authorization(conn, auth, params) - else - error -> - handle_create_authorization_error(conn, error, params) - end - end - - def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ - "authorization" => %{"redirect_uri" => @oob_token_redirect_uri} - }) do - # Enforcing the view to reuse the template when calling from other controllers - conn - |> put_view(OAuthView) - |> render("oob_authorization_created.html", %{auth: auth}) - end - - def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ - "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs - }) do - app = Repo.preload(auth, :app).app - - # An extra safety measure before we redirect (also done in `do_create_authorization/2`) - if redirect_uri in String.split(app.redirect_uris) do - redirect_uri = redirect_uri(conn, redirect_uri) - url_params = %{code: auth.token} - url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"]) - url = UriHelper.modify_uri_params(redirect_uri, url_params) - redirect(conn, external: url) - else - conn - |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) - |> redirect(external: redirect_uri(conn, redirect_uri)) - end - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:error, scopes_issue}, - %{"authorization" => _} = params - ) - when scopes_issue in [:unsupported_scopes, :missing_scopes] do - # Per https://github.com/tootsuite/mastodon/blob/ - # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 - conn - |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes")) - |> put_status(:unauthorized) - |> authorize(params) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:account_status, :confirmation_pending}, - %{"authorization" => _} = params - ) do - conn - |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address")) - |> put_status(:forbidden) - |> authorize(params) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:mfa_required, user, auth, _}, - params - ) do - {:ok, token} = MFA.Token.create(user, auth) - - data = %{ - "mfa_token" => token.token, - "redirect_uri" => params["authorization"]["redirect_uri"], - "state" => params["authorization"]["state"] - } - - MFAController.show(conn, data) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:account_status, :password_reset_pending}, - %{"authorization" => _} = params - ) do - conn - |> put_flash(:error, dgettext("errors", "Password reset is required")) - |> put_status(:forbidden) - |> authorize(params) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:account_status, :deactivated}, - %{"authorization" => _} = params - ) do - conn - |> put_flash(:error, dgettext("errors", "Your account is currently disabled")) - |> put_status(:forbidden) - |> authorize(params) - end - - defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do - Authenticator.handle_error(conn, error) - end - - @doc "Renew access_token with refresh_token" - def token_exchange( - %Plug.Conn{} = conn, - %{"grant_type" => "refresh_token", "refresh_token" => token} = _params - ) do - with {:ok, app} <- Token.Utils.fetch_app(conn), - {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), - {:ok, token} <- RefreshToken.grant(token) do - json(conn, OAuthView.render("token.json", %{user: user, token: token})) - else - _error -> render_invalid_credentials_error(conn) - end - end - - def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do - with {:ok, app} <- Token.Utils.fetch_app(conn), - fixed_token = Token.Utils.fix_padding(params["code"]), - {:ok, auth} <- Authorization.get_by_token(app, fixed_token), - %User{} = user <- User.get_cached_by_id(auth.user_id), - {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, OAuthView.render("token.json", %{user: user, token: token})) - else - error -> - handle_token_exchange_error(conn, error) - end - end - - def token_exchange( - %Plug.Conn{} = conn, - %{"grant_type" => "password"} = params - ) do - with {:ok, %User{} = user} <- Authenticator.get_user(conn), - {:ok, app} <- Token.Utils.fetch_app(conn), - requested_scopes <- Scopes.fetch_scopes(params, app.scopes), - {:ok, token} <- login(user, app, requested_scopes) do - json(conn, OAuthView.render("token.json", %{user: user, token: token})) - else - error -> - handle_token_exchange_error(conn, error) - end - end - - def token_exchange( - %Plug.Conn{} = conn, - %{"grant_type" => "password", "name" => name, "password" => _password} = params - ) do - params = - params - |> Map.delete("name") - |> Map.put("username", name) - - token_exchange(conn, params) - end - - def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do - with {:ok, app} <- Token.Utils.fetch_app(conn), - {:ok, auth} <- Authorization.create_authorization(app, %User{}), - {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, OAuthView.render("token.json", %{token: token})) - else - _error -> - handle_token_exchange_error(conn, :invalid_credentails) - end - end - - # Bad request - def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params) - - defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do - conn - |> put_status(:forbidden) - |> json(build_and_response_mfa_token(user, auth)) - end - - defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do - render_error( - conn, - :forbidden, - "Your account is currently disabled", - %{}, - "account_is_disabled" - ) - end - - defp handle_token_exchange_error( - %Plug.Conn{} = conn, - {:account_status, :password_reset_pending} - ) do - render_error( - conn, - :forbidden, - "Password reset is required", - %{}, - "password_reset_required" - ) - end - - defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do - render_error( - conn, - :forbidden, - "Your login is missing a confirmed e-mail address", - %{}, - "missing_confirmed_email" - ) - end - - defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do - render_error( - conn, - :forbidden, - "Your account is awaiting approval.", - %{}, - "awaiting_approval" - ) - end - - defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do - render_invalid_credentials_error(conn) - end - - def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do - with {:ok, app} <- Token.Utils.fetch_app(conn), - {:ok, _token} <- RevokeToken.revoke(app, params) do - json(conn, %{}) - else - _error -> - # RFC 7009: invalid tokens [in the request] do not cause an error response - json(conn, %{}) - end - end - - def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params) - - # Response for bad request - defp bad_request(%Plug.Conn{} = conn, _) do - render_error(conn, :internal_server_error, "Bad request") - end - - @doc "Prepares OAuth request to provider for Ueberauth" - def prepare_request(%Plug.Conn{} = conn, %{ - "provider" => provider, - "authorization" => auth_attrs - }) do - scope = - auth_attrs - |> Scopes.fetch_scopes([]) - |> Scopes.to_string() - - state = - auth_attrs - |> Map.delete("scopes") - |> Map.put("scope", scope) - |> Jason.encode!() - - params = - auth_attrs - |> Map.drop(~w(scope scopes client_id redirect_uri)) - |> Map.put("state", state) - - # Handing the request to Ueberauth - redirect(conn, to: o_auth_path(conn, :request, provider, params)) - end - - def request(%Plug.Conn{} = conn, params) do - message = - if params["provider"] do - dgettext("errors", "Unsupported OAuth provider: %{provider}.", - provider: params["provider"] - ) - else - dgettext("errors", "Bad OAuth request.") - end - - conn - |> put_flash(:error, message) - |> redirect(to: "/") - end - - def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do - params = callback_params(params) - messages = for e <- Map.get(failure, :errors, []), do: e.message - message = Enum.join(messages, "; ") - - conn - |> put_flash( - :error, - dgettext("errors", "Failed to authenticate: %{message}.", message: message) - ) - |> redirect(external: redirect_uri(conn, params["redirect_uri"])) - end - - def callback(%Plug.Conn{} = conn, params) do - params = callback_params(params) - - with {:ok, registration} <- Authenticator.get_registration(conn) do - auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state)) - - case Repo.get_assoc(registration, :user) do - {:ok, user} -> - create_authorization(conn, %{"authorization" => auth_attrs}, user: user) - - _ -> - registration_params = - Map.merge(auth_attrs, %{ - "nickname" => Registration.nickname(registration), - "email" => Registration.email(registration) - }) - - conn - |> put_session_registration_id(registration.id) - |> registration_details(%{"authorization" => registration_params}) - end - else - error -> - Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns])) - - conn - |> put_flash(:error, dgettext("errors", "Failed to set up user account.")) - |> redirect(external: redirect_uri(conn, params["redirect_uri"])) - end - end - - defp callback_params(%{"state" => state} = params) do - Map.merge(params, Jason.decode!(state)) - end - - def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do - render(conn, "register.html", %{ - client_id: auth_attrs["client_id"], - redirect_uri: auth_attrs["redirect_uri"], - state: auth_attrs["state"], - scopes: Scopes.fetch_scopes(auth_attrs, []), - nickname: auth_attrs["nickname"], - email: auth_attrs["email"] - }) - end - - def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do - with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), - %Registration{} = registration <- Repo.get(Registration, registration_id), - {_, {:ok, auth, _user}} <- - {:create_authorization, do_create_authorization(conn, params)}, - %User{} = user <- Repo.preload(auth, :user).user, - {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do - conn - |> put_session_registration_id(nil) - |> after_create_authorization(auth, params) - else - {:create_authorization, error} -> - {:register, handle_create_authorization_error(conn, error, params)} - - _ -> - {:register, :generic_error} - end - end - - def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do - with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), - %Registration{} = registration <- Repo.get(Registration, registration_id), - {:ok, user} <- Authenticator.create_from_registration(conn, registration) do - conn - |> put_session_registration_id(nil) - |> create_authorization( - params, - user: user - ) - else - {:error, changeset} -> - message = - Enum.map(changeset.errors, fn {field, {error, _}} -> - "#{field} #{error}" - end) - |> Enum.join("; ") - - message = - String.replace( - message, - "ap_id has already been taken", - "nickname has already been taken" - ) - - conn - |> put_status(:forbidden) - |> put_flash(:error, "Error: #{message}.") - |> registration_details(params) - - _ -> - {:register, :generic_error} - end - end - - defp do_create_authorization(conn, auth_attrs, user \\ nil) - - defp do_create_authorization( - %Plug.Conn{} = conn, - %{ - "authorization" => - %{ - "client_id" => client_id, - "redirect_uri" => redirect_uri - } = auth_attrs - }, - user - ) do - with {_, {:ok, %User{} = user}} <- - {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, - %App{} = app <- Repo.get_by(App, client_id: client_id), - true <- redirect_uri in String.split(app.redirect_uris), - requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes), - {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do - {:ok, auth, user} - end - end - - defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes) - when is_list(requested_scopes) do - with {:account_status, :active} <- {:account_status, User.account_status(user)}, - {:ok, scopes} <- validate_scopes(app, requested_scopes), - {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do - {:ok, auth} - end - end - - # Note: intended to be a private function but opened for AccountController that logs in on signup - @doc "If checks pass, creates authorization and token for given user, app and requested scopes." - def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do - with {:ok, auth} <- do_create_authorization(user, app, requested_scopes), - {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)}, - {:ok, token} <- Token.exchange_token(app, auth) do - {:ok, token} - end - end - - # Special case: Local MastodonFE - defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login) - - defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri - - defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id) - - defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), - do: put_session(conn, :registration_id, registration_id) - - defp build_and_response_mfa_token(user, auth) do - with {:ok, token} <- MFA.Token.create(user, auth) do - MFAView.render("mfa_response.json", %{token: token, user: user}) - end - end - - @spec validate_scopes(App.t(), map() | list()) :: - {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} - defp validate_scopes(%App{} = app, params) when is_map(params) do - requested_scopes = Scopes.fetch_scopes(params, app.scopes) - validate_scopes(app, requested_scopes) - end - - defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do - Scopes.validate(requested_scopes, app.scopes) - end - - def default_redirect_uri(%App{} = app) do - app.redirect_uris - |> String.split() - |> Enum.at(0) - end - - defp render_invalid_credentials_error(conn) do - render_error(conn, :bad_request, "Invalid credentials") - end -end diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex @@ -1,76 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Scopes do - @moduledoc """ - Functions for dealing with scopes. - """ - - alias Pleroma.Plugs.OAuthScopesPlug - - @doc """ - Fetch scopes from request params. - - Note: `scopes` is used by Mastodon — supporting it but sticking to - OAuth's standard `scope` wherever we control it - """ - @spec fetch_scopes(map() | struct(), list()) :: list() - - def fetch_scopes(params, default) do - parse_scopes(params["scope"] || params["scopes"] || params[:scopes], default) - end - - def parse_scopes(scopes, _default) when is_list(scopes) do - Enum.filter(scopes, &(&1 not in [nil, ""])) - end - - def parse_scopes(scopes, default) when is_binary(scopes) do - scopes - |> to_list - |> parse_scopes(default) - end - - def parse_scopes(_, default) do - default - end - - @doc """ - Convert scopes string to list - """ - @spec to_list(binary()) :: [binary()] - def to_list(nil), do: [] - - def to_list(str) do - str - |> String.trim() - |> String.split(~r/[\s,]+/) - end - - @doc """ - Convert scopes list to string - """ - @spec to_string(list()) :: binary() - def to_string(scopes), do: Enum.join(scopes, " ") - - @doc """ - Validates scopes. - """ - @spec validate(list() | nil, list()) :: - {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} - def validate(blank_scopes, _app_scopes) when blank_scopes in [nil, []], - do: {:error, :missing_scopes} - - def validate(scopes, app_scopes) do - case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do - ^scopes -> {:ok, scopes} - _ -> {:error, :unsupported_scopes} - end - end - - def contains_admin_scopes?(scopes) do - scopes - |> OAuthScopesPlug.filter_descendants(["admin"]) - |> Enum.any?() - end -end diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -1,151 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OStatus.OStatusController do - use Pleroma.Web, :controller - - alias Fallback.RedirectController - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Plugs.RateLimiter - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPubController - alias Pleroma.Web.ActivityPub.Visibility - alias Pleroma.Web.Endpoint - alias Pleroma.Web.Metadata.PlayerView - alias Pleroma.Web.Router - - plug(Pleroma.Plugs.EnsureAuthenticatedPlug, - unless_func: &Pleroma.Web.FederatingPlug.federating?/1 - ) - - plug( - RateLimiter, - [name: :ap_routes, params: ["uuid"]] when action in [:object, :activity] - ) - - plug( - Pleroma.Plugs.SetFormatPlug - when action in [:object, :activity, :notice] - ) - - action_fallback(:errors) - - def object(%{assigns: %{format: format}} = conn, _params) - when format in ["json", "activity+json"] do - ActivityPubController.call(conn, :object) - end - - def object(%{assigns: %{format: format}} = conn, _params) do - with id <- Endpoint.url() <> conn.request_path, - {_, %Activity{} = activity} <- - {:activity, Activity.get_create_by_object_ap_id_with_object(id)}, - {_, true} <- {:public?, Visibility.is_public?(activity)} do - case format do - _ -> redirect(conn, to: "/notice/#{activity.id}") - end - else - reason when reason in [{:public?, false}, {:activity, nil}] -> - {:error, :not_found} - - e -> - e - end - end - - def activity(%{assigns: %{format: format}} = conn, _params) - when format in ["json", "activity+json"] do - ActivityPubController.call(conn, :activity) - end - - def activity(%{assigns: %{format: format}} = conn, _params) do - with id <- Endpoint.url() <> conn.request_path, - {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, - {_, true} <- {:public?, Visibility.is_public?(activity)} do - case format do - _ -> redirect(conn, to: "/notice/#{activity.id}") - end - else - reason when reason in [{:public?, false}, {:activity, nil}] -> - {:error, :not_found} - - e -> - e - end - end - - def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do - with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)}, - {_, true} <- {:public?, Visibility.is_public?(activity)}, - %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do - cond do - format in ["json", "activity+json"] -> - if activity.local do - %{data: %{"id" => redirect_url}} = Object.normalize(activity) - redirect(conn, external: redirect_url) - else - {:error, :not_found} - end - - activity.data["type"] == "Create" -> - %Object{} = object = Object.normalize(activity) - - RedirectController.redirector_with_meta( - conn, - %{ - activity_id: activity.id, - object: object, - url: Router.Helpers.o_status_url(Endpoint, :notice, activity.id), - user: user - } - ) - - true -> - RedirectController.redirector(conn, nil) - end - else - reason when reason in [{:public?, false}, {:activity, nil}] -> - conn - |> put_status(404) - |> RedirectController.redirector(nil, 404) - - e -> - e - end - end - - # Returns an HTML embedded <audio> or <video> player suitable for embed iframes. - def notice_player(conn, %{"id" => id}) do - with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id), - true <- Visibility.is_public?(activity), - %Object{} = object <- Object.normalize(activity), - %{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object, - true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do - conn - |> put_layout(:metadata_player) - |> put_resp_header("x-frame-options", "ALLOW") - |> put_resp_header( - "content-security-policy", - "default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;" - ) - |> put_view(PlayerView) - |> render("player.html", url) - else - _error -> - conn - |> put_status(404) - |> RedirectController.redirector(nil, 404) - end - end - - defp errors(conn, {:error, :not_found}) do - render_error(conn, :not_found, "Not found") - end - - defp errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found}) - - defp errors(conn, _) do - render_error(conn, :internal_server_error, "Something went wrong") - end -end diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -8,12 +8,12 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do import Pleroma.Web.ControllerHelper, only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.RateLimiter alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.RateLimiter require Pleroma.Constants diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -11,12 +11,12 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do alias Pleroma.Chat.MessageReference alias Pleroma.Object alias Pleroma.Pagination - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.CommonAPI alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView alias Pleroma.Web.PleromaAPI.ChatView + alias Pleroma.Web.Plugs.OAuthScopesPlug import Ecto.Query diff --git a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex @@ -8,9 +8,9 @@ defmodule Pleroma.Web.PleromaAPI.ConversationController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] alias Pleroma.Conversation.Participation - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(:put_view, Pleroma.Web.MastodonAPI.ConversationView) diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex @@ -11,7 +11,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileController do plug(Pleroma.Web.ApiSpec.CastAndValidate) plug( - Pleroma.Plugs.OAuthScopesPlug, + Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [ :create, diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do plug(Pleroma.Web.ApiSpec.CastAndValidate) plug( - Pleroma.Plugs.OAuthScopesPlug, + Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [ :import_from_filesystem, @@ -22,8 +22,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do ] ) - @skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] - plug(:skip_plug, @skip_plugs when action in [:index, :show, :archive]) + @skip_plugs [ + Pleroma.Web.Plugs.OAuthScopesPlug, + Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug + ] + plug(:skip_plug, @skip_plugs when action in [:index, :archive, :show]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex @@ -7,9 +7,9 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do alias Pleroma.Activity alias Pleroma.Object - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action in [:create, :delete]) diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do use Pleroma.Web, :controller - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show) diff --git a/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex @@ -6,10 +6,14 @@ defmodule Pleroma.Web.PleromaAPI.NotificationController do use Pleroma.Web, :controller alias Pleroma.Notification - alias Pleroma.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :mark_as_read) + + plug( + Pleroma.Web.Plugs.OAuthScopesPlug, + %{scopes: ["write:notifications"]} when action == :mark_as_read + ) + plug(:put_view, Pleroma.Web.MastodonAPI.NotificationView) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaNotificationOperation diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex @@ -7,10 +7,10 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex @@ -10,8 +10,8 @@ defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationController do alias Pleroma.MFA alias Pleroma.MFA.TOTP - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(OAuthScopesPlug, %{scopes: ["read:security"]} when action in [:settings]) diff --git a/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex @@ -7,9 +7,9 @@ defmodule Pleroma.Web.PleromaAPI.UserImportController do require Logger - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.ApiSpec + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(OAuthScopesPlug, %{scopes: ["follow", "write:follows"]} when action == :follow) plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks) diff --git a/lib/pleroma/web/plug.ex b/lib/pleroma/web/plug.ex @@ -0,0 +1,8 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plug do + # Substitute for `call/2` which is defined with `use Pleroma.Web, :plug` + @callback perform(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t() +end diff --git a/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex b/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex @@ -0,0 +1,60 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlug do + import Plug.Conn + + alias Pleroma.User + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.RateLimiter + + def init(options) do + options + end + + def secret_token do + case Pleroma.Config.get(:admin_token) do + blank when blank in [nil, ""] -> nil + token -> token + end + end + + def call(%{assigns: %{user: %User{}}} = conn, _), do: conn + + def call(conn, _) do + if secret_token() do + authenticate(conn) + else + conn + end + end + + def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do + if admin_token == secret_token() do + assign_admin_user(conn) + else + handle_bad_token(conn) + end + end + + def authenticate(conn) do + token = secret_token() + + case get_req_header(conn, "x-admin-token") do + blank when blank in [[], [""]] -> conn + [^token] -> assign_admin_user(conn) + _ -> handle_bad_token(conn) + end + end + + defp assign_admin_user(conn) do + conn + |> assign(:user, %User{is_admin: true}) + |> OAuthScopesPlug.skip_plug() + end + + defp handle_bad_token(conn) do + RateLimiter.call(conn, name: :authentication) + end +end diff --git a/lib/pleroma/web/plugs/authentication_plug.ex b/lib/pleroma/web/plugs/authentication_plug.ex @@ -0,0 +1,79 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.AuthenticationPlug do + alias Pleroma.User + + import Plug.Conn + + require Logger + + def init(options), do: options + + def checkpw(password, "$6" <> _ = password_hash) do + :crypt.crypt(password, password_hash) == password_hash + end + + def checkpw(password, "$2" <> _ = password_hash) do + # Handle bcrypt passwords for Mastodon migration + Bcrypt.verify_pass(password, password_hash) + end + + def checkpw(password, "$pbkdf2" <> _ = password_hash) do + Pbkdf2.verify_pass(password, password_hash) + end + + def checkpw(_password, _password_hash) do + Logger.error("Password hash not recognized") + false + end + + def maybe_update_password(%User{password_hash: "$2" <> _} = user, password) do + do_update_password(user, password) + end + + def maybe_update_password(%User{password_hash: "$6" <> _} = user, password) do + do_update_password(user, password) + end + + def maybe_update_password(user, _), do: {:ok, user} + + defp do_update_password(user, password) do + user + |> User.password_update_changeset(%{ + "password" => password, + "password_confirmation" => password + }) + |> Pleroma.Repo.update() + end + + def call(%{assigns: %{user: %User{}}} = conn, _), do: conn + + def call( + %{ + assigns: %{ + auth_user: %{password_hash: password_hash} = auth_user, + auth_credentials: %{password: password} + } + } = conn, + _ + ) do + if checkpw(password, password_hash) do + {:ok, auth_user} = maybe_update_password(auth_user, password) + + conn + |> assign(:user, auth_user) + |> Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug() + else + conn + end + end + + def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do + Pbkdf2.no_user_verify() + conn + end + + def call(conn, _), do: conn +end diff --git a/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex b/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex @@ -0,0 +1,25 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.BasicAuthDecoderPlug do + import Plug.Conn + + def init(options) do + options + end + + def call(conn, _opts) do + with ["Basic " <> header] <- get_req_header(conn, "authorization"), + {:ok, userinfo} <- Base.decode64(header), + [username, password] <- String.split(userinfo, ":", parts: 2) do + conn + |> assign(:auth_credentials, %{ + username: username, + password: password + }) + else + _ -> conn + end + end +end diff --git a/lib/pleroma/web/plugs/cache.ex b/lib/pleroma/web/plugs/cache.ex @@ -0,0 +1,136 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.Cache do + @moduledoc """ + Caches successful GET responses. + + To enable the cache add the plug to a router pipeline or controller: + + plug(Pleroma.Web.Plugs.Cache) + + ## Configuration + + To configure the plug you need to pass settings as the second argument to the `plug/2` macro: + + plug(Pleroma.Web.Plugs.Cache, [ttl: nil, query_params: true]) + + Available options: + + - `ttl`: An expiration time (time-to-live). This value should be in milliseconds or `nil` to disable expiration. Defaults to `nil`. + - `query_params`: Take URL query string into account (`true`), ignore it (`false`) or limit to specific params only (list). Defaults to `true`. + - `tracking_fun`: A function that is called on successfull responses, no matter if the request is cached or not. It should accept a conn as the first argument and the value assigned to `tracking_fun_data` as the second. + + Additionally, you can overwrite the TTL inside a controller action by assigning `cache_ttl` to the connection struct: + + def index(conn, _params) do + ttl = 60_000 # one minute + + conn + |> assign(:cache_ttl, ttl) + |> render("index.html") + end + + """ + + import Phoenix.Controller, only: [current_path: 1, json: 2] + import Plug.Conn + + @behaviour Plug + + @defaults %{ttl: nil, query_params: true} + + @impl true + def init([]), do: @defaults + + def init(opts) do + opts = Map.new(opts) + Map.merge(@defaults, opts) + end + + @impl true + def call(%{method: "GET"} = conn, opts) do + key = cache_key(conn, opts) + + case Cachex.get(:web_resp_cache, key) do + {:ok, nil} -> + cache_resp(conn, opts) + + {:ok, {content_type, body, tracking_fun_data}} -> + conn = opts.tracking_fun.(conn, tracking_fun_data) + + send_cached(conn, {content_type, body}) + + {:ok, record} -> + send_cached(conn, record) + + {atom, message} when atom in [:ignore, :error] -> + render_error(conn, message) + end + end + + def call(conn, _), do: conn + + # full path including query params + defp cache_key(conn, %{query_params: true}), do: current_path(conn) + + # request path without query params + defp cache_key(conn, %{query_params: false}), do: conn.request_path + + # request path with specific query params + defp cache_key(conn, %{query_params: query_params}) when is_list(query_params) do + query_string = + conn.params + |> Map.take(query_params) + |> URI.encode_query() + + conn.request_path <> "?" <> query_string + end + + defp cache_resp(conn, opts) do + register_before_send(conn, fn + %{status: 200, resp_body: body} = conn -> + ttl = Map.get(conn.assigns, :cache_ttl, opts.ttl) + key = cache_key(conn, opts) + content_type = content_type(conn) + + conn = + unless opts[:tracking_fun] do + Cachex.put(:web_resp_cache, key, {content_type, body}, ttl: ttl) + conn + else + tracking_fun_data = Map.get(conn.assigns, :tracking_fun_data, nil) + Cachex.put(:web_resp_cache, key, {content_type, body, tracking_fun_data}, ttl: ttl) + + opts.tracking_fun.(conn, tracking_fun_data) + end + + put_resp_header(conn, "x-cache", "MISS from Pleroma") + + conn -> + conn + end) + end + + defp content_type(conn) do + conn + |> Plug.Conn.get_resp_header("content-type") + |> hd() + end + + defp send_cached(conn, {content_type, body}) do + conn + |> put_resp_content_type(content_type, nil) + |> put_resp_header("x-cache", "HIT from Pleroma") + |> send_resp(:ok, body) + |> halt() + end + + defp render_error(conn, message) do + conn + |> put_status(:internal_server_error) + |> json(%{error: message}) + |> halt() + end +end diff --git a/lib/pleroma/plugs/digest.ex b/lib/pleroma/web/plugs/digest_plug.ex diff --git a/lib/pleroma/web/plugs/ensure_authenticated_plug.ex b/lib/pleroma/web/plugs/ensure_authenticated_plug.ex @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.EnsureAuthenticatedPlug do + import Plug.Conn + import Pleroma.Web.TranslationHelpers + + alias Pleroma.User + + use Pleroma.Web, :plug + + def init(options) do + options + end + + @impl true + def perform( + %{ + assigns: %{ + auth_credentials: %{password: _}, + user: %User{multi_factor_authentication_settings: %{enabled: true}} + } + } = conn, + _ + ) do + conn + |> render_error(:forbidden, "Two-factor authentication enabled, you must use a access token.") + |> halt() + end + + def perform(%{assigns: %{user: %User{}}} = conn, _) do + conn + end + + def perform(conn, _) do + conn + |> render_error(:forbidden, "Invalid credentials.") + |> halt() + end +end diff --git a/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex b/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug do + import Pleroma.Web.TranslationHelpers + import Plug.Conn + + alias Pleroma.Config + alias Pleroma.User + + use Pleroma.Web, :plug + + def init(options) do + options + end + + @impl true + def perform(conn, _) do + public? = Config.get!([:instance, :public]) + + case {public?, conn} do + {true, _} -> + conn + + {false, %{assigns: %{user: %User{}}}} -> + conn + + {false, _} -> + conn + |> render_error(:forbidden, "This resource requires authentication.") + |> halt + end + end +end diff --git a/lib/pleroma/web/plugs/ensure_user_key_plug.ex b/lib/pleroma/web/plugs/ensure_user_key_plug.ex @@ -0,0 +1,18 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.EnsureUserKeyPlug do + import Plug.Conn + + def init(opts) do + opts + end + + def call(%{assigns: %{user: _}} = conn, _), do: conn + + def call(conn, _) do + conn + |> assign(:user, nil) + end +end diff --git a/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex b/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug do + @moduledoc """ + Marks `Pleroma.Web.Plugs.EnsureAuthenticatedPlug` as expected to be executed later in plug chain. + + No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`). + """ + + use Pleroma.Web, :plug + + def init(options), do: options + + @impl true + def perform(conn, _) do + conn + end +end diff --git a/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex b/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug do + @moduledoc """ + Marks `Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug` as expected to be executed later in plug + chain. + + No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`). + """ + + use Pleroma.Web, :plug + + def init(options), do: options + + @impl true + def perform(conn, _) do + conn + end +end diff --git a/lib/pleroma/web/plugs/federating_plug.ex b/lib/pleroma/web/plugs/federating_plug.ex @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.FederatingPlug do + import Plug.Conn + + def init(options) do + options + end + + def call(conn, _opts) do + if federating?() do + conn + else + fail(conn) + end + end + + def federating?, do: Pleroma.Config.get([:instance, :federating]) + + # Definition for the use in :if_func / :unless_func plug options + def federating?(_conn), do: federating?() + + defp fail(conn) do + conn + |> put_status(404) + |> Phoenix.Controller.put_view(Pleroma.Web.ErrorView) + |> Phoenix.Controller.render("404.json") + |> halt() + end +end diff --git a/lib/pleroma/web/plugs/frontend_static.ex b/lib/pleroma/web/plugs/frontend_static.ex @@ -0,0 +1,55 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.FrontendStatic do + require Pleroma.Constants + + @moduledoc """ + This is a shim to call `Plug.Static` but with runtime `from` configuration`. It dispatches to the different frontends. + """ + @behaviour Plug + + def file_path(path, frontend_type \\ :primary) do + if configuration = Pleroma.Config.get([:frontends, frontend_type]) do + instance_static_path = Pleroma.Config.get([:instance, :static_dir], "instance/static") + + Path.join([ + instance_static_path, + "frontends", + configuration["name"], + configuration["ref"], + path + ]) + else + nil + end + end + + def init(opts) do + opts + |> Keyword.put(:from, "__unconfigured_frontend_static_plug") + |> Plug.Static.init() + |> Map.put(:frontend_type, opts[:frontend_type]) + end + + def call(conn, opts) do + frontend_type = Map.get(opts, :frontend_type, :primary) + path = file_path("", frontend_type) + + if path do + conn + |> call_static(opts, path) + else + conn + end + end + + defp call_static(conn, opts, from) do + opts = + opts + |> Map.put(:from, from) + + Plug.Static.call(conn, opts) + end +end diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex @@ -0,0 +1,225 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do + alias Pleroma.Config + import Plug.Conn + + require Logger + + def init(opts), do: opts + + def call(conn, _options) do + if Config.get([:http_security, :enabled]) do + conn + |> merge_resp_headers(headers()) + |> maybe_send_sts_header(Config.get([:http_security, :sts])) + else + conn + end + end + + defp headers do + referrer_policy = Config.get([:http_security, :referrer_policy]) + report_uri = Config.get([:http_security, :report_uri]) + + headers = [ + {"x-xss-protection", "1; mode=block"}, + {"x-permitted-cross-domain-policies", "none"}, + {"x-frame-options", "DENY"}, + {"x-content-type-options", "nosniff"}, + {"referrer-policy", referrer_policy}, + {"x-download-options", "noopen"}, + {"content-security-policy", csp_string()} + ] + + if report_uri do + report_group = %{ + "group" => "csp-endpoint", + "max-age" => 10_886_400, + "endpoints" => [ + %{"url" => report_uri} + ] + } + + [{"reply-to", Jason.encode!(report_group)} | headers] + else + headers + end + end + + static_csp_rules = [ + "default-src 'none'", + "base-uri 'self'", + "frame-ancestors 'none'", + "style-src 'self' 'unsafe-inline'", + "font-src 'self'", + "manifest-src 'self'" + ] + + @csp_start [Enum.join(static_csp_rules, ";") <> ";"] + + defp csp_string do + scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme] + static_url = Pleroma.Web.Endpoint.static_url() + websocket_url = Pleroma.Web.Endpoint.websocket_url() + report_uri = Config.get([:http_security, :report_uri]) + + img_src = "img-src 'self' data: blob:" + media_src = "media-src 'self'" + + # Strict multimedia CSP enforcement only when MediaProxy is enabled + {img_src, media_src} = + if Config.get([:media_proxy, :enabled]) && + !Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do + sources = build_csp_multimedia_source_list() + {[img_src, sources], [media_src, sources]} + else + {[img_src, " https:"], [media_src, " https:"]} + end + + connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url] + + connect_src = + if Config.get(:env) == :dev do + [connect_src, " http://localhost:3035/"] + else + connect_src + end + + script_src = + if Config.get(:env) == :dev do + "script-src 'self' 'unsafe-eval'" + else + "script-src 'self'" + end + + report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"] + insecure = if scheme == "https", do: "upgrade-insecure-requests" + + @csp_start + |> add_csp_param(img_src) + |> add_csp_param(media_src) + |> add_csp_param(connect_src) + |> add_csp_param(script_src) + |> add_csp_param(insecure) + |> add_csp_param(report) + |> :erlang.iolist_to_binary() + end + + defp build_csp_from_whitelist([], acc), do: acc + + defp build_csp_from_whitelist([last], acc) do + [build_csp_param_from_whitelist(last) | acc] + end + + defp build_csp_from_whitelist([head | tail], acc) do + build_csp_from_whitelist(tail, [[?\s, build_csp_param_from_whitelist(head)] | acc]) + end + + # TODO: use `build_csp_param/1` after removing support bare domains for media proxy whitelist + defp build_csp_param_from_whitelist("http" <> _ = url) do + build_csp_param(url) + end + + defp build_csp_param_from_whitelist(url), do: url + + defp build_csp_multimedia_source_list do + media_proxy_whitelist = + [:media_proxy, :whitelist] + |> Config.get() + |> build_csp_from_whitelist([]) + + captcha_method = Config.get([Pleroma.Captcha, :method]) + captcha_endpoint = Config.get([captcha_method, :endpoint]) + + base_endpoints = + [ + [:media_proxy, :base_url], + [Pleroma.Upload, :base_url], + [Pleroma.Uploaders.S3, :public_endpoint] + ] + |> Enum.map(&Config.get/1) + + [captcha_endpoint | base_endpoints] + |> Enum.map(&build_csp_param/1) + |> Enum.reduce([], &add_source(&2, &1)) + |> add_source(media_proxy_whitelist) + end + + defp add_source(iodata, nil), do: iodata + defp add_source(iodata, []), do: iodata + defp add_source(iodata, source), do: [[?\s, source] | iodata] + + defp add_csp_param(csp_iodata, nil), do: csp_iodata + + defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata] + + defp build_csp_param(nil), do: nil + + defp build_csp_param(url) when is_binary(url) do + %{host: host, scheme: scheme} = URI.parse(url) + + if scheme do + [scheme, "://", host] + end + end + + def warn_if_disabled do + unless Config.get([:http_security, :enabled]) do + Logger.warn(" + .i;;;;i. + iYcviii;vXY: + .YXi .i1c. + .YC. . in7. + .vc. ...... ;1c. + i7, .. .;1; + i7, .. ... .Y1i + ,7v .6MMM@; .YX, + .7;. ..IMMMMMM1 :t7. + .;Y. ;$MMMMMM9. :tc. + vY. .. .nMMM@MMU. ;1v. + i7i ... .#MM@M@C. .....:71i + it: .... $MMM@9;.,i;;;i,;tti + :t7. ..... 0MMMWv.,iii:::,,;St. + .nC. ..... IMMMQ..,::::::,.,czX. + .ct: ....... .ZMMMI..,:::::::,,:76Y. + c2: ......,i..Y$M@t..:::::::,,..inZY + vov ......:ii..c$MBc..,,,,,,,,,,..iI9i + i9Y ......iii:..7@MA,..,,,,,,,,,....;AA: + iIS. ......:ii::..;@MI....,............;Ez. + .I9. ......:i::::...8M1..................C0z. + .z9; ......:i::::,.. .i:...................zWX. + vbv ......,i::::,,. ................. :AQY + c6Y. .,...,::::,,..:t0@@QY. ................ :8bi + :6S. ..,,...,:::,,,..EMMMMMMI. ............... .;bZ, + :6o, .,,,,..:::,,,..i#MMMMMM#v................. YW2. + .n8i ..,,,,,,,::,,,,.. tMMMMM@C:.................. .1Wn + 7Uc. .:::,,,,,::,,,,.. i1t;,..................... .UEi + 7C...::::::::::::,,,,.. .................... vSi. + ;1;...,,::::::,......... .................. Yz: + v97,......... .voC. + izAotX7777777777777777777777777777777777777777Y7n92: + .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov. + +HTTP Security is disabled. Please re-enable it to prevent users from attacking +your instance and your users via malicious posts: + + config :pleroma, :http_security, enabled: true + ") + end + end + + defp maybe_send_sts_header(conn, true) do + max_age_sts = Config.get([:http_security, :sts_max_age]) + max_age_ct = Config.get([:http_security, :ct_max_age]) + + merge_resp_headers(conn, [ + {"strict-transport-security", "max-age=#{max_age_sts}; includeSubDomains"}, + {"expect-ct", "enforce, max-age=#{max_age_ct}"} + ]) + end + + defp maybe_send_sts_header(conn, _), do: conn +end diff --git a/lib/pleroma/plugs/http_signature.ex b/lib/pleroma/web/plugs/http_signature_plug.ex diff --git a/lib/pleroma/web/plugs/idempotency_plug.ex b/lib/pleroma/web/plugs/idempotency_plug.ex @@ -0,0 +1,84 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.IdempotencyPlug do + import Phoenix.Controller, only: [json: 2] + import Plug.Conn + + @behaviour Plug + + @impl true + def init(opts), do: opts + + # Sending idempotency keys in `GET` and `DELETE` requests has no effect + # and should be avoided, as these requests are idempotent by definition. + + @impl true + def call(%{method: method} = conn, _) when method in ["POST", "PUT", "PATCH"] do + case get_req_header(conn, "idempotency-key") do + [key] -> process_request(conn, key) + _ -> conn + end + end + + def call(conn, _), do: conn + + def process_request(conn, key) do + case Cachex.get(:idempotency_cache, key) do + {:ok, nil} -> + cache_resposnse(conn, key) + + {:ok, record} -> + send_cached(conn, key, record) + + {atom, message} when atom in [:ignore, :error] -> + render_error(conn, message) + end + end + + defp cache_resposnse(conn, key) do + register_before_send(conn, fn conn -> + [request_id] = get_resp_header(conn, "x-request-id") + content_type = get_content_type(conn) + + record = {request_id, content_type, conn.status, conn.resp_body} + {:ok, _} = Cachex.put(:idempotency_cache, key, record) + + conn + |> put_resp_header("idempotency-key", key) + |> put_resp_header("x-original-request-id", request_id) + end) + end + + defp send_cached(conn, key, record) do + {request_id, content_type, status, body} = record + + conn + |> put_resp_header("idempotency-key", key) + |> put_resp_header("idempotent-replayed", "true") + |> put_resp_header("x-original-request-id", request_id) + |> put_resp_content_type(content_type) + |> send_resp(status, body) + |> halt() + end + + defp render_error(conn, message) do + conn + |> put_status(:unprocessable_entity) + |> json(%{error: message}) + |> halt() + end + + defp get_content_type(conn) do + [content_type] = get_resp_header(conn, "content-type") + + if String.contains?(content_type, ";") do + content_type + |> String.split(";") + |> hd() + else + content_type + end + end +end diff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.InstanceStatic do + require Pleroma.Constants + + @moduledoc """ + This is a shim to call `Plug.Static` but with runtime `from` configuration. + + Mountpoints are defined directly in the module to avoid calling the configuration for every request including non-static ones. + """ + @behaviour Plug + + def file_path(path) do + instance_path = + Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path) + + frontend_path = Pleroma.Web.Plugs.FrontendStatic.file_path(path, :primary) + + (File.exists?(instance_path) && instance_path) || + (frontend_path && File.exists?(frontend_path) && frontend_path) || + Path.join(Application.app_dir(:pleroma, "priv/static/"), path) + end + + def init(opts) do + opts + |> Keyword.put(:from, "__unconfigured_instance_static_plug") + |> Plug.Static.init() + end + + for only <- Pleroma.Constants.static_only_files() do + def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do + call_static( + conn, + opts, + Pleroma.Config.get([:instance, :static_dir], "instance/static") + ) + end + end + + def call(conn, _) do + conn + end + + defp call_static(conn, opts, from) do + opts = + opts + |> Map.put(:from, from) + + Plug.Static.call(conn, opts) + end +end diff --git a/lib/pleroma/web/plugs/legacy_authentication_plug.ex b/lib/pleroma/web/plugs/legacy_authentication_plug.ex @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.LegacyAuthenticationPlug do + import Plug.Conn + + alias Pleroma.User + + def init(options) do + options + end + + def call(%{assigns: %{user: %User{}}} = conn, _), do: conn + + def call( + %{ + assigns: %{ + auth_user: %{password_hash: "$6$" <> _ = password_hash} = auth_user, + auth_credentials: %{password: password} + } + } = conn, + _ + ) do + with ^password_hash <- :crypt.crypt(password, password_hash), + {:ok, user} <- + User.reset_password(auth_user, %{password: password, password_confirmation: password}) do + conn + |> assign(:auth_user, user) + |> assign(:user, user) + |> Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug() + else + _ -> + conn + end + end + + def call(conn, _) do + conn + end +end diff --git a/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex diff --git a/lib/pleroma/web/plugs/o_auth_plug.ex b/lib/pleroma/web/plugs/o_auth_plug.ex @@ -0,0 +1,120 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.OAuthPlug do + import Plug.Conn + import Ecto.Query + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Token + + @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") + + def init(options), do: options + + def call(%{assigns: %{user: %User{}}} = conn, _), do: conn + + def call(%{params: %{"access_token" => access_token}} = conn, _) do + with {:ok, user, token_record} <- fetch_user_and_token(access_token) do + conn + |> assign(:token, token_record) + |> assign(:user, user) + else + _ -> + # token found, but maybe only with app + with {:ok, app, token_record} <- fetch_app_and_token(access_token) do + conn + |> assign(:token, token_record) + |> assign(:app, app) + else + _ -> conn + end + end + end + + def call(conn, _) do + case fetch_token_str(conn) do + {:ok, token} -> + with {:ok, user, token_record} <- fetch_user_and_token(token) do + conn + |> assign(:token, token_record) + |> assign(:user, user) + else + _ -> + # token found, but maybe only with app + with {:ok, app, token_record} <- fetch_app_and_token(token) do + conn + |> assign(:token, token_record) + |> assign(:app, app) + else + _ -> conn + end + end + + _ -> + conn + end + end + + # Gets user by token + # + @spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil + defp fetch_user_and_token(token) do + query = + from(t in Token, + where: t.token == ^token, + join: user in assoc(t, :user), + preload: [user: user] + ) + + # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength + with %Token{user: user} = token_record <- Repo.one(query) do + {:ok, user, token_record} + end + end + + @spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil + defp fetch_app_and_token(token) do + query = + from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app]) + + with %Token{app: app} = token_record <- Repo.one(query) do + {:ok, app, token_record} + end + end + + # Gets token from session by :oauth_token key + # + @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token_from_session(conn) do + case get_session(conn, :oauth_token) do + nil -> :no_token_found + token -> {:ok, token} + end + end + + # Gets token from headers + # + @spec fetch_token_str(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token_str(%Plug.Conn{} = conn) do + headers = get_req_header(conn, "authorization") + + with :no_token_found <- fetch_token_str(headers), + do: fetch_token_from_session(conn) + end + + @spec fetch_token_str(Keyword.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token_str([]), do: :no_token_found + + defp fetch_token_str([token | tail]) do + trimmed_token = String.trim(token) + + case Regex.run(@realm_reg, trimmed_token) do + [_, match] -> {:ok, String.trim(match)} + _ -> fetch_token_str(tail) + end + end +end diff --git a/lib/pleroma/web/plugs/o_auth_scopes_plug.ex b/lib/pleroma/web/plugs/o_auth_scopes_plug.ex @@ -0,0 +1,77 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.OAuthScopesPlug do + import Plug.Conn + import Pleroma.Web.Gettext + + alias Pleroma.Config + + use Pleroma.Web, :plug + + def init(%{scopes: _} = options), do: options + + @impl true + def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do + op = options[:op] || :| + token = assigns[:token] + + scopes = transform_scopes(scopes, options) + matched_scopes = (token && filter_descendants(scopes, token.scopes)) || [] + + cond do + token && op == :| && Enum.any?(matched_scopes) -> + conn + + token && op == :& && matched_scopes == scopes -> + conn + + options[:fallback] == :proceed_unauthenticated -> + drop_auth_info(conn) + + true -> + missing_scopes = scopes -- matched_scopes + permissions = Enum.join(missing_scopes, " #{op} ") + + error_message = + dgettext("errors", "Insufficient permissions: %{permissions}.", permissions: permissions) + + conn + |> put_resp_content_type("application/json") + |> send_resp(:forbidden, Jason.encode!(%{error: error_message})) + |> halt() + end + end + + @doc "Drops authentication info from connection" + def drop_auth_info(conn) do + # To simplify debugging, setting a private variable on `conn` if auth info is dropped + conn + |> put_private(:authentication_ignored, true) + |> assign(:user, nil) + |> assign(:token, nil) + end + + @doc "Keeps those of `scopes` which are descendants of `supported_scopes`" + def filter_descendants(scopes, supported_scopes) do + Enum.filter( + scopes, + fn scope -> + Enum.find( + supported_scopes, + &(scope == &1 || String.starts_with?(scope, &1 <> ":")) + ) + end + ) + end + + @doc "Transforms scopes by applying supported options (e.g. :admin)" + def transform_scopes(scopes, options) do + if options[:admin] do + Config.oauth_admin_scopes(scopes) + else + scopes + end + end +end diff --git a/lib/pleroma/web/plugs/plug_helper.ex b/lib/pleroma/web/plugs/plug_helper.ex @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.PlugHelper do + @moduledoc "Pleroma Plug helper" + + @called_plugs_list_id :called_plugs + def called_plugs_list_id, do: @called_plugs_list_id + + @skipped_plugs_list_id :skipped_plugs + def skipped_plugs_list_id, do: @skipped_plugs_list_id + + @doc "Returns `true` if specified plug was called." + def plug_called?(conn, plug_module) do + contained_in_private_list?(conn, @called_plugs_list_id, plug_module) + end + + @doc "Returns `true` if specified plug was explicitly marked as skipped." + def plug_skipped?(conn, plug_module) do + contained_in_private_list?(conn, @skipped_plugs_list_id, plug_module) + end + + @doc "Returns `true` if specified plug was either called or explicitly marked as skipped." + def plug_called_or_skipped?(conn, plug_module) do + plug_called?(conn, plug_module) || plug_skipped?(conn, plug_module) + end + + # Appends plug to known list (skipped, called). Intended to be used from within plug code only. + def append_to_private_list(conn, list_id, value) do + list = conn.private[list_id] || [] + modified_list = Enum.uniq(list ++ [value]) + Plug.Conn.put_private(conn, list_id, modified_list) + end + + defp contained_in_private_list?(conn, private_variable, value) do + list = conn.private[private_variable] || [] + value in list + end +end diff --git a/lib/pleroma/web/plugs/rate_limiter.ex b/lib/pleroma/web/plugs/rate_limiter.ex @@ -0,0 +1,267 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.RateLimiter do + @moduledoc """ + + ## Configuration + + A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. + The basic configuration is a tuple where: + + * The first element: `scale` (Integer). The time scale in milliseconds. + * The second element: `limit` (Integer). How many requests to limit in the time scale provided. + + It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a + list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated. + + To disable a limiter set its value to `nil`. + + ### Example + + config :pleroma, :rate_limit, + one: {1000, 10}, + two: [{10_000, 10}, {10_000, 50}], + foobar: nil + + Here we have three limiters: + + * `one` which is not over 10req/1s + * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users + * `foobar` which is disabled + + ## Usage + + AllowedSyntax: + + plug(Pleroma.Web.Plugs.RateLimiter, name: :limiter_name) + plug(Pleroma.Web.Plugs.RateLimiter, options) # :name is a required option + + Allowed options: + + * `name` required, always used to fetch the limit values from the config + * `bucket_name` overrides name for counting purposes (e.g. to have a separate limit for a set of actions) + * `params` appends values of specified request params (e.g. ["id"]) to bucket name + + Inside a controller: + + plug(Pleroma.Web.Plugs.RateLimiter, [name: :one] when action == :one) + plug(Pleroma.Web.Plugs.RateLimiter, [name: :two] when action in [:two, :three]) + + plug( + Pleroma.Web.Plugs.RateLimiter, + [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]] + when action in ~w(fav_status unfav_status)a + ) + + or inside a router pipeline: + + pipeline :api do + ... + plug(Pleroma.Web.Plugs.RateLimiter, name: :one) + ... + end + """ + import Pleroma.Web.TranslationHelpers + import Plug.Conn + + alias Pleroma.Config + alias Pleroma.User + alias Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor + + require Logger + + @doc false + def init(plug_opts) do + plug_opts + end + + def call(conn, plug_opts) do + if disabled?(conn) do + handle_disabled(conn) + else + action_settings = action_settings(plug_opts) + handle(conn, action_settings) + end + end + + defp handle_disabled(conn) do + Logger.warn( + "Rate limiter disabled due to forwarded IP not being found. Please ensure your reverse proxy is providing the X-Forwarded-For header or disable the RemoteIP plug/rate limiter." + ) + + conn + end + + defp handle(conn, nil), do: conn + + defp handle(conn, action_settings) do + action_settings + |> incorporate_conn_info(conn) + |> check_rate() + |> case do + {:ok, _count} -> + conn + + {:error, _count} -> + render_throttled_error(conn) + end + end + + def disabled?(conn) do + if Map.has_key?(conn.assigns, :remote_ip_found), + do: !conn.assigns.remote_ip_found, + else: false + end + + @inspect_bucket_not_found {:error, :not_found} + + def inspect_bucket(conn, bucket_name_root, plug_opts) do + with %{name: _} = action_settings <- action_settings(plug_opts) do + action_settings = incorporate_conn_info(action_settings, conn) + bucket_name = make_bucket_name(%{action_settings | name: bucket_name_root}) + key_name = make_key_name(action_settings) + limit = get_limits(action_settings) + + case Cachex.get(bucket_name, key_name) do + {:error, :no_cache} -> + @inspect_bucket_not_found + + {:ok, nil} -> + {0, limit} + + {:ok, value} -> + {value, limit - value} + end + else + _ -> @inspect_bucket_not_found + end + end + + def action_settings(plug_opts) do + with limiter_name when is_atom(limiter_name) <- plug_opts[:name], + limits when not is_nil(limits) <- Config.get([:rate_limit, limiter_name]) do + bucket_name_root = Keyword.get(plug_opts, :bucket_name, limiter_name) + + %{ + name: bucket_name_root, + limits: limits, + opts: plug_opts + } + end + end + + defp check_rate(action_settings) do + bucket_name = make_bucket_name(action_settings) + key_name = make_key_name(action_settings) + limit = get_limits(action_settings) + + case Cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do + {:commit, value} -> + {:ok, value} + + {:ignore, value} -> + {:error, value} + + {:error, :no_cache} -> + initialize_buckets!(action_settings) + check_rate(action_settings) + end + end + + defp increment_value(nil, _limit), do: {:commit, 1} + + defp increment_value(val, limit) when val >= limit, do: {:ignore, val} + + defp increment_value(val, _limit), do: {:commit, val + 1} + + defp incorporate_conn_info(action_settings, %{ + assigns: %{user: %User{id: user_id}}, + params: params + }) do + Map.merge(action_settings, %{ + mode: :user, + conn_params: params, + conn_info: "#{user_id}" + }) + end + + defp incorporate_conn_info(action_settings, %{params: params} = conn) do + Map.merge(action_settings, %{ + mode: :anon, + conn_params: params, + conn_info: "#{ip(conn)}" + }) + end + + defp ip(%{remote_ip: remote_ip}) do + remote_ip + |> Tuple.to_list() + |> Enum.join(".") + end + + defp render_throttled_error(conn) do + conn + |> render_error(:too_many_requests, "Throttled") + |> halt() + end + + defp make_key_name(action_settings) do + "" + |> attach_selected_params(action_settings) + |> attach_identity(action_settings) + end + + defp get_scale(_, {scale, _}), do: scale + + defp get_scale(:anon, [{scale, _}, {_, _}]), do: scale + + defp get_scale(:user, [{_, _}, {scale, _}]), do: scale + + defp get_limits(%{limits: {_scale, limit}}), do: limit + + defp get_limits(%{mode: :user, limits: [_, {_, limit}]}), do: limit + + defp get_limits(%{limits: [{_, limit}, _]}), do: limit + + defp make_bucket_name(%{mode: :user, name: bucket_name_root}), + do: user_bucket_name(bucket_name_root) + + defp make_bucket_name(%{mode: :anon, name: bucket_name_root}), + do: anon_bucket_name(bucket_name_root) + + defp attach_selected_params(input, %{conn_params: conn_params, opts: plug_opts}) do + params_string = + plug_opts + |> Keyword.get(:params, []) + |> Enum.sort() + |> Enum.map(&Map.get(conn_params, &1, "")) + |> Enum.join(":") + + [input, params_string] + |> Enum.join(":") + |> String.replace_leading(":", "") + end + + defp initialize_buckets!(%{name: _name, limits: nil}), do: :ok + + defp initialize_buckets!(%{name: name, limits: limits}) do + {:ok, _pid} = + LimiterSupervisor.add_or_return_limiter(anon_bucket_name(name), get_scale(:anon, limits)) + + {:ok, _pid} = + LimiterSupervisor.add_or_return_limiter(user_bucket_name(name), get_scale(:user, limits)) + + :ok + end + + defp attach_identity(base, %{mode: :user, conn_info: conn_info}), + do: "user:#{base}:#{conn_info}" + + defp attach_identity(base, %{mode: :anon, conn_info: conn_info}), + do: "ip:#{base}:#{conn_info}" + + defp user_bucket_name(bucket_name_root), do: "user:#{bucket_name_root}" |> String.to_atom() + defp anon_bucket_name(bucket_name_root), do: "anon:#{bucket_name_root}" |> String.to_atom() +end diff --git a/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex b/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex @@ -0,0 +1,54 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor do + use DynamicSupervisor + + import Cachex.Spec + + def start_link(init_arg) do + DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__) + end + + def add_or_return_limiter(limiter_name, expiration) do + result = + DynamicSupervisor.start_child( + __MODULE__, + %{ + id: String.to_atom("rl_#{limiter_name}"), + start: + {Cachex, :start_link, + [ + limiter_name, + [ + expiration: + expiration( + default: expiration, + interval: check_interval(expiration), + lazy: true + ) + ] + ]} + } + ) + + case result do + {:ok, _pid} = result -> result + {:error, {:already_started, pid}} -> {:ok, pid} + _ -> result + end + end + + @impl true + def init(_init_arg) do + DynamicSupervisor.init(strategy: :one_for_one) + end + + defp check_interval(exp) do + (exp / 2) + |> Kernel.trunc() + |> Kernel.min(5000) + |> Kernel.max(1) + end +end diff --git a/lib/pleroma/web/plugs/rate_limiter/supervisor.ex b/lib/pleroma/web/plugs/rate_limiter/supervisor.ex @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.RateLimiter.Supervisor do + use Supervisor + + def start_link(opts) do + Supervisor.start_link(__MODULE__, opts, name: __MODULE__) + end + + def init(_args) do + children = [ + Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor + ] + + opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor] + Supervisor.init(children, opts) + end +end diff --git a/lib/pleroma/web/plugs/remote_ip.ex b/lib/pleroma/web/plugs/remote_ip.ex @@ -0,0 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.RemoteIp do + @moduledoc """ + This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. + """ + + alias Pleroma.Config + import Plug.Conn + + @behaviour Plug + + def init(_), do: nil + + def call(%{remote_ip: original_remote_ip} = conn, _) do + if Config.get([__MODULE__, :enabled]) do + %{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts()) + assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip) + else + conn + end + end + + defp remote_ip_opts do + headers = Config.get([__MODULE__, :headers], []) |> MapSet.new() + reserved = Config.get([__MODULE__, :reserved], []) + + proxies = + Config.get([__MODULE__, :proxies], []) + |> Enum.concat(reserved) + |> Enum.map(&maybe_add_cidr/1) + + {headers, proxies} + end + + defp maybe_add_cidr(proxy) when is_binary(proxy) do + proxy = + cond do + "/" in String.codepoints(proxy) -> proxy + InetCidr.v4?(InetCidr.parse_address!(proxy)) -> proxy <> "/32" + InetCidr.v6?(InetCidr.parse_address!(proxy)) -> proxy <> "/128" + end + + InetCidr.parse(proxy, true) + end +end diff --git a/lib/pleroma/web/plugs/session_authentication_plug.ex b/lib/pleroma/web/plugs/session_authentication_plug.ex @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.SessionAuthenticationPlug do + import Plug.Conn + + def init(options) do + options + end + + def call(conn, _) do + with saved_user_id <- get_session(conn, :user_id), + %{auth_user: %{id: ^saved_user_id}} <- conn.assigns do + conn + |> assign(:user, conn.assigns.auth_user) + else + _ -> conn + end + end +end diff --git a/lib/pleroma/web/plugs/set_format_plug.ex b/lib/pleroma/web/plugs/set_format_plug.ex @@ -0,0 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.SetFormatPlug do + import Plug.Conn, only: [assign: 3, fetch_query_params: 1] + + def init(_), do: nil + + def call(conn, _) do + case get_format(conn) do + nil -> conn + format -> assign(conn, :format, format) + end + end + + defp get_format(conn) do + conn.private[:phoenix_format] || + case fetch_query_params(conn) do + %{query_params: %{"_format" => format}} -> format + _ -> nil + end + end +end diff --git a/lib/pleroma/web/plugs/set_locale_plug.ex b/lib/pleroma/web/plugs/set_locale_plug.ex @@ -0,0 +1,63 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +# NOTE: this module is based on https://github.com/smeevil/set_locale +defmodule Pleroma.Web.Plugs.SetLocalePlug do + import Plug.Conn, only: [get_req_header: 2, assign: 3] + + def init(_), do: nil + + def call(conn, _) do + locale = get_locale_from_header(conn) || Gettext.get_locale() + Gettext.put_locale(locale) + assign(conn, :locale, locale) + end + + defp get_locale_from_header(conn) do + conn + |> extract_accept_language() + |> Enum.find(&supported_locale?/1) + end + + defp extract_accept_language(conn) do + case get_req_header(conn, "accept-language") do + [value | _] -> + value + |> String.split(",") + |> Enum.map(&parse_language_option/1) + |> Enum.sort(&(&1.quality > &2.quality)) + |> Enum.map(& &1.tag) + |> Enum.reject(&is_nil/1) + |> ensure_language_fallbacks() + + _ -> + [] + end + end + + defp supported_locale?(locale) do + Pleroma.Web.Gettext + |> Gettext.known_locales() + |> Enum.member?(locale) + end + + defp parse_language_option(string) do + captures = Regex.named_captures(~r/^\s?(?<tag>[\w\-]+)(?:;q=(?<quality>[\d\.]+))?$/i, string) + + quality = + case Float.parse(captures["quality"] || "1.0") do + {val, _} -> val + :error -> 1.0 + end + + %{tag: captures["tag"], quality: quality} + end + + defp ensure_language_fallbacks(tags) do + Enum.flat_map(tags, fn tag -> + [language | _] = String.split(tag, "-") + if Enum.member?(tags, language), do: [tag], else: [tag, language] + end) + end +end diff --git a/lib/pleroma/web/plugs/set_user_session_id_plug.ex b/lib/pleroma/web/plugs/set_user_session_id_plug.ex @@ -0,0 +1,19 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.SetUserSessionIdPlug do + import Plug.Conn + alias Pleroma.User + + def init(opts) do + opts + end + + def call(%{assigns: %{user: %User{id: id}}} = conn, _) do + conn + |> put_session(:user_id, id) + end + + def call(conn, _), do: conn +end diff --git a/lib/pleroma/web/plugs/static_fe_plug.ex b/lib/pleroma/web/plugs/static_fe_plug.ex @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.StaticFEPlug do + import Plug.Conn + alias Pleroma.Web.StaticFE.StaticFEController + + def init(options), do: options + + def call(conn, _) do + if enabled?() and requires_html?(conn) do + conn + |> StaticFEController.call(:show) + |> halt() + else + conn + end + end + + defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false) + + defp requires_html?(conn) do + Phoenix.Controller.get_format(conn) == "html" + end +end diff --git a/lib/pleroma/web/plugs/trailing_format_plug.ex b/lib/pleroma/web/plugs/trailing_format_plug.ex @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.TrailingFormatPlug do + @moduledoc "Calls TrailingFormatPlug for specific paths. Ideally we would just do this in the router, but TrailingFormatPlug needs to be called before Plug.Parsers." + + @behaviour Plug + @paths [ + "/api/statusnet", + "/api/statuses", + "/api/qvitter", + "/api/search", + "/api/account", + "/api/friends", + "/api/mutes", + "/api/media", + "/api/favorites", + "/api/blocks", + "/api/friendships", + "/api/users", + "/users", + "/nodeinfo", + "/api/help", + "/api/externalprofile", + "/notice", + "/api/pleroma/emoji", + "/api/oauth_tokens" + ] + + def init(opts) do + TrailingFormatPlug.init(opts) + end + + for path <- @paths do + def call(%{request_path: unquote(path) <> _} = conn, opts) do + TrailingFormatPlug.call(conn, opts) + end + end + + def call(conn, _opts), do: conn +end diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex @@ -0,0 +1,107 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.UploadedMedia do + @moduledoc """ + """ + + import Plug.Conn + import Pleroma.Web.Gettext + require Logger + + alias Pleroma.Web.MediaProxy + + @behaviour Plug + # no slashes + @path "media" + + @default_cache_control_header "public, max-age=1209600" + + def init(_opts) do + static_plug_opts = + [ + headers: %{"cache-control" => @default_cache_control_header}, + cache_control_for_etags: @default_cache_control_header + ] + |> Keyword.put(:from, "__unconfigured_media_plug") + |> Keyword.put(:at, "/__unconfigured_media_plug") + |> Plug.Static.init() + + %{static_plug_opts: static_plug_opts} + end + + def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do + conn = + case fetch_query_params(conn) do + %{query_params: %{"name" => name}} = conn -> + name = String.replace(name, "\"", "\\\"") + + put_resp_header(conn, "content-disposition", "filename=\"#{name}\"") + + conn -> + conn + end + |> merge_resp_headers([{"content-security-policy", "sandbox"}]) + + config = Pleroma.Config.get(Pleroma.Upload) + + with uploader <- Keyword.fetch!(config, :uploader), + proxy_remote = Keyword.get(config, :proxy_remote, false), + {:ok, get_method} <- uploader.get_file(file), + false <- media_is_banned(conn, get_method) do + get_media(conn, get_method, proxy_remote, opts) + else + _ -> + conn + |> send_resp(:internal_server_error, dgettext("errors", "Failed")) + |> halt() + end + end + + def call(conn, _opts), do: conn + + defp media_is_banned(%{request_path: path} = _conn, {:static_dir, _}) do + MediaProxy.in_banned_urls(Pleroma.Web.base_url() <> path) + end + + defp media_is_banned(_, {:url, url}), do: MediaProxy.in_banned_urls(url) + + defp media_is_banned(_, _), do: false + + defp get_media(conn, {:static_dir, directory}, _, opts) do + static_opts = + Map.get(opts, :static_plug_opts) + |> Map.put(:at, [@path]) + |> Map.put(:from, directory) + + conn = Plug.Static.call(conn, static_opts) + + if conn.halted do + conn + else + conn + |> send_resp(:not_found, dgettext("errors", "Not found")) + |> halt() + end + end + + defp get_media(conn, {:url, url}, true, _) do + conn + |> Pleroma.ReverseProxy.call(url, Pleroma.Config.get([Pleroma.Upload, :proxy_opts], [])) + end + + defp get_media(conn, {:url, url}, _, _) do + conn + |> Phoenix.Controller.redirect(external: url) + |> halt() + end + + defp get_media(conn, unknown, _, _) do + Logger.error("#{__MODULE__}: Unknown get startegy: #{inspect(unknown)}") + + conn + |> send_resp(:internal_server_error, dgettext("errors", "Internal Error")) + |> halt() + end +end diff --git a/lib/pleroma/web/plugs/user_enabled_plug.ex b/lib/pleroma/web/plugs/user_enabled_plug.ex @@ -0,0 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.UserEnabledPlug do + import Plug.Conn + alias Pleroma.User + + def init(options) do + options + end + + def call(%{assigns: %{user: %User{} = user}} = conn, _) do + case User.account_status(user) do + :active -> conn + _ -> assign(conn, :user, nil) + end + end + + def call(conn, _) do + conn + end +end diff --git a/lib/pleroma/web/plugs/user_fetcher_plug.ex b/lib/pleroma/web/plugs/user_fetcher_plug.ex @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.UserFetcherPlug do + alias Pleroma.User + import Plug.Conn + + def init(options) do + options + end + + def call(conn, _options) do + with %{auth_credentials: %{username: username}} <- conn.assigns, + %User{} = user <- User.get_by_nickname_or_email(username) do + assign(conn, :auth_user, user) + else + _ -> conn + end + end +end diff --git a/lib/pleroma/web/plugs/user_is_admin_plug.ex b/lib/pleroma/web/plugs/user_is_admin_plug.ex @@ -0,0 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.UserIsAdminPlug do + import Pleroma.Web.TranslationHelpers + import Plug.Conn + + alias Pleroma.User + + def init(options) do + options + end + + def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do + conn + end + + def call(conn, _) do + conn + |> render_error(:forbidden, "User is not an admin.") + |> halt() + end +end diff --git a/lib/pleroma/web/preload/instance.ex b/lib/pleroma/web/preload/instance.ex @@ -1,59 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Preload.Providers.Instance do - alias Pleroma.Plugs.InstanceStatic - alias Pleroma.Web.MastodonAPI.InstanceView - alias Pleroma.Web.Nodeinfo.Nodeinfo - alias Pleroma.Web.Preload.Providers.Provider - alias Pleroma.Web.TwitterAPI.UtilView - - @behaviour Provider - @instance_url "/api/v1/instance" - @panel_url "/instance/panel.html" - @nodeinfo_url "/nodeinfo/2.0.json" - @fe_config_url "/api/pleroma/frontend_configurations" - - @impl Provider - def generate_terms(_params) do - %{} - |> build_info_tag() - |> build_panel_tag() - |> build_nodeinfo_tag() - |> build_fe_config_tag() - end - - defp build_info_tag(acc) do - info_data = InstanceView.render("show.json", %{}) - - Map.put(acc, @instance_url, info_data) - end - - defp build_panel_tag(acc) do - instance_path = InstanceStatic.file_path(@panel_url |> to_string()) - - if File.exists?(instance_path) do - panel_data = File.read!(instance_path) - Map.put(acc, @panel_url, panel_data) - else - acc - end - end - - defp build_nodeinfo_tag(acc) do - case Nodeinfo.get_nodeinfo("2.0") do - {:error, _} -> - acc - - nodeinfo_data -> - Map.put(acc, @nodeinfo_url, nodeinfo_data) - end - end - - defp build_fe_config_tag(acc) do - fe_data = UtilView.render("frontend_configurations.json", %{}) - - Map.put(acc, @fe_config_url, fe_data) - end -end diff --git a/lib/pleroma/web/preload/providers/instance.ex b/lib/pleroma/web/preload/providers/instance.ex @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.Instance do + alias Pleroma.Web.MastodonAPI.InstanceView + alias Pleroma.Web.Nodeinfo.Nodeinfo + alias Pleroma.Web.Plugs.InstanceStatic + alias Pleroma.Web.Preload.Providers.Provider + alias Pleroma.Web.TwitterAPI.UtilView + + @behaviour Provider + @instance_url "/api/v1/instance" + @panel_url "/instance/panel.html" + @nodeinfo_url "/nodeinfo/2.0.json" + @fe_config_url "/api/pleroma/frontend_configurations" + + @impl Provider + def generate_terms(_params) do + %{} + |> build_info_tag() + |> build_panel_tag() + |> build_nodeinfo_tag() + |> build_fe_config_tag() + end + + defp build_info_tag(acc) do + info_data = InstanceView.render("show.json", %{}) + + Map.put(acc, @instance_url, info_data) + end + + defp build_panel_tag(acc) do + instance_path = InstanceStatic.file_path(@panel_url |> to_string()) + + if File.exists?(instance_path) do + panel_data = File.read!(instance_path) + Map.put(acc, @panel_url, panel_data) + else + acc + end + end + + defp build_nodeinfo_tag(acc) do + case Nodeinfo.get_nodeinfo("2.0") do + {:error, _} -> + acc + + nodeinfo_data -> + Map.put(acc, @nodeinfo_url, nodeinfo_data) + end + end + + defp build_fe_config_tag(acc) do + fe_data = UtilView.render("frontend_configurations.json", %{}) + + Map.put(acc, @fe_config_url, fe_data) + end +end diff --git a/lib/pleroma/web/preload/provider.ex b/lib/pleroma/web/preload/providers/provider.ex diff --git a/lib/pleroma/web/preload/timelines.ex b/lib/pleroma/web/preload/providers/timelines.ex diff --git a/lib/pleroma/web/preload/user.ex b/lib/pleroma/web/preload/providers/user.ex diff --git a/lib/pleroma/web/push/push.ex b/lib/pleroma/web/push.ex diff --git a/lib/pleroma/web/rich_media/parser/ttl.ex b/lib/pleroma/web/rich_media/parser/ttl.ex @@ -0,0 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parser.TTL do + @callback ttl(Map.t(), String.t()) :: Integer.t() | nil +end diff --git a/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex b/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex @@ -0,0 +1,50 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do + @behaviour Pleroma.Web.RichMedia.Parser.TTL + + @impl true + def ttl(data, _url) do + image = Map.get(data, :image) + + if is_aws_signed_url(image) do + image + |> parse_query_params() + |> format_query_params() + |> get_expiration_timestamp() + else + {:error, "Not aws signed url #{inspect(image)}"} + end + end + + defp is_aws_signed_url(image) when is_binary(image) and image != "" do + %URI{host: host, query: query} = URI.parse(image) + + String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires") + end + + defp is_aws_signed_url(_), do: nil + + defp parse_query_params(image) do + %URI{query: query} = URI.parse(image) + query + end + + defp format_query_params(query) do + query + |> String.split(~r/&|=/) + |> Enum.chunk_every(2) + |> Map.new(fn [k, v] -> {k, v} end) + end + + defp get_expiration_timestamp(params) when is_map(params) do + {:ok, date} = + params + |> Map.get("X-Amz-Date") + |> Timex.parse("{ISO:Basic:Z}") + + {:ok, Timex.to_unix(date) + String.to_integer(Map.get(params, "X-Amz-Expires"))} + end +end diff --git a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex b/lib/pleroma/web/rich_media/parsers/o_embed.ex diff --git a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex b/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex @@ -1,50 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do - @behaviour Pleroma.Web.RichMedia.Parser.TTL - - @impl Pleroma.Web.RichMedia.Parser.TTL - def ttl(data, _url) do - image = Map.get(data, :image) - - if is_aws_signed_url(image) do - image - |> parse_query_params() - |> format_query_params() - |> get_expiration_timestamp() - else - {:error, "Not aws signed url #{inspect(image)}"} - end - end - - defp is_aws_signed_url(image) when is_binary(image) and image != "" do - %URI{host: host, query: query} = URI.parse(image) - - String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires") - end - - defp is_aws_signed_url(_), do: nil - - defp parse_query_params(image) do - %URI{query: query} = URI.parse(image) - query - end - - defp format_query_params(query) do - query - |> String.split(~r/&|=/) - |> Enum.chunk_every(2) - |> Map.new(fn [k, v] -> {k, v} end) - end - - defp get_expiration_timestamp(params) when is_map(params) do - {:ok, date} = - params - |> Map.get("X-Amz-Date") - |> Timex.parse("{ISO:Basic:Z}") - - {:ok, Timex.to_unix(date) + String.to_integer(Map.get(params, "X-Amz-Expires"))} - end -end diff --git a/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex b/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex @@ -1,7 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.Parser.TTL do - @callback ttl(Map.t(), String.t()) :: {:ok, Integer.t()} | {:error, String.t()} -end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex @@ -12,31 +12,31 @@ defmodule Pleroma.Web.Router do pipeline :oauth do plug(:fetch_session) - plug(Pleroma.Plugs.OAuthPlug) - plug(Pleroma.Plugs.UserEnabledPlug) + plug(Pleroma.Web.Plugs.OAuthPlug) + plug(Pleroma.Web.Plugs.UserEnabledPlug) end pipeline :expect_authentication do - plug(Pleroma.Plugs.ExpectAuthenticatedCheckPlug) + plug(Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug) end pipeline :expect_public_instance_or_authentication do - plug(Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug) + plug(Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug) end pipeline :authenticate do - plug(Pleroma.Plugs.OAuthPlug) - plug(Pleroma.Plugs.BasicAuthDecoderPlug) - plug(Pleroma.Plugs.UserFetcherPlug) - plug(Pleroma.Plugs.SessionAuthenticationPlug) - plug(Pleroma.Plugs.LegacyAuthenticationPlug) - plug(Pleroma.Plugs.AuthenticationPlug) + plug(Pleroma.Web.Plugs.OAuthPlug) + plug(Pleroma.Web.Plugs.BasicAuthDecoderPlug) + plug(Pleroma.Web.Plugs.UserFetcherPlug) + plug(Pleroma.Web.Plugs.SessionAuthenticationPlug) + plug(Pleroma.Web.Plugs.LegacyAuthenticationPlug) + plug(Pleroma.Web.Plugs.AuthenticationPlug) end pipeline :after_auth do - plug(Pleroma.Plugs.UserEnabledPlug) - plug(Pleroma.Plugs.SetUserSessionIdPlug) - plug(Pleroma.Plugs.EnsureUserKeyPlug) + plug(Pleroma.Web.Plugs.UserEnabledPlug) + plug(Pleroma.Web.Plugs.SetUserSessionIdPlug) + plug(Pleroma.Web.Plugs.EnsureUserKeyPlug) end pipeline :base_api do @@ -50,25 +50,25 @@ defmodule Pleroma.Web.Router do plug(:expect_public_instance_or_authentication) plug(:base_api) plug(:after_auth) - plug(Pleroma.Plugs.IdempotencyPlug) + plug(Pleroma.Web.Plugs.IdempotencyPlug) end pipeline :authenticated_api do plug(:expect_authentication) plug(:base_api) plug(:after_auth) - plug(Pleroma.Plugs.EnsureAuthenticatedPlug) - plug(Pleroma.Plugs.IdempotencyPlug) + plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug) + plug(Pleroma.Web.Plugs.IdempotencyPlug) end pipeline :admin_api do plug(:expect_authentication) plug(:base_api) - plug(Pleroma.Plugs.AdminSecretAuthenticationPlug) + plug(Pleroma.Web.Plugs.AdminSecretAuthenticationPlug) plug(:after_auth) - plug(Pleroma.Plugs.EnsureAuthenticatedPlug) - plug(Pleroma.Plugs.UserIsAdminPlug) - plug(Pleroma.Plugs.IdempotencyPlug) + plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug) + plug(Pleroma.Web.Plugs.UserIsAdminPlug) + plug(Pleroma.Web.Plugs.IdempotencyPlug) end pipeline :mastodon_html do @@ -80,7 +80,7 @@ defmodule Pleroma.Web.Router do pipeline :pleroma_html do plug(:browser) plug(:authenticate) - plug(Pleroma.Plugs.EnsureUserKeyPlug) + plug(Pleroma.Web.Plugs.EnsureUserKeyPlug) end pipeline :well_known do @@ -568,7 +568,7 @@ defmodule Pleroma.Web.Router do pipeline :ostatus do plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"]) - plug(Pleroma.Plugs.StaticFEPlug) + plug(Pleroma.Web.Plugs.StaticFEPlug) end pipeline :oembed do @@ -737,7 +737,7 @@ defmodule Pleroma.Web.Router do get("/check_password", MongooseIMController, :check_password) end - scope "/", Fallback do + scope "/", Pleroma.Web.Fallback do get("/registration/:token", RedirectController, :registration_page) get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta) get("/api*path", RedirectController, :api_not_implemented) diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex @@ -17,8 +17,8 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do plug(:put_view, Pleroma.Web.StaticFE.StaticFEView) plug(:assign_id) - plug(Pleroma.Plugs.EnsureAuthenticatedPlug, - unless_func: &Pleroma.Web.FederatingPlug.federating?/1 + plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug, + unless_func: &Pleroma.Web.Plugs.FederatingPlug.federating?/1 ) @page_keys ["max_id", "min_id", "limit", "since_id", "order"] diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex @@ -0,0 +1,331 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Streamer do + require Logger + + alias Pleroma.Activity + alias Pleroma.Chat.MessageReference + alias Pleroma.Config + alias Pleroma.Conversation.Participation + alias Pleroma.Notification + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.StreamerView + + @mix_env Mix.env() + @registry Pleroma.Web.StreamerRegistry + + def registry, do: @registry + + @public_streams ["public", "public:local", "public:media", "public:local:media"] + @user_streams ["user", "user:notification", "direct", "user:pleroma_chat"] + + @doc "Expands and authorizes a stream, and registers the process for streaming." + @spec get_topic_and_add_socket( + stream :: String.t(), + User.t() | nil, + Token.t() | nil, + Map.t() | nil + ) :: + {:ok, topic :: String.t()} | {:error, :bad_topic} | {:error, :unauthorized} + def get_topic_and_add_socket(stream, user, oauth_token, params \\ %{}) do + case get_topic(stream, user, oauth_token, params) do + {:ok, topic} -> add_socket(topic, user) + error -> error + end + end + + @doc "Expand and authorizes a stream" + @spec get_topic(stream :: String.t(), User.t() | nil, Token.t() | nil, Map.t()) :: + {:ok, topic :: String.t()} | {:error, :bad_topic} + def get_topic(stream, user, oauth_token, params \\ %{}) + + # Allow all public steams. + def get_topic(stream, _user, _oauth_token, _params) when stream in @public_streams do + {:ok, stream} + end + + # Allow all hashtags streams. + def get_topic("hashtag", _user, _oauth_token, %{"tag" => tag} = _params) do + {:ok, "hashtag:" <> tag} + end + + # Expand user streams. + def get_topic( + stream, + %User{id: user_id} = user, + %Token{user_id: token_user_id} = oauth_token, + _params + ) + when stream in @user_streams and user_id == token_user_id do + # Note: "read" works for all user streams (not mentioning it since it's an ancestor scope) + required_scopes = + if stream == "user:notification" do + ["read:notifications"] + else + ["read:statuses"] + end + + if OAuthScopesPlug.filter_descendants(required_scopes, oauth_token.scopes) == [] do + {:error, :unauthorized} + else + {:ok, stream <> ":" <> to_string(user.id)} + end + end + + def get_topic(stream, _user, _oauth_token, _params) when stream in @user_streams do + {:error, :unauthorized} + end + + # List streams. + def get_topic( + "list", + %User{id: user_id} = user, + %Token{user_id: token_user_id} = oauth_token, + %{"list" => id} + ) + when user_id == token_user_id do + cond do + OAuthScopesPlug.filter_descendants(["read", "read:lists"], oauth_token.scopes) == [] -> + {:error, :unauthorized} + + Pleroma.List.get(id, user) -> + {:ok, "list:" <> to_string(id)} + + true -> + {:error, :bad_topic} + end + end + + def get_topic("list", _user, _oauth_token, _params) do + {:error, :unauthorized} + end + + def get_topic(_stream, _user, _oauth_token, _params) do + {:error, :bad_topic} + end + + @doc "Registers the process for streaming. Use `get_topic/3` to get the full authorized topic." + def add_socket(topic, user) do + if should_env_send?() do + auth? = if user, do: true + Registry.register(@registry, topic, auth?) + end + + {:ok, topic} + end + + def remove_socket(topic) do + if should_env_send?(), do: Registry.unregister(@registry, topic) + end + + def stream(topics, items) do + if should_env_send?() do + List.wrap(topics) + |> Enum.each(fn topic -> + List.wrap(items) + |> Enum.each(fn item -> + spawn(fn -> do_stream(topic, item) end) + end) + end) + end + + :ok + end + + def filtered_by_user?(user, item, streamed_type \\ :activity) + + def filtered_by_user?(%User{} = user, %Activity{} = item, streamed_type) do + %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} = + User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute]) + + recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids) + recipients = MapSet.new(item.recipients) + domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) + + with parent <- Object.normalize(item) || item, + true <- + Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)), + true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids, + true <- + !(streamed_type == :activity && item.data["type"] == "Announce" && + parent.data["actor"] == user.ap_id), + true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)), + true <- MapSet.disjoint?(recipients, recipient_blocks), + %{host: item_host} <- URI.parse(item.actor), + %{host: parent_host} <- URI.parse(parent.data["actor"]), + false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host), + false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host), + true <- thread_containment(item, user), + false <- CommonAPI.thread_muted?(user, parent) do + false + else + _ -> true + end + end + + def filtered_by_user?(%User{} = user, %Notification{activity: activity}, _) do + filtered_by_user?(user, activity, :notification) + end + + defp do_stream("direct", item) do + recipient_topics = + User.get_recipients_from_activity(item) + |> Enum.map(fn %{id: id} -> "direct:#{id}" end) + + Enum.each(recipient_topics, fn user_topic -> + Logger.debug("Trying to push direct message to #{user_topic}\n\n") + push_to_socket(user_topic, item) + end) + end + + defp do_stream("participation", participation) do + user_topic = "direct:#{participation.user_id}" + Logger.debug("Trying to push a conversation participation to #{user_topic}\n\n") + + push_to_socket(user_topic, participation) + end + + defp do_stream("list", item) do + # filter the recipient list if the activity is not public, see #270. + recipient_lists = + case Visibility.is_public?(item) do + true -> + Pleroma.List.get_lists_from_activity(item) + + _ -> + Pleroma.List.get_lists_from_activity(item) + |> Enum.filter(fn list -> + owner = User.get_cached_by_id(list.user_id) + + Visibility.visible_for_user?(item, owner) + end) + end + + recipient_topics = + recipient_lists + |> Enum.map(fn %{id: id} -> "list:#{id}" end) + + Enum.each(recipient_topics, fn list_topic -> + Logger.debug("Trying to push message to #{list_topic}\n\n") + push_to_socket(list_topic, item) + end) + end + + defp do_stream(topic, %Notification{} = item) + when topic in ["user", "user:notification"] do + Registry.dispatch(@registry, "#{topic}:#{item.user_id}", fn list -> + Enum.each(list, fn {pid, _auth} -> + send(pid, {:render_with_user, StreamerView, "notification.json", item}) + end) + end) + end + + defp do_stream(topic, {user, %MessageReference{} = cm_ref}) + when topic in ["user", "user:pleroma_chat"] do + topic = "#{topic}:#{user.id}" + + text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) + + Registry.dispatch(@registry, topic, fn list -> + Enum.each(list, fn {pid, _auth} -> + send(pid, {:text, text}) + end) + end) + end + + defp do_stream("user", item) do + Logger.debug("Trying to push to users") + + recipient_topics = + User.get_recipients_from_activity(item) + |> Enum.map(fn %{id: id} -> "user:#{id}" end) + + Enum.each(recipient_topics, fn topic -> + push_to_socket(topic, item) + end) + end + + defp do_stream(topic, item) do + Logger.debug("Trying to push to #{topic}") + Logger.debug("Pushing item to #{topic}") + push_to_socket(topic, item) + end + + defp push_to_socket(topic, %Participation{} = participation) do + rendered = StreamerView.render("conversation.json", participation) + + Registry.dispatch(@registry, topic, fn list -> + Enum.each(list, fn {pid, _} -> + send(pid, {:text, rendered}) + end) + end) + end + + defp push_to_socket(topic, %Activity{ + data: %{"type" => "Delete", "deleted_activity_id" => deleted_activity_id} + }) do + rendered = Jason.encode!(%{event: "delete", payload: to_string(deleted_activity_id)}) + + Registry.dispatch(@registry, topic, fn list -> + Enum.each(list, fn {pid, _} -> + send(pid, {:text, rendered}) + end) + end) + end + + defp push_to_socket(_topic, %Activity{data: %{"type" => "Delete"}}), do: :noop + + defp push_to_socket(topic, item) do + anon_render = StreamerView.render("update.json", item) + + Registry.dispatch(@registry, topic, fn list -> + Enum.each(list, fn {pid, auth?} -> + if auth? do + send(pid, {:render_with_user, StreamerView, "update.json", item}) + else + send(pid, {:text, anon_render}) + end + end) + end) + end + + defp thread_containment(_activity, %User{skip_thread_containment: true}), do: true + + defp thread_containment(activity, user) do + if Config.get([:instance, :skip_thread_containment]) do + true + else + ActivityPub.contain_activity(activity, user) + end + end + + # In test environement, only return true if the registry is started. + # In benchmark environment, returns false. + # In any other environment, always returns true. + cond do + @mix_env == :test -> + def should_env_send? do + case Process.whereis(@registry) do + nil -> + false + + pid -> + Process.alive?(pid) + end + end + + @mix_env == :benchmark -> + def should_env_send?, do: false + + true -> + def should_env_send?, do: true + end +end diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex @@ -1,331 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Streamer do - require Logger - - alias Pleroma.Activity - alias Pleroma.Chat.MessageReference - alias Pleroma.Config - alias Pleroma.Conversation.Participation - alias Pleroma.Notification - alias Pleroma.Object - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Visibility - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.StreamerView - - @mix_env Mix.env() - @registry Pleroma.Web.StreamerRegistry - - def registry, do: @registry - - @public_streams ["public", "public:local", "public:media", "public:local:media"] - @user_streams ["user", "user:notification", "direct", "user:pleroma_chat"] - - @doc "Expands and authorizes a stream, and registers the process for streaming." - @spec get_topic_and_add_socket( - stream :: String.t(), - User.t() | nil, - Token.t() | nil, - Map.t() | nil - ) :: - {:ok, topic :: String.t()} | {:error, :bad_topic} | {:error, :unauthorized} - def get_topic_and_add_socket(stream, user, oauth_token, params \\ %{}) do - case get_topic(stream, user, oauth_token, params) do - {:ok, topic} -> add_socket(topic, user) - error -> error - end - end - - @doc "Expand and authorizes a stream" - @spec get_topic(stream :: String.t(), User.t() | nil, Token.t() | nil, Map.t()) :: - {:ok, topic :: String.t()} | {:error, :bad_topic} - def get_topic(stream, user, oauth_token, params \\ %{}) - - # Allow all public steams. - def get_topic(stream, _user, _oauth_token, _params) when stream in @public_streams do - {:ok, stream} - end - - # Allow all hashtags streams. - def get_topic("hashtag", _user, _oauth_token, %{"tag" => tag} = _params) do - {:ok, "hashtag:" <> tag} - end - - # Expand user streams. - def get_topic( - stream, - %User{id: user_id} = user, - %Token{user_id: token_user_id} = oauth_token, - _params - ) - when stream in @user_streams and user_id == token_user_id do - # Note: "read" works for all user streams (not mentioning it since it's an ancestor scope) - required_scopes = - if stream == "user:notification" do - ["read:notifications"] - else - ["read:statuses"] - end - - if OAuthScopesPlug.filter_descendants(required_scopes, oauth_token.scopes) == [] do - {:error, :unauthorized} - else - {:ok, stream <> ":" <> to_string(user.id)} - end - end - - def get_topic(stream, _user, _oauth_token, _params) when stream in @user_streams do - {:error, :unauthorized} - end - - # List streams. - def get_topic( - "list", - %User{id: user_id} = user, - %Token{user_id: token_user_id} = oauth_token, - %{"list" => id} - ) - when user_id == token_user_id do - cond do - OAuthScopesPlug.filter_descendants(["read", "read:lists"], oauth_token.scopes) == [] -> - {:error, :unauthorized} - - Pleroma.List.get(id, user) -> - {:ok, "list:" <> to_string(id)} - - true -> - {:error, :bad_topic} - end - end - - def get_topic("list", _user, _oauth_token, _params) do - {:error, :unauthorized} - end - - def get_topic(_stream, _user, _oauth_token, _params) do - {:error, :bad_topic} - end - - @doc "Registers the process for streaming. Use `get_topic/3` to get the full authorized topic." - def add_socket(topic, user) do - if should_env_send?() do - auth? = if user, do: true - Registry.register(@registry, topic, auth?) - end - - {:ok, topic} - end - - def remove_socket(topic) do - if should_env_send?(), do: Registry.unregister(@registry, topic) - end - - def stream(topics, items) do - if should_env_send?() do - List.wrap(topics) - |> Enum.each(fn topic -> - List.wrap(items) - |> Enum.each(fn item -> - spawn(fn -> do_stream(topic, item) end) - end) - end) - end - - :ok - end - - def filtered_by_user?(user, item, streamed_type \\ :activity) - - def filtered_by_user?(%User{} = user, %Activity{} = item, streamed_type) do - %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} = - User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute]) - - recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids) - recipients = MapSet.new(item.recipients) - domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) - - with parent <- Object.normalize(item) || item, - true <- - Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)), - true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids, - true <- - !(streamed_type == :activity && item.data["type"] == "Announce" && - parent.data["actor"] == user.ap_id), - true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)), - true <- MapSet.disjoint?(recipients, recipient_blocks), - %{host: item_host} <- URI.parse(item.actor), - %{host: parent_host} <- URI.parse(parent.data["actor"]), - false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host), - false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host), - true <- thread_containment(item, user), - false <- CommonAPI.thread_muted?(user, parent) do - false - else - _ -> true - end - end - - def filtered_by_user?(%User{} = user, %Notification{activity: activity}, _) do - filtered_by_user?(user, activity, :notification) - end - - defp do_stream("direct", item) do - recipient_topics = - User.get_recipients_from_activity(item) - |> Enum.map(fn %{id: id} -> "direct:#{id}" end) - - Enum.each(recipient_topics, fn user_topic -> - Logger.debug("Trying to push direct message to #{user_topic}\n\n") - push_to_socket(user_topic, item) - end) - end - - defp do_stream("participation", participation) do - user_topic = "direct:#{participation.user_id}" - Logger.debug("Trying to push a conversation participation to #{user_topic}\n\n") - - push_to_socket(user_topic, participation) - end - - defp do_stream("list", item) do - # filter the recipient list if the activity is not public, see #270. - recipient_lists = - case Visibility.is_public?(item) do - true -> - Pleroma.List.get_lists_from_activity(item) - - _ -> - Pleroma.List.get_lists_from_activity(item) - |> Enum.filter(fn list -> - owner = User.get_cached_by_id(list.user_id) - - Visibility.visible_for_user?(item, owner) - end) - end - - recipient_topics = - recipient_lists - |> Enum.map(fn %{id: id} -> "list:#{id}" end) - - Enum.each(recipient_topics, fn list_topic -> - Logger.debug("Trying to push message to #{list_topic}\n\n") - push_to_socket(list_topic, item) - end) - end - - defp do_stream(topic, %Notification{} = item) - when topic in ["user", "user:notification"] do - Registry.dispatch(@registry, "#{topic}:#{item.user_id}", fn list -> - Enum.each(list, fn {pid, _auth} -> - send(pid, {:render_with_user, StreamerView, "notification.json", item}) - end) - end) - end - - defp do_stream(topic, {user, %MessageReference{} = cm_ref}) - when topic in ["user", "user:pleroma_chat"] do - topic = "#{topic}:#{user.id}" - - text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) - - Registry.dispatch(@registry, topic, fn list -> - Enum.each(list, fn {pid, _auth} -> - send(pid, {:text, text}) - end) - end) - end - - defp do_stream("user", item) do - Logger.debug("Trying to push to users") - - recipient_topics = - User.get_recipients_from_activity(item) - |> Enum.map(fn %{id: id} -> "user:#{id}" end) - - Enum.each(recipient_topics, fn topic -> - push_to_socket(topic, item) - end) - end - - defp do_stream(topic, item) do - Logger.debug("Trying to push to #{topic}") - Logger.debug("Pushing item to #{topic}") - push_to_socket(topic, item) - end - - defp push_to_socket(topic, %Participation{} = participation) do - rendered = StreamerView.render("conversation.json", participation) - - Registry.dispatch(@registry, topic, fn list -> - Enum.each(list, fn {pid, _} -> - send(pid, {:text, rendered}) - end) - end) - end - - defp push_to_socket(topic, %Activity{ - data: %{"type" => "Delete", "deleted_activity_id" => deleted_activity_id} - }) do - rendered = Jason.encode!(%{event: "delete", payload: to_string(deleted_activity_id)}) - - Registry.dispatch(@registry, topic, fn list -> - Enum.each(list, fn {pid, _} -> - send(pid, {:text, rendered}) - end) - end) - end - - defp push_to_socket(_topic, %Activity{data: %{"type" => "Delete"}}), do: :noop - - defp push_to_socket(topic, item) do - anon_render = StreamerView.render("update.json", item) - - Registry.dispatch(@registry, topic, fn list -> - Enum.each(list, fn {pid, auth?} -> - if auth? do - send(pid, {:render_with_user, StreamerView, "update.json", item}) - else - send(pid, {:text, anon_render}) - end - end) - end) - end - - defp thread_containment(_activity, %User{skip_thread_containment: true}), do: true - - defp thread_containment(activity, user) do - if Config.get([:instance, :skip_thread_containment]) do - true - else - ActivityPub.contain_activity(activity, user) - end - end - - # In test environement, only return true if the registry is started. - # In benchmark environment, returns false. - # In any other environment, always returns true. - cond do - @mix_env == :test -> - def should_env_send? do - case Process.whereis(@registry) do - nil -> - false - - pid -> - Process.alive?(pid) - end - end - - @mix_env == :benchmark -> - def should_env_send?, do: false - - true -> - def should_env_send?, do: true - end -end diff --git a/lib/pleroma/web/twitter_api/controller.ex b/lib/pleroma/web/twitter_api/controller.ex @@ -0,0 +1,100 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.Controller do + use Pleroma.Web, :controller + + alias Pleroma.Notification + alias Pleroma.User + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.TwitterAPI.TokenView + + require Logger + + plug( + OAuthScopesPlug, + %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read + ) + + plug( + :skip_plug, + [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirm_email + ) + + plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token]) + + action_fallback(:errors) + + def confirm_email(conn, %{"user_id" => uid, "token" => token}) do + with %User{} = user <- User.get_cached_by_id(uid), + true <- user.local and user.confirmation_pending and user.confirmation_token == token, + {:ok, _} <- + user + |> User.confirmation_changeset(need_confirmation: false) + |> User.update_and_set_cache() do + redirect(conn, to: "/") + end + end + + def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do + with oauth_tokens <- Token.get_user_tokens(user) do + conn + |> put_view(TokenView) + |> render("index.json", %{tokens: oauth_tokens}) + end + end + + def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do + Token.delete_user_token(user, id) + + json_reply(conn, 201, "") + end + + defp errors(conn, {:param_cast, _}) do + conn + |> put_status(400) + |> json("Invalid parameters") + end + + defp errors(conn, _) do + conn + |> put_status(500) + |> json("Something went wrong") + end + + defp json_reply(conn, status, json) do + conn + |> put_resp_content_type("application/json") + |> send_resp(status, json) + end + + def mark_notifications_as_read( + %{assigns: %{user: user}} = conn, + %{"latest_id" => latest_id} = params + ) do + Notification.set_read_up_to(user, latest_id) + + notifications = Notification.for_user(user, params) + + conn + # XXX: This is a hack because pleroma-fe still uses that API. + |> put_view(Pleroma.Web.MastodonAPI.NotificationView) + |> render("index.json", %{notifications: notifications, for: user}) + end + + def mark_notifications_as_read(%{assigns: %{user: _user}} = conn, _) do + bad_request_reply(conn, "You need to specify latest_id") + end + + defp bad_request_reply(conn, error_message) do + json = error_json(conn, error_message) + json_reply(conn, 400, json) + end + + defp error_json(conn, error_message) do + %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!() + end +end diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex @@ -10,7 +10,6 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do alias Pleroma.Activity alias Pleroma.MFA alias Pleroma.Object.Fetcher - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.Auth.Authenticator alias Pleroma.Web.Auth.TOTPAuthenticator @@ -18,11 +17,11 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do @status_types ["Article", "Event", "Note", "Video", "Page", "Question"] - plug(Pleroma.Web.FederatingPlug) + plug(Pleroma.Web.Plugs.FederatingPlug) # Note: follower can submit the form (with password auth) not being signed in (having no token) plug( - OAuthScopesPlug, + Pleroma.Web.Plugs.OAuthScopesPlug, %{fallback: :proceed_unauthenticated, scopes: ["follow", "write:follows"]} when action in [:do_follow] ) diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -11,12 +11,12 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Emoji alias Pleroma.Healthcheck alias Pleroma.Notification - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.WebFinger - plug(Pleroma.Web.FederatingPlug when action == :remote_subscribe) + plug(Pleroma.Web.Plugs.FederatingPlug when action == :remote_subscribe) plug( OAuthScopesPlug, diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -1,100 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.TwitterAPI.Controller do - use Pleroma.Web, :controller - - alias Pleroma.Notification - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.User - alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.TwitterAPI.TokenView - - require Logger - - plug( - OAuthScopesPlug, - %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read - ) - - plug( - :skip_plug, - [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirm_email - ) - - plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token]) - - action_fallback(:errors) - - def confirm_email(conn, %{"user_id" => uid, "token" => token}) do - with %User{} = user <- User.get_cached_by_id(uid), - true <- user.local and user.confirmation_pending and user.confirmation_token == token, - {:ok, _} <- - user - |> User.confirmation_changeset(need_confirmation: false) - |> User.update_and_set_cache() do - redirect(conn, to: "/") - end - end - - def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do - with oauth_tokens <- Token.get_user_tokens(user) do - conn - |> put_view(TokenView) - |> render("index.json", %{tokens: oauth_tokens}) - end - end - - def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do - Token.delete_user_token(user, id) - - json_reply(conn, 201, "") - end - - defp errors(conn, {:param_cast, _}) do - conn - |> put_status(400) - |> json("Invalid parameters") - end - - defp errors(conn, _) do - conn - |> put_status(500) - |> json("Something went wrong") - end - - defp json_reply(conn, status, json) do - conn - |> put_resp_content_type("application/json") - |> send_resp(status, json) - end - - def mark_notifications_as_read( - %{assigns: %{user: user}} = conn, - %{"latest_id" => latest_id} = params - ) do - Notification.set_read_up_to(user, latest_id) - - notifications = Notification.for_user(user, params) - - conn - # XXX: This is a hack because pleroma-fe still uses that API. - |> put_view(Pleroma.Web.MastodonAPI.NotificationView) - |> render("index.json", %{notifications: notifications, for: user}) - end - - def mark_notifications_as_read(%{assigns: %{user: _user}} = conn, _) do - bad_request_reply(conn, "You need to specify latest_id") - end - - defp bad_request_reply(conn, error_message) do - json = error_json(conn, error_message) - json_reply(conn, 400, json) - end - - defp error_json(conn, error_message) do - %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!() - end -end diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex @@ -1,239 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Plug do - # Substitute for `call/2` which is defined with `use Pleroma.Web, :plug` - @callback perform(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t() -end - -defmodule Pleroma.Web do - @moduledoc """ - A module that keeps using definitions for controllers, - views and so on. - - This can be used in your application as: - - use Pleroma.Web, :controller - use Pleroma.Web, :view - - The definitions below will be executed for every view, - controller, etc, so keep them short and clean, focused - on imports, uses and aliases. - - Do NOT define functions inside the quoted expressions - below. - """ - - alias Pleroma.Plugs.EnsureAuthenticatedPlug - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug - alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.PlugHelper - - def controller do - quote do - use Phoenix.Controller, namespace: Pleroma.Web - - import Plug.Conn - - import Pleroma.Web.Gettext - import Pleroma.Web.Router.Helpers - import Pleroma.Web.TranslationHelpers - - plug(:set_put_layout) - - defp set_put_layout(conn, _) do - put_layout(conn, Pleroma.Config.get(:app_layout, "app.html")) - end - - # Marks plugs intentionally skipped and blocks their execution if present in plugs chain - defp skip_plug(conn, plug_modules) do - plug_modules - |> List.wrap() - |> Enum.reduce( - conn, - fn plug_module, conn -> - try do - plug_module.skip_plug(conn) - rescue - UndefinedFunctionError -> - raise "`#{plug_module}` is not skippable. Append `use Pleroma.Web, :plug` to its code." - end - end - ) - end - - # Executed just before actual controller action, invokes before-action hooks (callbacks) - defp action(conn, params) do - with %{halted: false} = conn <- maybe_drop_authentication_if_oauth_check_ignored(conn), - %{halted: false} = conn <- maybe_perform_public_or_authenticated_check(conn), - %{halted: false} = conn <- maybe_perform_authenticated_check(conn), - %{halted: false} = conn <- maybe_halt_on_missing_oauth_scopes_check(conn) do - super(conn, params) - end - end - - # For non-authenticated API actions, drops auth info if OAuth scopes check was ignored - # (neither performed nor explicitly skipped) - defp maybe_drop_authentication_if_oauth_check_ignored(conn) do - if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and - not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do - OAuthScopesPlug.drop_auth_info(conn) - else - conn - end - end - - # Ensures instance is public -or- user is authenticated if such check was scheduled - defp maybe_perform_public_or_authenticated_check(conn) do - if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) do - EnsurePublicOrAuthenticatedPlug.call(conn, %{}) - else - conn - end - end - - # Ensures user is authenticated if such check was scheduled - # Note: runs prior to action even if it was already executed earlier in plug chain - # (since OAuthScopesPlug has option of proceeding unauthenticated) - defp maybe_perform_authenticated_check(conn) do - if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) do - EnsureAuthenticatedPlug.call(conn, %{}) - else - conn - end - end - - # Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check - defp maybe_halt_on_missing_oauth_scopes_check(conn) do - if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and - not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do - conn - |> render_error( - :forbidden, - "Security violation: OAuth scopes check was neither handled nor explicitly skipped." - ) - |> halt() - else - conn - end - end - end - end - - def view do - quote do - use Phoenix.View, - root: "lib/pleroma/web/templates", - namespace: Pleroma.Web - - # Import convenience functions from controllers - import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] - - import Pleroma.Web.ErrorHelpers - import Pleroma.Web.Gettext - import Pleroma.Web.Router.Helpers - - require Logger - - @doc "Same as `render/3` but wrapped in a rescue block" - def safe_render(view, template, assigns \\ %{}) do - Phoenix.View.render(view, template, assigns) - rescue - error -> - Logger.error( - "#{__MODULE__} failed to render #{inspect({view, template})}\n" <> - Exception.format(:error, error, __STACKTRACE__) - ) - - nil - end - - @doc """ - Same as `render_many/4` but wrapped in rescue block. - """ - def safe_render_many(collection, view, template, assigns \\ %{}) do - Enum.map(collection, fn resource -> - as = Map.get(assigns, :as) || view.__resource__ - assigns = Map.put(assigns, as, resource) - safe_render(view, template, assigns) - end) - |> Enum.filter(& &1) - end - end - end - - def router do - quote do - use Phoenix.Router - # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse - import Plug.Conn - import Phoenix.Controller - end - end - - def channel do - quote do - # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse - use Phoenix.Channel - import Pleroma.Web.Gettext - end - end - - def plug do - quote do - @behaviour Pleroma.Web.Plug - @behaviour Plug - - @doc """ - Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain. - """ - def skip_plug(conn) do - PlugHelper.append_to_private_list( - conn, - PlugHelper.skipped_plugs_list_id(), - __MODULE__ - ) - end - - @impl Plug - @doc """ - Before-plug hook that - * ensures the plug is not skipped - * processes `:if_func` / `:unless_func` functional pre-run conditions - * adds plug to the list of called plugs and calls `perform/2` if checks are passed - - Note: multiple invocations of the same plug (with different or same options) are allowed. - """ - def call(%Plug.Conn{} = conn, options) do - if PlugHelper.plug_skipped?(conn, __MODULE__) || - (options[:if_func] && !options[:if_func].(conn)) || - (options[:unless_func] && options[:unless_func].(conn)) do - conn - else - conn = - PlugHelper.append_to_private_list( - conn, - PlugHelper.called_plugs_list_id(), - __MODULE__ - ) - - apply(__MODULE__, :perform, [conn, options]) - end - end - end - end - - @doc """ - When used, dispatch to the appropriate controller/view/etc. - """ - defmacro __using__(which) when is_atom(which) do - apply(__MODULE__, which, []) - end - - def base_url do - Pleroma.Web.Endpoint.url() - end -end diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger.ex diff --git a/lib/pleroma/web/web_finger/web_finger_controller.ex b/lib/pleroma/web/web_finger/web_finger_controller.ex @@ -7,8 +7,8 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do alias Pleroma.Web.WebFinger - plug(Pleroma.Plugs.SetFormatPlug) - plug(Pleroma.Web.FederatingPlug) + plug(Pleroma.Web.Plugs.SetFormatPlug) + plug(Pleroma.Web.Plugs.FederatingPlug) def host_meta(conn, _params) do xml = WebFinger.host_meta() diff --git a/lib/pleroma/web/xml/xml.ex b/lib/pleroma/web/xml.ex diff --git a/lib/xml_builder.ex b/lib/pleroma/xml_builder.ex diff --git a/priv/repo/migrations/20200919182636_remoteip_plug_rename.exs b/priv/repo/migrations/20200919182636_remoteip_plug_rename.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Repo.Migrations.RemoteipPlugRename do + use Ecto.Migration + + import Ecto.Query + + def up do + config = + from(c in Pleroma.ConfigDB, where: c.group == ^:pleroma and c.key == ^Pleroma.Plugs.RemoteIp) + |> Pleroma.Repo.one() + + if config do + config + |> Ecto.Changeset.change(key: Pleroma.Web.Plugs.RemoteIp) + |> Pleroma.Repo.update() + end + end + + def down, do: :ok +end diff --git a/test/application_requirements_test.exs b/test/application_requirements_test.exs @@ -1,146 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ApplicationRequirementsTest do - use Pleroma.DataCase - import ExUnit.CaptureLog - import Mock - - alias Pleroma.Repo - - describe "check_welcome_message_config!/1" do - setup do: clear_config([:welcome]) - setup do: clear_config([Pleroma.Emails.Mailer]) - - test "raises if welcome email enabled but mail disabled" do - Pleroma.Config.put([:welcome, :email, :enabled], true) - Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) - - assert_raise Pleroma.ApplicationRequirements.VerifyError, "The mail disabled.", fn -> - capture_log(&Pleroma.ApplicationRequirements.verify!/0) - end - end - end - - describe "check_confirmation_accounts!" do - setup_with_mocks([ - {Pleroma.ApplicationRequirements, [:passthrough], - [ - check_migrations_applied!: fn _ -> :ok end - ]} - ]) do - :ok - end - - setup do: clear_config([:instance, :account_activation_required]) - - test "raises if account confirmation is required but mailer isn't enable" do - Pleroma.Config.put([:instance, :account_activation_required], true) - Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) - - assert_raise Pleroma.ApplicationRequirements.VerifyError, - "Account activation enabled, but Mailer is disabled. Cannot send confirmation emails.", - fn -> - capture_log(&Pleroma.ApplicationRequirements.verify!/0) - end - end - - test "doesn't do anything if account confirmation is disabled" do - Pleroma.Config.put([:instance, :account_activation_required], false) - Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) - assert Pleroma.ApplicationRequirements.verify!() == :ok - end - - test "doesn't do anything if account confirmation is required and mailer is enabled" do - Pleroma.Config.put([:instance, :account_activation_required], true) - Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], true) - assert Pleroma.ApplicationRequirements.verify!() == :ok - end - end - - describe "check_rum!" do - setup_with_mocks([ - {Pleroma.ApplicationRequirements, [:passthrough], - [check_migrations_applied!: fn _ -> :ok end]} - ]) do - :ok - end - - setup do: clear_config([:database, :rum_enabled]) - - test "raises if rum is enabled and detects unapplied rum migrations" do - Pleroma.Config.put([:database, :rum_enabled], true) - - with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> false end]}]) do - assert_raise Pleroma.ApplicationRequirements.VerifyError, - "Unapplied RUM Migrations detected", - fn -> - capture_log(&Pleroma.ApplicationRequirements.verify!/0) - end - end - end - - test "raises if rum is disabled and detects rum migrations" do - Pleroma.Config.put([:database, :rum_enabled], false) - - with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> true end]}]) do - assert_raise Pleroma.ApplicationRequirements.VerifyError, - "RUM Migrations detected", - fn -> - capture_log(&Pleroma.ApplicationRequirements.verify!/0) - end - end - end - - test "doesn't do anything if rum enabled and applied migrations" do - Pleroma.Config.put([:database, :rum_enabled], true) - - with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> true end]}]) do - assert Pleroma.ApplicationRequirements.verify!() == :ok - end - end - - test "doesn't do anything if rum disabled" do - Pleroma.Config.put([:database, :rum_enabled], false) - - with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> false end]}]) do - assert Pleroma.ApplicationRequirements.verify!() == :ok - end - end - end - - describe "check_migrations_applied!" do - setup_with_mocks([ - {Ecto.Migrator, [], - [ - with_repo: fn repo, fun -> passthrough([repo, fun]) end, - migrations: fn Repo -> - [ - {:up, 20_191_128_153_944, "fix_missing_following_count"}, - {:up, 20_191_203_043_610, "create_report_notes"}, - {:down, 20_191_220_174_645, "add_scopes_to_pleroma_feo_auth_records"} - ] - end - ]} - ]) do - :ok - end - - setup do: clear_config([:i_am_aware_this_may_cause_data_loss, :disable_migration_check]) - - test "raises if it detects unapplied migrations" do - assert_raise Pleroma.ApplicationRequirements.VerifyError, - "Unapplied Migrations detected", - fn -> - capture_log(&Pleroma.ApplicationRequirements.verify!/0) - end - end - - test "doesn't do anything if disabled" do - Pleroma.Config.put([:i_am_aware_this_may_cause_data_loss, :disable_migration_check], true) - - assert :ok == Pleroma.ApplicationRequirements.verify!() - end - end -end diff --git a/test/config/deprecation_warnings_test.exs b/test/config/deprecation_warnings_test.exs @@ -1,140 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Config.DeprecationWarningsTest do - use ExUnit.Case - use Pleroma.Tests.Helpers - - import ExUnit.CaptureLog - - alias Pleroma.Config - alias Pleroma.Config.DeprecationWarnings - - test "check_old_mrf_config/0" do - clear_config([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.NoOpPolicy) - clear_config([:instance, :mrf_transparency], true) - clear_config([:instance, :mrf_transparency_exclusions], []) - - assert capture_log(fn -> DeprecationWarnings.check_old_mrf_config() end) =~ - """ - !!!DEPRECATION WARNING!!! - Your config is using old namespaces for MRF configuration. They should work for now, but you are advised to change to new namespaces to prevent possible issues later: - - * `config :pleroma, :instance, rewrite_policy` is now `config :pleroma, :mrf, policies` - * `config :pleroma, :instance, mrf_transparency` is now `config :pleroma, :mrf, transparency` - * `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions` - """ - end - - test "move_namespace_and_warn/2" do - old_group1 = [:group, :key] - old_group2 = [:group, :key2] - old_group3 = [:group, :key3] - - new_group1 = [:another_group, :key4] - new_group2 = [:another_group, :key5] - new_group3 = [:another_group, :key6] - - clear_config(old_group1, 1) - clear_config(old_group2, 2) - clear_config(old_group3, 3) - - clear_config(new_group1) - clear_config(new_group2) - clear_config(new_group3) - - config_map = [ - {old_group1, new_group1, "\n error :key"}, - {old_group2, new_group2, "\n error :key2"}, - {old_group3, new_group3, "\n error :key3"} - ] - - assert capture_log(fn -> - DeprecationWarnings.move_namespace_and_warn( - config_map, - "Warning preface" - ) - end) =~ "Warning preface\n error :key\n error :key2\n error :key3" - - assert Config.get(new_group1) == 1 - assert Config.get(new_group2) == 2 - assert Config.get(new_group3) == 3 - end - - test "check_media_proxy_whitelist_config/0" do - clear_config([:media_proxy, :whitelist], ["https://example.com", "example2.com"]) - - assert capture_log(fn -> - DeprecationWarnings.check_media_proxy_whitelist_config() - end) =~ "Your config is using old format (only domain) for MediaProxy whitelist option" - end - - test "check_welcome_message_config/0" do - clear_config([:instance, :welcome_user_nickname], "LainChan") - - assert capture_log(fn -> - DeprecationWarnings.check_welcome_message_config() - end) =~ "Your config is using the old namespace for Welcome messages configuration." - end - - test "check_hellthread_threshold/0" do - clear_config([:mrf_hellthread, :threshold], 16) - - assert capture_log(fn -> - DeprecationWarnings.check_hellthread_threshold() - end) =~ "You are using the old configuration mechanism for the hellthread filter." - end - - test "check_activity_expiration_config/0" do - clear_config([Pleroma.ActivityExpiration, :enabled], true) - - assert capture_log(fn -> - DeprecationWarnings.check_activity_expiration_config() - end) =~ "Your config is using old namespace for activity expiration configuration." - end - - describe "check_gun_pool_options/0" do - test "await_up_timeout" do - config = Config.get(:connections_pool) - clear_config(:connections_pool, Keyword.put(config, :await_up_timeout, 5_000)) - - assert capture_log(fn -> - DeprecationWarnings.check_gun_pool_options() - end) =~ - "Your config is using old setting `config :pleroma, :connections_pool, await_up_timeout`." - end - - test "pool timeout" do - old_config = [ - federation: [ - size: 50, - max_waiting: 10, - timeout: 10_000 - ], - media: [ - size: 50, - max_waiting: 10, - timeout: 10_000 - ], - upload: [ - size: 25, - max_waiting: 5, - timeout: 15_000 - ], - default: [ - size: 10, - max_waiting: 2, - timeout: 5_000 - ] - ] - - clear_config(:pools, old_config) - - assert capture_log(fn -> - DeprecationWarnings.check_gun_pool_options() - end) =~ - "Your config is using old setting name `timeout` instead of `recv_timeout` in pool settings" - end - end -end diff --git a/test/fixtures/modules/runtime_module.ex b/test/fixtures/modules/runtime_module.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule RuntimeModule do +defmodule Fixtures.Modules.RuntimeModule do @moduledoc """ This is a dummy module to test custom runtime modules. """ diff --git a/test/mfa/backup_codes_test.exs b/test/mfa/backup_codes_test.exs @@ -1,15 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.MFA.BackupCodesTest do - use Pleroma.DataCase - - alias Pleroma.MFA.BackupCodes - - test "generate backup codes" do - codes = BackupCodes.generate(number_of_codes: 2, length: 4) - - assert [<<_::bytes-size(4)>>, <<_::bytes-size(4)>>] = codes - end -end diff --git a/test/mfa/totp_test.exs b/test/mfa/totp_test.exs @@ -1,21 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.MFA.TOTPTest do - use Pleroma.DataCase - - alias Pleroma.MFA.TOTP - - test "create provisioning_uri to generate qrcode" do - uri = - TOTP.provisioning_uri("test-secrcet", "test@example.com", - issuer: "Plerome-42", - digits: 8, - period: 60 - ) - - assert uri == - "otpauth://totp/test@example.com?digits=8&issuer=Plerome-42&period=60&secret=test-secrcet" - end -end diff --git a/test/mfa_test.exs b/test/mfa_test.exs @@ -1,52 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.MFATest do - use Pleroma.DataCase - - import Pleroma.Factory - alias Pleroma.MFA - - describe "mfa_settings" do - test "returns settings user's" do - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: "xx", confirmed: true} - } - ) - - settings = MFA.mfa_settings(user) - assert match?(^settings, %{enabled: true, totp: true}) - end - end - - describe "generate backup codes" do - test "returns backup codes" do - user = insert(:user) - - {:ok, [code1, code2]} = MFA.generate_backup_codes(user) - updated_user = refresh_record(user) - [hash1, hash2] = updated_user.multi_factor_authentication_settings.backup_codes - assert Pbkdf2.verify_pass(code1, hash1) - assert Pbkdf2.verify_pass(code2, hash2) - end - end - - describe "invalidate_backup_code" do - test "invalid used code" do - user = insert(:user) - - {:ok, _} = MFA.generate_backup_codes(user) - user = refresh_record(user) - assert length(user.multi_factor_authentication_settings.backup_codes) == 2 - [hash_code | _] = user.multi_factor_authentication_settings.backup_codes - - {:ok, user} = MFA.invalidate_backup_code(user, hash_code) - - assert length(user.multi_factor_authentication_settings.backup_codes) == 1 - end - end -end diff --git a/test/migration_helper/notification_backfill_test.exs b/test/migration_helper/notification_backfill_test.exs @@ -1,56 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.MigrationHelper.NotificationBackfillTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.MigrationHelper.NotificationBackfill - alias Pleroma.Notification - alias Pleroma.Repo - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "fill_in_notification_types" do - test "it fills in missing notification types" do - user = insert(:user) - other_user = insert(:user) - - {:ok, post} = CommonAPI.post(user, %{status: "yeah, @#{other_user.nickname}"}) - {:ok, chat} = CommonAPI.post_chat_message(user, other_user, "yo") - {:ok, react} = CommonAPI.react_with_emoji(post.id, other_user, "☕") - {:ok, like} = CommonAPI.favorite(other_user, post.id) - {:ok, react_2} = CommonAPI.react_with_emoji(post.id, other_user, "☕") - - data = - react_2.data - |> Map.put("type", "EmojiReaction") - - {:ok, react_2} = - react_2 - |> Activity.change(%{data: data}) - |> Repo.update() - - assert {5, nil} = Repo.update_all(Notification, set: [type: nil]) - - NotificationBackfill.fill_in_notification_types() - - assert %{type: "mention"} = - Repo.get_by(Notification, user_id: other_user.id, activity_id: post.id) - - assert %{type: "favourite"} = - Repo.get_by(Notification, user_id: user.id, activity_id: like.id) - - assert %{type: "pleroma:emoji_reaction"} = - Repo.get_by(Notification, user_id: user.id, activity_id: react.id) - - assert %{type: "pleroma:emoji_reaction"} = - Repo.get_by(Notification, user_id: user.id, activity_id: react_2.id) - - assert %{type: "pleroma:chat_mention"} = - Repo.get_by(Notification, user_id: other_user.id, activity_id: chat.id) - end - end -end diff --git a/test/migrations/20200716195806_autolinker_to_linkify_test.exs b/test/migrations/20200716195806_autolinker_to_linkify_test.exs @@ -1,72 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Repo.Migrations.AutolinkerToLinkifyTest do - use Pleroma.DataCase - import Pleroma.Factory - import Pleroma.Tests.Helpers - alias Pleroma.ConfigDB - - setup do: clear_config(Pleroma.Formatter) - setup_all do: require_migration("20200716195806_autolinker_to_linkify") - - test "change/0 converts auto_linker opts for Pleroma.Formatter", %{migration: migration} do - autolinker_opts = [ - extra: true, - validate_tld: true, - class: false, - strip_prefix: false, - new_window: false, - rel: "testing" - ] - - insert(:config, group: :auto_linker, key: :opts, value: autolinker_opts) - - migration.change() - - assert nil == ConfigDB.get_by_params(%{group: :auto_linker, key: :opts}) - - %{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter}) - - assert new_opts == [ - class: false, - extra: true, - new_window: false, - rel: "testing", - strip_prefix: false - ] - - Pleroma.Config.put(Pleroma.Formatter, new_opts) - assert new_opts == Pleroma.Config.get(Pleroma.Formatter) - - {text, _mentions, []} = - Pleroma.Formatter.linkify( - "https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7\n\nOmg will COVID finally end Black Friday???" - ) - - assert text == - "<a href=\"https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7\" rel=\"testing\">https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7</a>\n\nOmg will COVID finally end Black Friday???" - end - - test "transform_opts/1 returns a list of compatible opts", %{migration: migration} do - old_opts = [ - extra: true, - validate_tld: true, - class: false, - strip_prefix: false, - new_window: false, - rel: "qqq" - ] - - expected_opts = [ - class: false, - extra: true, - new_window: false, - rel: "qqq", - strip_prefix: false - ] - - assert migration.transform_opts(old_opts) == expected_opts - end -end diff --git a/test/migrations/20200722185515_fix_malformed_formatter_config_test.exs b/test/migrations/20200722185515_fix_malformed_formatter_config_test.exs @@ -1,70 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Repo.Migrations.FixMalformedFormatterConfigTest do - use Pleroma.DataCase - import Pleroma.Factory - import Pleroma.Tests.Helpers - alias Pleroma.ConfigDB - - setup do: clear_config(Pleroma.Formatter) - setup_all do: require_migration("20200722185515_fix_malformed_formatter_config") - - test "change/0 converts a map into a list", %{migration: migration} do - incorrect_opts = %{ - class: false, - extra: true, - new_window: false, - rel: "F", - strip_prefix: false - } - - insert(:config, group: :pleroma, key: Pleroma.Formatter, value: incorrect_opts) - - assert :ok == migration.change() - - %{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter}) - - assert new_opts == [ - class: false, - extra: true, - new_window: false, - rel: "F", - strip_prefix: false - ] - - Pleroma.Config.put(Pleroma.Formatter, new_opts) - assert new_opts == Pleroma.Config.get(Pleroma.Formatter) - - {text, _mentions, []} = - Pleroma.Formatter.linkify( - "https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7\n\nOmg will COVID finally end Black Friday???" - ) - - assert text == - "<a href=\"https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7\" rel=\"F\">https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7</a>\n\nOmg will COVID finally end Black Friday???" - end - - test "change/0 skips if Pleroma.Formatter config is already a list", %{migration: migration} do - opts = [ - class: false, - extra: true, - new_window: false, - rel: "ugc", - strip_prefix: false - ] - - insert(:config, group: :pleroma, key: Pleroma.Formatter, value: opts) - - assert :skipped == migration.change() - - %{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter}) - - assert new_opts == opts - end - - test "change/0 skips if Pleroma.Formatter is empty", %{migration: migration} do - assert :skipped == migration.change() - end -end diff --git a/test/migrations/20200724133313_move_welcome_settings_test.exs b/test/migrations/20200724133313_move_welcome_settings_test.exs @@ -1,144 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Repo.Migrations.MoveWelcomeSettingsTest do - use Pleroma.DataCase - import Pleroma.Factory - import Pleroma.Tests.Helpers - alias Pleroma.ConfigDB - - setup_all do: require_migration("20200724133313_move_welcome_settings") - - describe "up/0" do - test "converts welcome settings", %{migration: migration} do - insert(:config, - group: :pleroma, - key: :instance, - value: [ - welcome_message: "Test message", - welcome_user_nickname: "jimm", - name: "Pleroma" - ] - ) - - migration.up() - instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) - welcome_config = ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) - - assert instance_config.value == [name: "Pleroma"] - - assert welcome_config.value == [ - direct_message: %{ - enabled: true, - message: "Test message", - sender_nickname: "jimm" - }, - email: %{ - enabled: false, - html: "Welcome to <%= instance_name %>", - sender: nil, - subject: "Welcome to <%= instance_name %>", - text: "Welcome to <%= instance_name %>" - } - ] - end - - test "does nothing when message empty", %{migration: migration} do - insert(:config, - group: :pleroma, - key: :instance, - value: [ - welcome_message: "", - welcome_user_nickname: "jimm", - name: "Pleroma" - ] - ) - - migration.up() - instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) - refute ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) - assert instance_config.value == [name: "Pleroma"] - end - - test "does nothing when welcome_message not set", %{migration: migration} do - insert(:config, - group: :pleroma, - key: :instance, - value: [welcome_user_nickname: "jimm", name: "Pleroma"] - ) - - migration.up() - instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) - refute ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) - assert instance_config.value == [name: "Pleroma"] - end - end - - describe "down/0" do - test "revert new settings to old when instance setting not exists", %{migration: migration} do - insert(:config, - group: :pleroma, - key: :welcome, - value: [ - direct_message: %{ - enabled: true, - message: "Test message", - sender_nickname: "jimm" - }, - email: %{ - enabled: false, - html: "Welcome to <%= instance_name %>", - sender: nil, - subject: "Welcome to <%= instance_name %>", - text: "Welcome to <%= instance_name %>" - } - ] - ) - - migration.down() - - refute ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) - instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) - - assert instance_config.value == [ - welcome_user_nickname: "jimm", - welcome_message: "Test message" - ] - end - - test "revert new settings to old when instance setting exists", %{migration: migration} do - insert(:config, group: :pleroma, key: :instance, value: [name: "Pleroma App"]) - - insert(:config, - group: :pleroma, - key: :welcome, - value: [ - direct_message: %{ - enabled: true, - message: "Test message", - sender_nickname: "jimm" - }, - email: %{ - enabled: false, - html: "Welcome to <%= instance_name %>", - sender: nil, - subject: "Welcome to <%= instance_name %>", - text: "Welcome to <%= instance_name %>" - } - ] - ) - - migration.down() - - refute ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) - instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) - - assert instance_config.value == [ - name: "Pleroma App", - welcome_user_nickname: "jimm", - welcome_message: "Test message" - ] - end - end -end diff --git a/test/migrations/20200802170532_fix_legacy_tags_test.exs b/test/migrations/20200802170532_fix_legacy_tags_test.exs @@ -1,28 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Repo.Migrations.FixLegacyTagsTest do - alias Pleroma.User - use Pleroma.DataCase - import Pleroma.Factory - import Pleroma.Tests.Helpers - - setup_all do: require_migration("20200802170532_fix_legacy_tags") - - test "change/0 converts legacy user tags into correct values", %{migration: migration} do - user = insert(:user, tags: ["force_nsfw", "force_unlisted", "verified"]) - user2 = insert(:user) - - assert :ok == migration.change() - - fixed_user = User.get_by_id(user.id) - fixed_user2 = User.get_by_id(user2.id) - - assert fixed_user.tags == ["mrf_tag:media-force-nsfw", "mrf_tag:force-unlisted", "verified"] - assert fixed_user2.tags == [] - - # user2 should not have been updated - assert fixed_user2.updated_at == fixed_user2.inserted_at - end -end diff --git a/test/mix/pleroma_test.exs b/test/mix/pleroma_test.exs @@ -0,0 +1,50 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.PleromaTest do + use ExUnit.Case, async: true + import Mix.Pleroma + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + describe "shell_prompt/1" do + test "input" do + send(self(), {:mix_shell_input, :prompt, "Yes"}) + + answer = shell_prompt("Do you want this?") + assert_received {:mix_shell, :prompt, [message]} + assert message =~ "Do you want this?" + assert answer == "Yes" + end + + test "with defval" do + send(self(), {:mix_shell_input, :prompt, "\n"}) + + answer = shell_prompt("Do you want this?", "defval") + + assert_received {:mix_shell, :prompt, [message]} + assert message =~ "Do you want this? [defval]" + assert answer == "defval" + end + end + + describe "get_option/3" do + test "get from options" do + assert get_option([domain: "some-domain.com"], :domain, "Promt") == "some-domain.com" + end + + test "get from prompt" do + send(self(), {:mix_shell_input, :prompt, "another-domain.com"}) + assert get_option([], :domain, "Prompt") == "another-domain.com" + end + end +end diff --git a/test/mix/tasks/pleroma/app_test.exs b/test/mix/tasks/pleroma/app_test.exs @@ -0,0 +1,65 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.AppTest do + use Pleroma.DataCase, async: true + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + end + + describe "creates new app" do + test "with default scopes" do + name = "Some name" + redirect = "https://example.com" + Mix.Tasks.Pleroma.App.run(["create", "-n", name, "-r", redirect]) + + assert_app(name, redirect, ["read", "write", "follow", "push"]) + end + + test "with custom scopes" do + name = "Another name" + redirect = "https://example.com" + + Mix.Tasks.Pleroma.App.run([ + "create", + "-n", + name, + "-r", + redirect, + "-s", + "read,write,follow,push,admin" + ]) + + assert_app(name, redirect, ["read", "write", "follow", "push", "admin"]) + end + end + + test "with errors" do + Mix.Tasks.Pleroma.App.run(["create"]) + {:mix_shell, :error, ["Creating failed:"]} + {:mix_shell, :error, ["name: can't be blank"]} + {:mix_shell, :error, ["redirect_uris: can't be blank"]} + end + + defp assert_app(name, redirect, scopes) do + app = Repo.get_by(Pleroma.Web.OAuth.App, client_name: name) + + assert_receive {:mix_shell, :info, [message]} + assert message == "#{name} successfully created:" + + assert_receive {:mix_shell, :info, [message]} + assert message == "App client_id: #{app.client_id}" + + assert_receive {:mix_shell, :info, [message]} + assert message == "App client_secret: #{app.client_secret}" + + assert app.scopes == scopes + assert app.redirect_uris == redirect + end +end diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs @@ -0,0 +1,189 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.ConfigTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.ConfigDB + alias Pleroma.Repo + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + Application.delete_env(:pleroma, :first_setting) + Application.delete_env(:pleroma, :second_setting) + end) + + :ok + end + + setup_all do: clear_config(:configurable_from_database, true) + + test "error if file with custom settings doesn't exist" do + Mix.Tasks.Pleroma.Config.migrate_to_db("config/not_existance_config_file.exs") + + assert_receive {:mix_shell, :info, + [ + "To migrate settings, you must define custom settings in config/not_existance_config_file.exs." + ]}, + 15 + end + + describe "migrate_to_db/1" do + setup do + initial = Application.get_env(:quack, :level) + on_exit(fn -> Application.put_env(:quack, :level, initial) end) + end + + @tag capture_log: true + test "config migration refused when deprecated settings are found" do + clear_config([:media_proxy, :whitelist], ["domain_without_scheme.com"]) + assert Repo.all(ConfigDB) == [] + + Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs") + + assert_received {:mix_shell, :error, [message]} + + assert message =~ + "Migration is not allowed until all deprecation warnings have been resolved." + end + + test "filtered settings are migrated to db" do + assert Repo.all(ConfigDB) == [] + + Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs") + + 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 + insert(:config, key: :first_setting, value: [key: "value", key2: ["Activity"]]) + assert Repo.aggregate(ConfigDB, :count, :id) == 1 + + Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs") + + config = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"}) + assert config.value == [key: "value", key2: [Repo]] + end + end + + describe "with deletion temp file" do + setup do + temp_file = "config/temp.exported_from_db.secret.exs" + + on_exit(fn -> + :ok = File.rm(temp_file) + end) + + {:ok, temp_file: temp_file} + end + + test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do + insert(:config, key: :setting_first, value: [key: "value", key2: ["Activity"]]) + insert(:config, key: :setting_second, value: [key: "value2", key2: [Repo]]) + insert(:config, group: :quack, key: :level, value: :info) + + Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"]) + + assert Repo.all(ConfigDB) == [] + + 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 + insert(:config, + key: :instance, + value: [ + name: "Pleroma", + email: "example@example.com", + notify_email: "noreply@example.com", + description: "A Pleroma instance, an alternative fediverse server", + limit: 5_000, + chat_limit: 5_000, + remote_limit: 100_000, + upload_limit: 16_000_000, + avatar_upload_limit: 2_000_000, + background_upload_limit: 4_000_000, + banner_upload_limit: 4_000_000, + poll_limits: %{ + max_options: 20, + max_option_chars: 200, + min_expiration: 0, + max_expiration: 365 * 24 * 60 * 60 + }, + registrations_open: true, + 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: [], + managed_config: true, + static_dir: "instance/static/", + allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"], + autofollowed_nicknames: [], + max_pinned_statuses: 1, + attachment_links: false, + max_report_comment_size: 1000, + safe_dm_mentions: false, + healthcheck: false, + remote_post_retention_days: 90, + skip_thread_containment: true, + limit_to_local_content: :unauthenticated, + user_bio_length: 5000, + user_name_length: 100, + max_account_fields: 10, + max_remote_account_fields: 20, + account_field_name_length: 512, + account_field_value_length: 2048, + external_user_synchronization: true, + extended_nickname_format: true, + multi_factor_authentication: [ + totp: [ + digits: 6, + period: 30 + ], + backup_codes: [ + number: 2, + length: 6 + ] + ] + ] + ) + + Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"]) + + assert Repo.all(ConfigDB) == [] + assert File.exists?(temp_file) + {:ok, file} = File.read(temp_file) + + header = + if Code.ensure_loaded?(Config.Reader) do + "import Config" + else + "use Mix.Config" + end + + assert file == + "#{header}\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" + end + end +end diff --git a/test/mix/tasks/pleroma/count_statuses_test.exs b/test/mix/tasks/pleroma/count_statuses_test.exs @@ -0,0 +1,39 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.CountStatusesTest do + use Pleroma.DataCase + + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import ExUnit.CaptureIO, only: [capture_io: 1] + import Pleroma.Factory + + test "counts statuses" do + user = insert(:user) + {:ok, _} = CommonAPI.post(user, %{status: "test"}) + {:ok, _} = CommonAPI.post(user, %{status: "test2"}) + + user2 = insert(:user) + {:ok, _} = CommonAPI.post(user2, %{status: "test3"}) + + user = refresh_record(user) + user2 = refresh_record(user2) + + assert %{note_count: 2} = user + assert %{note_count: 1} = user2 + + {:ok, user} = User.update_note_count(user, 0) + {:ok, user2} = User.update_note_count(user2, 0) + + assert %{note_count: 0} = user + assert %{note_count: 0} = user2 + + assert capture_io(fn -> Mix.Tasks.Pleroma.CountStatuses.run([]) end) == "Done\n" + + assert %{note_count: 2} = refresh_record(user) + assert %{note_count: 1} = refresh_record(user2) + end +end diff --git a/test/mix/tasks/pleroma/database_test.exs b/test/mix/tasks/pleroma/database_test.exs @@ -0,0 +1,175 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.DatabaseTest do + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + describe "running remove_embedded_objects" do + test "it replaces objects with references" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "test"}) + new_data = Map.put(activity.data, "object", activity.object.data) + + {:ok, activity} = + activity + |> Activity.change(%{data: new_data}) + |> Repo.update() + + assert is_map(activity.data["object"]) + + Mix.Tasks.Pleroma.Database.run(["remove_embedded_objects"]) + + activity = Activity.get_by_id_with_object(activity.id) + assert is_binary(activity.data["object"]) + end + end + + describe "prune_objects" do + test "it prunes old objects from the database" do + insert(:note) + deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) + 1 + + date = + Timex.now() + |> Timex.shift(days: -deadline) + |> Timex.to_naive_datetime() + |> NaiveDateTime.truncate(:second) + + %{id: id} = + :note + |> insert() + |> Ecto.Changeset.change(%{inserted_at: date}) + |> Repo.update!() + + assert length(Repo.all(Object)) == 2 + + Mix.Tasks.Pleroma.Database.run(["prune_objects"]) + + assert length(Repo.all(Object)) == 1 + refute Object.get_by_id(id) + end + end + + describe "running update_users_following_followers_counts" do + test "following and followers count are updated" do + [user, user2] = insert_pair(:user) + {:ok, %User{} = user} = User.follow(user, user2) + + following = User.following(user) + + assert length(following) == 2 + assert user.follower_count == 0 + + {:ok, user} = + user + |> Ecto.Changeset.change(%{follower_count: 3}) + |> Repo.update() + + assert user.follower_count == 3 + + assert :ok == Mix.Tasks.Pleroma.Database.run(["update_users_following_followers_counts"]) + + user = User.get_by_id(user.id) + + assert length(User.following(user)) == 2 + assert user.follower_count == 0 + end + end + + describe "running fix_likes_collections" do + test "it turns OrderedCollection likes into empty arrays" do + [user, user2] = insert_pair(:user) + + {:ok, %{id: id, object: object}} = CommonAPI.post(user, %{status: "test"}) + {:ok, %{object: object2}} = CommonAPI.post(user, %{status: "test test"}) + + CommonAPI.favorite(user2, id) + + likes = %{ + "first" => + "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1", + "id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes", + "totalItems" => 3, + "type" => "OrderedCollection" + } + + new_data = Map.put(object2.data, "likes", likes) + + object2 + |> Ecto.Changeset.change(%{data: new_data}) + |> Repo.update() + + assert length(Object.get_by_id(object.id).data["likes"]) == 1 + assert is_map(Object.get_by_id(object2.id).data["likes"]) + + assert :ok == Mix.Tasks.Pleroma.Database.run(["fix_likes_collections"]) + + assert length(Object.get_by_id(object.id).data["likes"]) == 1 + assert Enum.empty?(Object.get_by_id(object2.id).data["likes"]) + end + end + + describe "ensure_expiration" do + test "it adds to expiration old statuses" do + activity1 = insert(:note_activity) + + {:ok, inserted_at, 0} = DateTime.from_iso8601("2015-01-23T23:50:07Z") + activity2 = insert(:note_activity, %{inserted_at: inserted_at}) + + %{id: activity_id3} = insert(:note_activity) + + expires_at = DateTime.add(DateTime.utc_now(), 60 * 61) + + Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ + activity_id: activity_id3, + expires_at: expires_at + }) + + Mix.Tasks.Pleroma.Database.run(["ensure_expiration"]) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity1.id}, + scheduled_at: + activity1.inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> Timex.shift(days: 365) + ) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity2.id}, + scheduled_at: + activity2.inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> Timex.shift(days: 365) + ) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity_id3}, + scheduled_at: expires_at + ) + end + end +end diff --git a/test/mix/tasks/pleroma/digest_test.exs b/test/mix/tasks/pleroma/digest_test.exs @@ -0,0 +1,60 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.DigestTest do + use Pleroma.DataCase + + import Pleroma.Factory + import Swoosh.TestAssertions + + alias Pleroma.Tests.ObanHelpers + alias Pleroma.Web.CommonAPI + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true) + + describe "pleroma.digest test" do + test "Sends digest to the given user" do + user1 = insert(:user) + user2 = insert(:user) + + Enum.each(0..10, fn i -> + {:ok, _activity} = + CommonAPI.post(user1, %{ + status: "hey ##{i} @#{user2.nickname}!" + }) + end) + + yesterday = + NaiveDateTime.add( + NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), + -60 * 60 * 24, + :second + ) + + {:ok, yesterday_date} = Timex.format(yesterday, "%F", :strftime) + + :ok = Mix.Tasks.Pleroma.Digest.run(["test", user2.nickname, yesterday_date]) + + ObanHelpers.perform_all() + + assert_receive {:mix_shell, :info, [message]} + assert message =~ "Digest email have been sent" + + assert_email_sent( + to: {user2.name, user2.email}, + html_body: ~r/here is what you've missed!/i + ) + end + end +end diff --git a/test/mix/tasks/pleroma/ecto/migrate_test.exs b/test/mix/tasks/pleroma/ecto/migrate_test.exs @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-onl + +defmodule Mix.Tasks.Pleroma.Ecto.MigrateTest do + use Pleroma.DataCase, async: true + import ExUnit.CaptureLog + require Logger + + test "ecto.migrate info message" do + level = Logger.level() + Logger.configure(level: :warn) + + assert capture_log(fn -> + Mix.Tasks.Pleroma.Ecto.Migrate.run() + end) =~ "[info] Already up" + + Logger.configure(level: level) + end +end diff --git a/test/mix/tasks/pleroma/ecto/rollback_test.exs b/test/mix/tasks/pleroma/ecto/rollback_test.exs @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.Ecto.RollbackTest do + use Pleroma.DataCase + import ExUnit.CaptureLog + require Logger + + test "ecto.rollback info message" do + level = Logger.level() + Logger.configure(level: :warn) + + assert capture_log(fn -> + Mix.Tasks.Pleroma.Ecto.Rollback.run() + end) =~ "[info] Rollback succesfully" + + Logger.configure(level: level) + end +end diff --git a/test/mix/tasks/pleroma/ecto_test.exs b/test/mix/tasks/pleroma/ecto_test.exs @@ -0,0 +1,15 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.EctoTest do + use ExUnit.Case, async: true + + test "raise on bad path" do + assert_raise RuntimeError, ~r/Could not find migrations directory/, fn -> + Mix.Tasks.Pleroma.Ecto.ensure_migrations_path(Pleroma.Repo, + migrations_path: "some-path" + ) + end + end +end diff --git a/test/mix/tasks/pleroma/email_test.exs b/test/mix/tasks/pleroma/email_test.exs @@ -0,0 +1,127 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.EmailTest do + use Pleroma.DataCase + + import Swoosh.TestAssertions + + alias Pleroma.Config + alias Pleroma.Tests.ObanHelpers + + import Pleroma.Factory + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true) + setup do: clear_config([:instance, :account_activation_required], true) + + describe "pleroma.email test" do + test "Sends test email with no given address" do + mail_to = Config.get([:instance, :email]) + + :ok = Mix.Tasks.Pleroma.Email.run(["test"]) + + ObanHelpers.perform_all() + + assert_receive {:mix_shell, :info, [message]} + assert message =~ "Test email has been sent" + + assert_email_sent( + to: mail_to, + html_body: ~r/a test email was requested./i + ) + end + + test "Sends test email with given address" do + mail_to = "hewwo@example.com" + + :ok = Mix.Tasks.Pleroma.Email.run(["test", "--to", mail_to]) + + ObanHelpers.perform_all() + + assert_receive {:mix_shell, :info, [message]} + assert message =~ "Test email has been sent" + + assert_email_sent( + to: mail_to, + html_body: ~r/a test email was requested./i + ) + end + + test "Sends confirmation emails" do + local_user1 = + insert(:user, %{ + confirmation_pending: true, + confirmation_token: "mytoken", + deactivated: false, + email: "local1@pleroma.com", + local: true + }) + + local_user2 = + insert(:user, %{ + confirmation_pending: true, + confirmation_token: "mytoken", + deactivated: false, + email: "local2@pleroma.com", + local: true + }) + + :ok = Mix.Tasks.Pleroma.Email.run(["resend_confirmation_emails"]) + + ObanHelpers.perform_all() + + assert_email_sent(to: {local_user1.name, local_user1.email}) + assert_email_sent(to: {local_user2.name, local_user2.email}) + end + + test "Does not send confirmation email to inappropriate users" do + # confirmed user + insert(:user, %{ + confirmation_pending: false, + confirmation_token: "mytoken", + deactivated: false, + email: "confirmed@pleroma.com", + local: true + }) + + # remote user + insert(:user, %{ + deactivated: false, + email: "remote@not-pleroma.com", + local: false + }) + + # deactivated user = + insert(:user, %{ + deactivated: true, + email: "deactivated@pleroma.com", + local: false + }) + + # invisible user + insert(:user, %{ + deactivated: false, + email: "invisible@pleroma.com", + local: true, + invisible: true + }) + + :ok = Mix.Tasks.Pleroma.Email.run(["resend_confirmation_emails"]) + + ObanHelpers.perform_all() + + refute_email_sent() + end + end +end diff --git a/test/mix/tasks/pleroma/emoji_test.exs b/test/mix/tasks/pleroma/emoji_test.exs @@ -0,0 +1,243 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.EmojiTest do + use ExUnit.Case, async: true + + import ExUnit.CaptureIO + import Tesla.Mock + + alias Mix.Tasks.Pleroma.Emoji + + describe "ls-packs" do + test "with default manifest as url" do + mock(fn + %{ + method: :get, + url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json" + } -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/emoji/packs/default-manifest.json") + } + end) + + capture_io(fn -> Emoji.run(["ls-packs"]) end) =~ + "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip" + end + + test "with passed manifest as file" do + capture_io(fn -> + Emoji.run(["ls-packs", "-m", "test/fixtures/emoji/packs/manifest.json"]) + end) =~ "https://git.pleroma.social/pleroma/emoji-index/raw/master/packs/blobs_gg.zip" + end + end + + describe "get-packs" do + test "download pack from default manifest" do + mock(fn + %{ + method: :get, + url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json" + } -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/emoji/packs/default-manifest.json") + } + + %{ + method: :get, + url: "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip" + } -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/emoji/packs/blank.png.zip") + } + + %{ + method: :get, + url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/finmoji.json" + } -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/emoji/packs/finmoji.json") + } + end) + + assert capture_io(fn -> Emoji.run(["get-packs", "finmoji"]) end) =~ "Writing pack.json for" + + emoji_path = + Path.join( + Pleroma.Config.get!([:instance, :static_dir]), + "emoji" + ) + + assert File.exists?(Path.join([emoji_path, "finmoji", "pack.json"])) + on_exit(fn -> File.rm_rf!("test/instance_static/emoji/finmoji") end) + end + + test "install local emoji pack" do + assert capture_io(fn -> + Emoji.run([ + "get-packs", + "local", + "--manifest", + "test/instance_static/local_pack/manifest.json" + ]) + end) =~ "Writing pack.json for" + + on_exit(fn -> File.rm_rf!("test/instance_static/emoji/local") end) + end + + test "pack not found" do + mock(fn + %{ + method: :get, + url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json" + } -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/emoji/packs/default-manifest.json") + } + end) + + assert capture_io(fn -> Emoji.run(["get-packs", "not_found"]) end) =~ + "No pack named \"not_found\" found" + end + + test "raise on bad sha256" do + mock(fn + %{ + method: :get, + url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/packs/blobs_gg.zip" + } -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/emoji/packs/blank.png.zip") + } + end) + + assert_raise RuntimeError, ~r/^Bad SHA256 for blobs.gg/, fn -> + capture_io(fn -> + Emoji.run(["get-packs", "blobs.gg", "-m", "test/fixtures/emoji/packs/manifest.json"]) + end) + end + end + end + + describe "gen-pack" do + setup do + url = "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip" + + mock(fn %{ + method: :get, + url: ^url + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/emoji/packs/blank.png.zip")} + end) + + {:ok, url: url} + end + + test "with default extensions", %{url: url} do + name = "pack1" + pack_json = "#{name}.json" + files_json = "#{name}_file.json" + refute File.exists?(pack_json) + refute File.exists?(files_json) + + captured = + capture_io(fn -> + Emoji.run([ + "gen-pack", + url, + "--name", + name, + "--license", + "license", + "--homepage", + "homepage", + "--description", + "description", + "--files", + files_json, + "--extensions", + ".png .gif" + ]) + end) + + assert captured =~ "#{pack_json} has been created with the pack1 pack" + assert captured =~ "Using .png .gif extensions" + + assert File.exists?(pack_json) + assert File.exists?(files_json) + + on_exit(fn -> + File.rm!(pack_json) + File.rm!(files_json) + end) + end + + test "with custom extensions and update existing files", %{url: url} do + name = "pack2" + pack_json = "#{name}.json" + files_json = "#{name}_file.json" + refute File.exists?(pack_json) + refute File.exists?(files_json) + + captured = + capture_io(fn -> + Emoji.run([ + "gen-pack", + url, + "--name", + name, + "--license", + "license", + "--homepage", + "homepage", + "--description", + "description", + "--files", + files_json, + "--extensions", + " .png .gif .jpeg " + ]) + end) + + assert captured =~ "#{pack_json} has been created with the pack2 pack" + assert captured =~ "Using .png .gif .jpeg extensions" + + assert File.exists?(pack_json) + assert File.exists?(files_json) + + captured = + capture_io(fn -> + Emoji.run([ + "gen-pack", + url, + "--name", + name, + "--license", + "license", + "--homepage", + "homepage", + "--description", + "description", + "--files", + files_json, + "--extensions", + " .png .gif .jpeg " + ]) + end) + + assert captured =~ "#{pack_json} has been updated with the pack2 pack" + + on_exit(fn -> + File.rm!(pack_json) + File.rm!(files_json) + end) + end + end +end diff --git a/test/mix/tasks/pleroma/frontend_test.exs b/test/mix/tasks/pleroma/frontend_test.exs @@ -0,0 +1,85 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.FrontendTest do + use Pleroma.DataCase + alias Mix.Tasks.Pleroma.Frontend + + import ExUnit.CaptureIO, only: [capture_io: 1] + + @dir "test/frontend_static_test" + + setup do + File.mkdir_p!(@dir) + clear_config([:instance, :static_dir], @dir) + + on_exit(fn -> + File.rm_rf(@dir) + end) + end + + test "it downloads and unzips a known frontend" do + clear_config([:frontends, :available], %{ + "pleroma" => %{ + "ref" => "fantasy", + "name" => "pleroma", + "build_url" => "http://gensokyo.2hu/builds/${ref}" + } + }) + + Tesla.Mock.mock(fn %{url: "http://gensokyo.2hu/builds/fantasy"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/frontend_dist.zip")} + end) + + capture_io(fn -> + Frontend.run(["install", "pleroma"]) + end) + + assert File.exists?(Path.join([@dir, "frontends", "pleroma", "fantasy", "test.txt"])) + end + + test "it also works given a file" do + clear_config([:frontends, :available], %{ + "pleroma" => %{ + "ref" => "fantasy", + "name" => "pleroma", + "build_dir" => "" + } + }) + + folder = Path.join([@dir, "frontends", "pleroma", "fantasy"]) + previously_existing = Path.join([folder, "temp"]) + File.mkdir_p!(folder) + File.write!(previously_existing, "yey") + assert File.exists?(previously_existing) + + capture_io(fn -> + Frontend.run(["install", "pleroma", "--file", "test/fixtures/tesla_mock/frontend.zip"]) + end) + + assert File.exists?(Path.join([folder, "test.txt"])) + refute File.exists?(previously_existing) + end + + test "it downloads and unzips unknown frontends" do + Tesla.Mock.mock(fn %{url: "http://gensokyo.2hu/madeup.zip"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/frontend.zip")} + end) + + capture_io(fn -> + Frontend.run([ + "install", + "unknown", + "--ref", + "baka", + "--build-url", + "http://gensokyo.2hu/madeup.zip", + "--build-dir", + "" + ]) + end) + + assert File.exists?(Path.join([@dir, "frontends", "unknown", "baka", "test.txt"])) + end +end diff --git a/test/mix/tasks/pleroma/instance_test.exs b/test/mix/tasks/pleroma/instance_test.exs @@ -0,0 +1,99 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.InstanceTest do + use ExUnit.Case + + setup do + File.mkdir_p!(tmp_path()) + + on_exit(fn -> + File.rm_rf(tmp_path()) + static_dir = Pleroma.Config.get([:instance, :static_dir], "test/instance_static/") + + if File.exists?(static_dir) do + File.rm_rf(Path.join(static_dir, "robots.txt")) + end + + Pleroma.Config.put([:instance, :static_dir], static_dir) + end) + + :ok + end + + defp tmp_path do + "/tmp/generated_files/" + end + + test "running gen" do + mix_task = fn -> + Mix.Tasks.Pleroma.Instance.run([ + "gen", + "--output", + tmp_path() <> "generated_config.exs", + "--output-psql", + tmp_path() <> "setup.psql", + "--domain", + "test.pleroma.social", + "--instance-name", + "Pleroma", + "--admin-email", + "admin@example.com", + "--notify-email", + "notify@example.com", + "--dbhost", + "dbhost", + "--dbname", + "dbname", + "--dbuser", + "dbuser", + "--dbpass", + "dbpass", + "--indexable", + "y", + "--db-configurable", + "y", + "--rum", + "y", + "--listen-port", + "4000", + "--listen-ip", + "127.0.0.1", + "--uploads-dir", + "test/uploads", + "--static-dir", + "./test/../test/instance/static/", + "--strip-uploads", + "y", + "--dedupe-uploads", + "n", + "--anonymize-uploads", + "n" + ]) + end + + ExUnit.CaptureIO.capture_io(fn -> + mix_task.() + end) + + generated_config = File.read!(tmp_path() <> "generated_config.exs") + assert generated_config =~ "host: \"test.pleroma.social\"" + assert generated_config =~ "name: \"Pleroma\"" + assert generated_config =~ "email: \"admin@example.com\"" + assert generated_config =~ "notify_email: \"notify@example.com\"" + assert generated_config =~ "hostname: \"dbhost\"" + assert generated_config =~ "database: \"dbname\"" + assert generated_config =~ "username: \"dbuser\"" + assert generated_config =~ "password: \"dbpass\"" + assert generated_config =~ "configurable_from_database: true" + assert generated_config =~ "http: [ip: {127, 0, 0, 1}, port: 4000]" + assert generated_config =~ "filters: [Pleroma.Upload.Filter.ExifTool]" + assert File.read!(tmp_path() <> "setup.psql") == generated_setup_psql() + assert File.exists?(Path.expand("./test/instance/static/robots.txt")) + end + + defp generated_setup_psql do + ~s(CREATE USER dbuser WITH ENCRYPTED PASSWORD 'dbpass';\nCREATE DATABASE dbname OWNER dbuser;\n\\c dbname;\n--Extensions made by ecto.migrate that need superuser access\nCREATE EXTENSION IF NOT EXISTS citext;\nCREATE EXTENSION IF NOT EXISTS pg_trgm;\nCREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";\nCREATE EXTENSION IF NOT EXISTS rum;\n) + end +end diff --git a/test/mix/tasks/pleroma/refresh_counter_cache_test.exs b/test/mix/tasks/pleroma/refresh_counter_cache_test.exs @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.RefreshCounterCacheTest do + use Pleroma.DataCase + alias Pleroma.Web.CommonAPI + import ExUnit.CaptureIO, only: [capture_io: 1] + import Pleroma.Factory + + test "counts statuses" do + user = insert(:user) + other_user = insert(:user) + + CommonAPI.post(user, %{visibility: "public", status: "hey"}) + + Enum.each(0..1, fn _ -> + CommonAPI.post(user, %{ + visibility: "unlisted", + status: "hey" + }) + end) + + Enum.each(0..2, fn _ -> + CommonAPI.post(user, %{ + visibility: "direct", + status: "hey @#{other_user.nickname}" + }) + end) + + Enum.each(0..3, fn _ -> + CommonAPI.post(user, %{ + visibility: "private", + status: "hey" + }) + end) + + assert capture_io(fn -> Mix.Tasks.Pleroma.RefreshCounterCache.run([]) end) =~ "Done\n" + + assert %{"direct" => 3, "private" => 4, "public" => 1, "unlisted" => 2} = + Pleroma.Stats.get_status_visibility_count() + end +end diff --git a/test/mix/tasks/pleroma/relay_test.exs b/test/mix/tasks/pleroma/relay_test.exs @@ -0,0 +1,180 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.RelayTest do + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Relay + alias Pleroma.Web.ActivityPub.Utils + use Pleroma.DataCase + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + describe "running follow" do + test "relay is followed" do + target_instance = "http://mastodon.example.org/users/admin" + + Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) + + local_user = Relay.get_actor() + assert local_user.ap_id =~ "/relay" + + target_user = User.get_cached_by_ap_id(target_instance) + refute target_user.local + + activity = Utils.fetch_latest_follow(local_user, target_user) + assert activity.data["type"] == "Follow" + assert activity.data["actor"] == local_user.ap_id + assert activity.data["object"] == target_user.ap_id + + :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) + + assert_receive {:mix_shell, :info, + [ + "http://mastodon.example.org/users/admin - no Accept received (relay didn't follow back)" + ]} + end + end + + describe "running unfollow" do + test "relay is unfollowed" do + user = insert(:user) + target_instance = user.ap_id + + Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) + + %User{ap_id: follower_id} = local_user = Relay.get_actor() + target_user = User.get_cached_by_ap_id(target_instance) + follow_activity = Utils.fetch_latest_follow(local_user, target_user) + User.follow(local_user, target_user) + assert "#{target_instance}/followers" in User.following(local_user) + Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance]) + + cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) + assert cancelled_activity.data["state"] == "cancelled" + + [undo_activity] = + ActivityPub.fetch_activities([], %{ + type: "Undo", + actor_id: follower_id, + limit: 1, + skip_preload: true, + invisible_actors: true + }) + + assert undo_activity.data["type"] == "Undo" + assert undo_activity.data["actor"] == local_user.ap_id + assert undo_activity.data["object"]["id"] == cancelled_activity.data["id"] + refute "#{target_instance}/followers" in User.following(local_user) + end + + test "unfollow when relay is dead" do + user = insert(:user) + target_instance = user.ap_id + + Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) + + %User{ap_id: follower_id} = local_user = Relay.get_actor() + target_user = User.get_cached_by_ap_id(target_instance) + follow_activity = Utils.fetch_latest_follow(local_user, target_user) + User.follow(local_user, target_user) + + assert "#{target_instance}/followers" in User.following(local_user) + + Tesla.Mock.mock(fn %{method: :get, url: ^target_instance} -> + %Tesla.Env{status: 404} + end) + + Pleroma.Repo.delete(user) + Cachex.clear(:user_cache) + + Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance]) + + cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) + assert cancelled_activity.data["state"] == "accept" + + assert [] == + ActivityPub.fetch_activities( + [], + %{ + type: "Undo", + actor_id: follower_id, + skip_preload: true, + invisible_actors: true + } + ) + end + + test "force unfollow when relay is dead" do + user = insert(:user) + target_instance = user.ap_id + + Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) + + %User{ap_id: follower_id} = local_user = Relay.get_actor() + target_user = User.get_cached_by_ap_id(target_instance) + follow_activity = Utils.fetch_latest_follow(local_user, target_user) + User.follow(local_user, target_user) + + assert "#{target_instance}/followers" in User.following(local_user) + + Tesla.Mock.mock(fn %{method: :get, url: ^target_instance} -> + %Tesla.Env{status: 404} + end) + + Pleroma.Repo.delete(user) + Cachex.clear(:user_cache) + + Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance, "--force"]) + + cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) + assert cancelled_activity.data["state"] == "cancelled" + + [undo_activity] = + ActivityPub.fetch_activities( + [], + %{type: "Undo", actor_id: follower_id, skip_preload: true, invisible_actors: true} + ) + + assert undo_activity.data["type"] == "Undo" + assert undo_activity.data["actor"] == local_user.ap_id + assert undo_activity.data["object"]["id"] == cancelled_activity.data["id"] + refute "#{target_instance}/followers" in User.following(local_user) + end + end + + describe "mix pleroma.relay list" do + test "Prints relay subscription list" do + :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) + + refute_receive {:mix_shell, :info, _} + + relay_user = Relay.get_actor() + + ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"] + |> Enum.each(fn ap_id -> + {:ok, user} = User.get_or_fetch_by_ap_id(ap_id) + User.follow(relay_user, user) + end) + + :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) + + assert_receive {:mix_shell, :info, ["https://mstdn.io/users/mayuutann"]} + assert_receive {:mix_shell, :info, ["http://mastodon.example.org/users/admin"]} + end + end +end diff --git a/test/mix/tasks/pleroma/robots_txt_test.exs b/test/mix/tasks/pleroma/robots_txt_test.exs @@ -0,0 +1,45 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.RobotsTxtTest do + use ExUnit.Case + use Pleroma.Tests.Helpers + alias Mix.Tasks.Pleroma.RobotsTxt + + setup do: clear_config([:instance, :static_dir]) + + test "creates new dir" do + path = "test/fixtures/new_dir/" + file_path = path <> "robots.txt" + Pleroma.Config.put([:instance, :static_dir], path) + + on_exit(fn -> + {:ok, ["test/fixtures/new_dir/", "test/fixtures/new_dir/robots.txt"]} = File.rm_rf(path) + end) + + RobotsTxt.run(["disallow_all"]) + + assert File.exists?(file_path) + {:ok, file} = File.read(file_path) + + assert file == "User-Agent: *\nDisallow: /\n" + end + + test "to existance folder" do + path = "test/fixtures/" + file_path = path <> "robots.txt" + Pleroma.Config.put([:instance, :static_dir], path) + + on_exit(fn -> + :ok = File.rm(file_path) + end) + + RobotsTxt.run(["disallow_all"]) + + assert File.exists?(file_path) + {:ok, file} = File.read(file_path) + + assert file == "User-Agent: *\nDisallow: /\n" + end +end diff --git a/test/mix/tasks/pleroma/uploads_test.exs b/test/mix/tasks/pleroma/uploads_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.UploadsTest do + alias Pleroma.Upload + use Pleroma.DataCase + + import Mock + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + describe "running migrate_local" do + test "uploads migrated" do + with_mock Upload, + store: fn %Upload{name: _file, path: _path}, _opts -> {:ok, %{}} end do + Mix.Tasks.Pleroma.Uploads.run(["migrate_local", "S3"]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Migrating files from local" + + assert_received {:mix_shell, :info, [message]} + + assert %{"total_count" => total_count} = + Regex.named_captures(~r"^Found (?<total_count>\d+) uploads$", message) + + assert_received {:mix_shell, :info, [message]} + + # @logevery in Mix.Tasks.Pleroma.Uploads + count = + min(50, String.to_integer(total_count)) + |> to_string() + + assert %{"count" => ^count, "total_count" => ^total_count} = + Regex.named_captures( + ~r"^Uploaded (?<count>\d+)/(?<total_count>\d+) files$", + message + ) + end + end + + test "nonexistent uploader" do + assert_raise RuntimeError, ~r/The uploader .* is not an existing/, fn -> + Mix.Tasks.Pleroma.Uploads.run(["migrate_local", "nonexistent"]) + end + end + end +end diff --git a/test/mix/tasks/pleroma/user_test.exs b/test/mix/tasks/pleroma/user_test.exs @@ -0,0 +1,614 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.UserTest do + alias Pleroma.Activity + alias Pleroma.MFA + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.Token + + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + import ExUnit.CaptureIO + import Mock + import Pleroma.Factory + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + describe "running new" do + test "user is created" do + # just get random data + unsaved = build(:user) + + # prepare to answer yes + send(self(), {:mix_shell_input, :yes?, true}) + + Mix.Tasks.Pleroma.User.run([ + "new", + unsaved.nickname, + unsaved.email, + "--name", + unsaved.name, + "--bio", + unsaved.bio, + "--password", + "test", + "--moderator", + "--admin" + ]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "user will be created" + + assert_received {:mix_shell, :yes?, [message]} + assert message =~ "Continue" + + assert_received {:mix_shell, :info, [message]} + assert message =~ "created" + + user = User.get_cached_by_nickname(unsaved.nickname) + assert user.name == unsaved.name + assert user.email == unsaved.email + assert user.bio == unsaved.bio + assert user.is_moderator + assert user.is_admin + end + + test "user is not created" do + unsaved = build(:user) + + # prepare to answer no + send(self(), {:mix_shell_input, :yes?, false}) + + Mix.Tasks.Pleroma.User.run(["new", unsaved.nickname, unsaved.email]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "user will be created" + + assert_received {:mix_shell, :yes?, [message]} + assert message =~ "Continue" + + assert_received {:mix_shell, :info, [message]} + assert message =~ "will not be created" + + refute User.get_cached_by_nickname(unsaved.nickname) + end + end + + describe "running rm" do + test "user is deleted" do + clear_config([:instance, :federating], true) + user = insert(:user) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + Mix.Tasks.Pleroma.User.run(["rm", user.nickname]) + ObanHelpers.perform_all() + + assert_received {:mix_shell, :info, [message]} + assert message =~ " deleted" + assert %{deactivated: true} = User.get_by_nickname(user.nickname) + + assert called(Pleroma.Web.Federator.publish(:_)) + end + end + + test "a remote user's create activity is deleted when the object has been pruned" do + user = insert(:user) + user2 = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "uguu"}) + {:ok, post2} = CommonAPI.post(user2, %{status: "test"}) + obj = Object.normalize(post2) + + {:ok, like_object, meta} = Pleroma.Web.ActivityPub.Builder.like(user, obj) + + {:ok, like_activity, _meta} = + Pleroma.Web.ActivityPub.Pipeline.common_pipeline( + like_object, + Keyword.put(meta, :local, true) + ) + + like_activity.data["object"] + |> Pleroma.Object.get_by_ap_id() + |> Repo.delete() + + clear_config([:instance, :federating], true) + + object = Object.normalize(post) + Object.prune(object) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + Mix.Tasks.Pleroma.User.run(["rm", user.nickname]) + ObanHelpers.perform_all() + + assert_received {:mix_shell, :info, [message]} + assert message =~ " deleted" + assert %{deactivated: true} = User.get_by_nickname(user.nickname) + + assert called(Pleroma.Web.Federator.publish(:_)) + refute Pleroma.Repo.get(Pleroma.Activity, like_activity.id) + end + + refute Activity.get_by_id(post.id) + end + + test "no user to delete" do + Mix.Tasks.Pleroma.User.run(["rm", "nonexistent"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "running toggle_activated" do + test "user is deactivated" do + user = insert(:user) + + Mix.Tasks.Pleroma.User.run(["toggle_activated", user.nickname]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ " deactivated" + + user = User.get_cached_by_nickname(user.nickname) + assert user.deactivated + end + + test "user is activated" do + user = insert(:user, deactivated: true) + + Mix.Tasks.Pleroma.User.run(["toggle_activated", user.nickname]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ " activated" + + user = User.get_cached_by_nickname(user.nickname) + refute user.deactivated + end + + test "no user to toggle" do + Mix.Tasks.Pleroma.User.run(["toggle_activated", "nonexistent"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No user" + end + end + + describe "running deactivate" do + test "user is unsubscribed" do + followed = insert(:user) + remote_followed = insert(:user, local: false) + user = insert(:user) + + User.follow(user, followed, :follow_accept) + User.follow(user, remote_followed, :follow_accept) + + Mix.Tasks.Pleroma.User.run(["deactivate", user.nickname]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Deactivating" + + # Note that the task has delay :timer.sleep(500) + assert_received {:mix_shell, :info, [message]} + assert message =~ "Successfully unsubscribed" + + user = User.get_cached_by_nickname(user.nickname) + assert Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) + assert user.deactivated + end + + test "no user to deactivate" do + Mix.Tasks.Pleroma.User.run(["deactivate", "nonexistent"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No user" + end + end + + describe "running set" do + test "All statuses set" do + user = insert(:user) + + Mix.Tasks.Pleroma.User.run([ + "set", + user.nickname, + "--admin", + "--confirmed", + "--locked", + "--moderator" + ]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Admin status .* true/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Confirmation pending .* false/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Locked status .* true/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Moderator status .* true/ + + user = User.get_cached_by_nickname(user.nickname) + assert user.is_moderator + assert user.locked + assert user.is_admin + refute user.confirmation_pending + end + + test "All statuses unset" do + user = + insert(:user, locked: true, is_moderator: true, is_admin: true, confirmation_pending: true) + + Mix.Tasks.Pleroma.User.run([ + "set", + user.nickname, + "--no-admin", + "--no-confirmed", + "--no-locked", + "--no-moderator" + ]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Admin status .* false/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Confirmation pending .* true/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Locked status .* false/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Moderator status .* false/ + + user = User.get_cached_by_nickname(user.nickname) + refute user.is_moderator + refute user.locked + refute user.is_admin + assert user.confirmation_pending + end + + test "no user to set status" do + Mix.Tasks.Pleroma.User.run(["set", "nonexistent", "--moderator"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "running reset_password" do + test "password reset token is generated" do + user = insert(:user) + + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run(["reset_password", user.nickname]) + end) =~ "URL:" + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Generated" + end + + test "no user to reset password" do + Mix.Tasks.Pleroma.User.run(["reset_password", "nonexistent"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "running reset_mfa" do + test "disables MFA" do + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: "xx", confirmed: true} + } + ) + + Mix.Tasks.Pleroma.User.run(["reset_mfa", user.nickname]) + + assert_received {:mix_shell, :info, [message]} + assert message == "Multi-Factor Authentication disabled for #{user.nickname}" + + assert %{enabled: false, totp: false} == + user.nickname + |> User.get_cached_by_nickname() + |> MFA.mfa_settings() + end + + test "no user to reset MFA" do + Mix.Tasks.Pleroma.User.run(["reset_password", "nonexistent"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "running invite" do + test "invite token is generated" do + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run(["invite"]) + end) =~ "http" + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Generated user invite token one time" + end + + test "token is generated with expires_at" do + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "invite", + "--expires-at", + Date.to_string(Date.utc_today()) + ]) + end) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Generated user invite token date limited" + end + + test "token is generated with max use" do + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "invite", + "--max-use", + "5" + ]) + end) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Generated user invite token reusable" + end + + test "token is generated with max use and expires date" do + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "invite", + "--max-use", + "5", + "--expires-at", + Date.to_string(Date.utc_today()) + ]) + end) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Generated user invite token reusable date limited" + end + end + + describe "running invites" do + test "invites are listed" do + {:ok, invite} = Pleroma.UserInviteToken.create_invite() + + {:ok, invite2} = + Pleroma.UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 15}) + + # assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "invites" + ]) + + # end) + + assert_received {:mix_shell, :info, [message]} + assert_received {:mix_shell, :info, [message2]} + assert_received {:mix_shell, :info, [message3]} + assert message =~ "Invites list:" + assert message2 =~ invite.invite_type + assert message3 =~ invite2.invite_type + end + end + + describe "running revoke_invite" do + test "invite is revoked" do + {:ok, invite} = Pleroma.UserInviteToken.create_invite(%{expires_at: Date.utc_today()}) + + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "revoke_invite", + invite.token + ]) + end) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Invite for token #{invite.token} was revoked." + end + + test "it prints an error message when invite is not exist" do + Mix.Tasks.Pleroma.User.run(["revoke_invite", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No invite found" + end + end + + describe "running delete_activities" do + test "activities are deleted" do + %{nickname: nickname} = insert(:user) + + assert :ok == Mix.Tasks.Pleroma.User.run(["delete_activities", nickname]) + assert_received {:mix_shell, :info, [message]} + assert message == "User #{nickname} statuses deleted." + end + + test "it prints an error message when user is not exist" do + Mix.Tasks.Pleroma.User.run(["delete_activities", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "running toggle_confirmed" do + test "user is confirmed" do + %{id: id, nickname: nickname} = insert(:user, confirmation_pending: false) + + assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname]) + assert_received {:mix_shell, :info, [message]} + assert message == "#{nickname} needs confirmation." + + user = Repo.get(User, id) + assert user.confirmation_pending + assert user.confirmation_token + end + + test "user is not confirmed" do + %{id: id, nickname: nickname} = + insert(:user, confirmation_pending: true, confirmation_token: "some token") + + assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname]) + assert_received {:mix_shell, :info, [message]} + assert message == "#{nickname} doesn't need confirmation." + + user = Repo.get(User, id) + refute user.confirmation_pending + refute user.confirmation_token + end + + test "it prints an error message when user is not exist" do + Mix.Tasks.Pleroma.User.run(["toggle_confirmed", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "search" do + test "it returns users matching" do + user = insert(:user) + moon = insert(:user, nickname: "moon", name: "fediverse expert moon") + moot = insert(:user, nickname: "moot") + kawen = insert(:user, nickname: "kawen", name: "fediverse expert moon") + + {:ok, user} = User.follow(user, moon) + + assert [moon.id, kawen.id] == User.Search.search("moon") |> Enum.map(& &1.id) + + res = User.search("moo") |> Enum.map(& &1.id) + assert Enum.sort([moon.id, moot.id, kawen.id]) == Enum.sort(res) + + assert [kawen.id, moon.id] == User.Search.search("expert fediverse") |> Enum.map(& &1.id) + + assert [moon.id, kawen.id] == + User.Search.search("expert fediverse", for_user: user) |> Enum.map(& &1.id) + end + end + + describe "signing out" do + test "it deletes all user's tokens and authorizations" do + user = insert(:user) + insert(:oauth_token, user: user) + insert(:oauth_authorization, user: user) + + assert Repo.get_by(Token, user_id: user.id) + assert Repo.get_by(Authorization, user_id: user.id) + + :ok = Mix.Tasks.Pleroma.User.run(["sign_out", user.nickname]) + + refute Repo.get_by(Token, user_id: user.id) + refute Repo.get_by(Authorization, user_id: user.id) + end + + test "it prints an error message when user is not exist" do + Mix.Tasks.Pleroma.User.run(["sign_out", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "tagging" do + test "it add tags to a user" do + user = insert(:user) + + :ok = Mix.Tasks.Pleroma.User.run(["tag", user.nickname, "pleroma"]) + + user = User.get_cached_by_nickname(user.nickname) + assert "pleroma" in user.tags + end + + test "it prints an error message when user is not exist" do + Mix.Tasks.Pleroma.User.run(["tag", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "Could not change user tags" + end + end + + describe "untagging" do + test "it deletes tags from a user" do + user = insert(:user, tags: ["pleroma"]) + assert "pleroma" in user.tags + + :ok = Mix.Tasks.Pleroma.User.run(["untag", user.nickname, "pleroma"]) + + user = User.get_cached_by_nickname(user.nickname) + assert Enum.empty?(user.tags) + end + + test "it prints an error message when user is not exist" do + Mix.Tasks.Pleroma.User.run(["untag", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "Could not change user tags" + end + end + + describe "bulk confirm and unconfirm" do + test "confirm all" do + user1 = insert(:user, confirmation_pending: true) + user2 = insert(:user, confirmation_pending: true) + + assert user1.confirmation_pending + assert user2.confirmation_pending + + Mix.Tasks.Pleroma.User.run(["confirm_all"]) + + user1 = User.get_cached_by_nickname(user1.nickname) + user2 = User.get_cached_by_nickname(user2.nickname) + + refute user1.confirmation_pending + refute user2.confirmation_pending + end + + test "unconfirm all" do + user1 = insert(:user, confirmation_pending: false) + user2 = insert(:user, confirmation_pending: false) + admin = insert(:user, is_admin: true, confirmation_pending: false) + mod = insert(:user, is_moderator: true, confirmation_pending: false) + + refute user1.confirmation_pending + refute user2.confirmation_pending + + Mix.Tasks.Pleroma.User.run(["unconfirm_all"]) + + user1 = User.get_cached_by_nickname(user1.nickname) + user2 = User.get_cached_by_nickname(user2.nickname) + admin = User.get_cached_by_nickname(admin.nickname) + mod = User.get_cached_by_nickname(mod.nickname) + + assert user1.confirmation_pending + assert user2.confirmation_pending + refute admin.confirmation_pending + refute mod.confirmation_pending + end + end +end diff --git a/test/moderation_log_test.exs b/test/moderation_log_test.exs @@ -1,297 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ModerationLogTest do - alias Pleroma.Activity - alias Pleroma.ModerationLog - - use Pleroma.DataCase - - import Pleroma.Factory - - describe "user moderation" do - setup do - admin = insert(:user, is_admin: true) - moderator = insert(:user, is_moderator: true) - subject1 = insert(:user) - subject2 = insert(:user) - - [admin: admin, moderator: moderator, subject1: subject1, subject2: subject2] - end - - test "logging user deletion by moderator", %{moderator: moderator, subject1: subject1} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - subject: [subject1], - action: "delete" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == "@#{moderator.nickname} deleted users: @#{subject1.nickname}" - end - - test "logging user creation by moderator", %{ - moderator: moderator, - subject1: subject1, - subject2: subject2 - } do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - subjects: [subject1, subject2], - action: "create" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} created users: @#{subject1.nickname}, @#{subject2.nickname}" - end - - test "logging user follow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: admin, - followed: subject1, - follower: subject2, - action: "follow" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{admin.nickname} made @#{subject2.nickname} follow @#{subject1.nickname}" - end - - test "logging user unfollow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: admin, - followed: subject1, - follower: subject2, - action: "unfollow" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{admin.nickname} made @#{subject2.nickname} unfollow @#{subject1.nickname}" - end - - test "logging user tagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: admin, - nicknames: [subject1.nickname, subject2.nickname], - tags: ["foo", "bar"], - action: "tag" - }) - - log = Repo.one(ModerationLog) - - users = - [subject1.nickname, subject2.nickname] - |> Enum.map(&"@#{&1}") - |> Enum.join(", ") - - tags = ["foo", "bar"] |> Enum.join(", ") - - assert log.data["message"] == "@#{admin.nickname} added tags: #{tags} to users: #{users}" - end - - test "logging user untagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: admin, - nicknames: [subject1.nickname, subject2.nickname], - tags: ["foo", "bar"], - action: "untag" - }) - - log = Repo.one(ModerationLog) - - users = - [subject1.nickname, subject2.nickname] - |> Enum.map(&"@#{&1}") - |> Enum.join(", ") - - tags = ["foo", "bar"] |> Enum.join(", ") - - assert log.data["message"] == - "@#{admin.nickname} removed tags: #{tags} from users: #{users}" - end - - test "logging user grant by moderator", %{moderator: moderator, subject1: subject1} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - subject: [subject1], - action: "grant", - permission: "moderator" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == "@#{moderator.nickname} made @#{subject1.nickname} moderator" - end - - test "logging user revoke by moderator", %{moderator: moderator, subject1: subject1} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - subject: [subject1], - action: "revoke", - permission: "moderator" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} revoked moderator role from @#{subject1.nickname}" - end - - test "logging relay follow", %{moderator: moderator} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - action: "relay_follow", - target: "https://example.org/relay" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} followed relay: https://example.org/relay" - end - - test "logging relay unfollow", %{moderator: moderator} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - action: "relay_unfollow", - target: "https://example.org/relay" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} unfollowed relay: https://example.org/relay" - end - - test "logging report update", %{moderator: moderator} do - report = %Activity{ - id: "9m9I1F4p8ftrTP6QTI", - data: %{ - "type" => "Flag", - "state" => "resolved" - } - } - - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - action: "report_update", - subject: report - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} updated report ##{report.id} with 'resolved' state" - end - - test "logging report response", %{moderator: moderator} do - report = %Activity{ - id: "9m9I1F4p8ftrTP6QTI", - data: %{ - "type" => "Note" - } - } - - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - action: "report_note", - subject: report, - text: "look at this" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} added note 'look at this' to report ##{report.id}" - end - - test "logging status sensitivity update", %{moderator: moderator} do - note = insert(:note_activity) - - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - action: "status_update", - subject: note, - sensitive: "true", - visibility: nil - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true'" - end - - test "logging status visibility update", %{moderator: moderator} do - note = insert(:note_activity) - - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - action: "status_update", - subject: note, - sensitive: nil, - visibility: "private" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} updated status ##{note.id}, set visibility: 'private'" - end - - test "logging status sensitivity & visibility update", %{moderator: moderator} do - note = insert(:note_activity) - - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - action: "status_update", - subject: note, - sensitive: "true", - visibility: "private" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true', visibility: 'private'" - end - - test "logging status deletion", %{moderator: moderator} do - note = insert(:note_activity) - - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - action: "status_delete", - subject_id: note.id - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == "@#{moderator.nickname} deleted status ##{note.id}" - end - end -end diff --git a/test/notification_test.exs b/test/notification_test.exs @@ -1,1144 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.NotificationTest do - use Pleroma.DataCase - - import Pleroma.Factory - import Mock - - alias Pleroma.FollowingRelationship - alias Pleroma.Notification - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.NotificationView - alias Pleroma.Web.Push - alias Pleroma.Web.Streamer - - describe "create_notifications" do - test "never returns nil" do - user = insert(:user) - other_user = insert(:user, %{invisible: true}) - - {:ok, activity} = CommonAPI.post(user, %{status: "yeah"}) - {:ok, activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") - - refute {:ok, [nil]} == Notification.create_notifications(activity) - end - - test "creates a notification for an emoji reaction" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "yeah"}) - {:ok, activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") - - {:ok, [notification]} = Notification.create_notifications(activity) - - assert notification.user_id == user.id - assert notification.type == "pleroma:emoji_reaction" - end - - test "notifies someone when they are directly addressed" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "hey @#{other_user.nickname} and @#{third_user.nickname}" - }) - - {:ok, [notification, other_notification]} = Notification.create_notifications(activity) - - notified_ids = Enum.sort([notification.user_id, other_notification.user_id]) - assert notified_ids == [other_user.id, third_user.id] - assert notification.activity_id == activity.id - assert notification.type == "mention" - assert other_notification.activity_id == activity.id - - assert [%Pleroma.Marker{unread_count: 2}] = - Pleroma.Marker.get_markers(other_user, ["notifications"]) - end - - test "it creates a notification for subscribed users" do - user = insert(:user) - subscriber = insert(:user) - - User.subscribe(subscriber, user) - - {:ok, status} = CommonAPI.post(user, %{status: "Akariiiin"}) - {:ok, [notification]} = Notification.create_notifications(status) - - assert notification.user_id == subscriber.id - end - - test "does not create a notification for subscribed users if status is a reply" do - user = insert(:user) - other_user = insert(:user) - subscriber = insert(:user) - - User.subscribe(subscriber, other_user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - - {:ok, _reply_activity} = - CommonAPI.post(other_user, %{ - status: "test reply", - in_reply_to_status_id: activity.id - }) - - user_notifications = Notification.for_user(user) - assert length(user_notifications) == 1 - - subscriber_notifications = Notification.for_user(subscriber) - assert Enum.empty?(subscriber_notifications) - end - end - - describe "CommonApi.post/2 notification-related functionality" do - test_with_mock "creates but does NOT send notification to blocker user", - Push, - [:passthrough], - [] do - user = insert(:user) - blocker = insert(:user) - {:ok, _user_relationship} = User.block(blocker, user) - - {:ok, _activity} = CommonAPI.post(user, %{status: "hey @#{blocker.nickname}!"}) - - blocker_id = blocker.id - assert [%Notification{user_id: ^blocker_id}] = Repo.all(Notification) - refute called(Push.send(:_)) - end - - 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, _activity} = CommonAPI.post(user, %{status: "hey @#{muter.nickname}!"}) - - muter_id = muter.id - assert [%Notification{user_id: ^muter_id}] = Repo.all(Notification) - refute called(Push.send(:_)) - end - - test_with_mock "creates but does NOT send notification to thread-muter user", - Push, - [:passthrough], - [] 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) - - {:ok, _same_context_activity} = - CommonAPI.post(user, %{ - status: "hey-hey-hey @#{thread_muter.nickname}!", - in_reply_to_status_id: activity.id - }) - - [pre_mute_notification, post_mute_notification] = - Repo.all(from(n in Notification, where: n.user_id == ^thread_muter.id, order_by: n.id)) - - pre_mute_notification_id = pre_mute_notification.id - post_mute_notification_id = post_mute_notification.id - - 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) - ) - ) - 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, false) - - {:ok, activity} = CommonAPI.post(muted, %{status: "Hi @#{muter.nickname}"}) - - assert Notification.create_notification(activity, muter) - end - - 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) - - {:ok, activity} = - CommonAPI.post(other_user, %{ - status: "Hi @#{muter.nickname}", - in_reply_to_status_id: activity.id - }) - - notification = Notification.create_notification(activity, muter) - - assert notification.id - assert notification.seen - end - - test "it disables notifications from strangers" do - follower = insert(:user) - - followed = - insert(:user, - notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true} - ) - - {:ok, activity} = CommonAPI.post(follower, %{status: "hey @#{followed.nickname}"}) - refute Notification.create_notification(activity, followed) - 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"]) - - refute Notification.create_notification(activity, author) - end - - test "it doesn't create duplicate notifications for follow+subscribed users" do - user = insert(:user) - subscriber = insert(:user) - - {:ok, _, _, _} = CommonAPI.follow(subscriber, user) - User.subscribe(subscriber, user) - {:ok, status} = CommonAPI.post(user, %{status: "Akariiiin"}) - {:ok, [_notif]} = Notification.create_notifications(status) - end - - test "it doesn't create subscription notifications if the recipient cannot see the status" do - user = insert(:user) - subscriber = insert(:user) - - User.subscribe(subscriber, user) - - {:ok, status} = CommonAPI.post(user, %{status: "inwisible", visibility: "direct"}) - - assert {:ok, []} == Notification.create_notifications(status) - end - - test "it disables notifications from people who are invisible" do - author = insert(:user, invisible: true) - user = insert(:user) - - {:ok, status} = CommonAPI.post(author, %{status: "hey @#{user.nickname}"}) - refute Notification.create_notification(status, user) - end - - test "it doesn't create notifications if content matches with an irreversible filter" do - user = insert(:user) - subscriber = insert(:user) - - User.subscribe(subscriber, user) - insert(:filter, user: subscriber, phrase: "cofe", hide: true) - - {:ok, status} = CommonAPI.post(user, %{status: "got cofe?"}) - - assert {:ok, []} == Notification.create_notifications(status) - end - - test "it creates notifications if content matches with a not irreversible filter" do - user = insert(:user) - subscriber = insert(:user) - - User.subscribe(subscriber, user) - insert(:filter, user: subscriber, phrase: "cofe", hide: false) - - {:ok, status} = CommonAPI.post(user, %{status: "got cofe?"}) - {:ok, [notification]} = Notification.create_notifications(status) - - assert notification - refute notification.seen - end - - test "it creates notifications when someone likes user's status with a filtered word" do - user = insert(:user) - other_user = insert(:user) - insert(:filter, user: user, phrase: "tesla", hide: true) - - {:ok, activity_one} = CommonAPI.post(user, %{status: "wow tesla"}) - {:ok, activity_two} = CommonAPI.favorite(other_user, activity_one.id) - - {:ok, [notification]} = Notification.create_notifications(activity_two) - - assert notification - refute notification.seen - end - end - - describe "follow / follow_request notifications" do - test "it creates `follow` notification for approved Follow activity" do - user = insert(:user) - followed_user = insert(:user, locked: false) - - {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) - assert FollowingRelationship.following?(user, followed_user) - assert [notification] = Notification.for_user(followed_user) - - assert %{type: "follow"} = - NotificationView.render("show.json", %{ - notification: notification, - for: followed_user - }) - end - - test "it creates `follow_request` notification for pending Follow activity" do - user = insert(:user) - followed_user = insert(:user, locked: true) - - {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) - refute FollowingRelationship.following?(user, followed_user) - assert [notification] = Notification.for_user(followed_user) - - render_opts = %{notification: notification, for: followed_user} - assert %{type: "follow_request"} = NotificationView.render("show.json", render_opts) - - # After request is accepted, the same notification is rendered with type "follow": - assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user) - - notification = - Repo.get(Notification, notification.id) - |> Repo.preload(:activity) - - assert %{type: "follow"} = - NotificationView.render("show.json", notification: notification, for: followed_user) - end - - test "it doesn't create a notification for follow-unfollow-follow chains" do - user = insert(:user) - followed_user = insert(:user, locked: false) - - {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) - assert FollowingRelationship.following?(user, followed_user) - assert [notification] = Notification.for_user(followed_user) - - CommonAPI.unfollow(user, followed_user) - {:ok, _, _, _activity_dupe} = CommonAPI.follow(user, followed_user) - - notification_id = notification.id - assert [%{id: ^notification_id}] = Notification.for_user(followed_user) - end - - test "dismisses the notification on follow request rejection" do - user = insert(:user, locked: true) - follower = insert(:user) - {:ok, _, _, _follow_activity} = CommonAPI.follow(follower, user) - assert [notification] = Notification.for_user(user) - {:ok, _follower} = CommonAPI.reject_follow_request(follower, user) - assert [] = Notification.for_user(user) - end - end - - describe "get notification" do - test "it gets a notification that belongs to the user" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"}) - - {:ok, [notification]} = Notification.create_notifications(activity) - {:ok, notification} = Notification.get(other_user, notification.id) - - assert notification.user_id == other_user.id - end - - test "it returns error if the notification doesn't belong to the user" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"}) - - {:ok, [notification]} = Notification.create_notifications(activity) - {:error, _notification} = Notification.get(user, notification.id) - end - end - - describe "dismiss notification" do - test "it dismisses a notification that belongs to the user" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"}) - - {:ok, [notification]} = Notification.create_notifications(activity) - {:ok, notification} = Notification.dismiss(other_user, notification.id) - - assert notification.user_id == other_user.id - end - - test "it returns error if the notification doesn't belong to the user" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"}) - - {:ok, [notification]} = Notification.create_notifications(activity) - {:error, _notification} = Notification.dismiss(user, notification.id) - end - end - - describe "clear notification" do - test "it clears all notifications belonging to the user" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "hey @#{other_user.nickname} and @#{third_user.nickname} !" - }) - - {:ok, _notifs} = Notification.create_notifications(activity) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "hey again @#{other_user.nickname} and @#{third_user.nickname} !" - }) - - {:ok, _notifs} = Notification.create_notifications(activity) - Notification.clear(other_user) - - assert Notification.for_user(other_user) == [] - assert Notification.for_user(third_user) != [] - 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) - other_user = insert(:user) - - {:ok, _activity} = - CommonAPI.post(user, %{ - status: "hey @#{other_user.nickname}!" - }) - - {:ok, _activity} = - CommonAPI.post(user, %{ - status: "hey again @#{other_user.nickname}!" - }) - - [n2, n1] = Notification.for_user(other_user) - - assert n2.id > n1.id - - {:ok, _activity} = - CommonAPI.post(user, %{ - status: "hey yet again @#{other_user.nickname}!" - }) - - [_, read_notification] = Notification.set_read_up_to(other_user, n2.id) - - assert read_notification.activity.object - - [n3, n2, n1] = Notification.for_user(other_user) - - assert n1.seen == true - assert n2.seen == true - assert n3.seen == false - - assert %Pleroma.Marker{} = - m = - Pleroma.Repo.get_by( - Pleroma.Marker, - user_id: other_user.id, - timeline: "notifications" - ) - - assert m.last_read_id == to_string(n2.id) - end - end - - describe "for_user_since/2" do - defp days_ago(days) do - NaiveDateTime.add( - NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), - -days * 60 * 60 * 24, - :second - ) - end - - test "Returns recent notifications" do - user1 = insert(:user) - user2 = insert(:user) - - Enum.each(0..10, fn i -> - {:ok, _activity} = - CommonAPI.post(user1, %{ - status: "hey ##{i} @#{user2.nickname}!" - }) - end) - - {old, new} = Enum.split(Notification.for_user(user2), 5) - - Enum.each(old, fn notification -> - notification - |> cast(%{updated_at: days_ago(10)}, [:updated_at]) - |> Pleroma.Repo.update!() - end) - - recent_notifications_ids = - user2 - |> Notification.for_user_since( - NaiveDateTime.add(NaiveDateTime.utc_now(), -5 * 86_400, :second) - ) - |> Enum.map(& &1.id) - - Enum.each(old, fn %{id: id} -> - refute id in recent_notifications_ids - end) - - Enum.each(new, fn %{id: id} -> - assert id in recent_notifications_ids - end) - end - end - - describe "notification target determination / get_notified_from_activity/2" do - test "it sends notifications to addressed users in new messages" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "hey @#{other_user.nickname}!" - }) - - {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) - - assert other_user in enabled_receivers - end - - test "it sends notifications to mentioned users in new messages" do - user = insert(:user) - other_user = insert(:user) - - create_activity = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "actor" => user.ap_id, - "object" => %{ - "type" => "Note", - "content" => "message with a Mention tag, but no explicit tagging", - "tag" => [ - %{ - "type" => "Mention", - "href" => other_user.ap_id, - "name" => other_user.nickname - } - ], - "attributedTo" => user.ap_id - } - } - - {:ok, activity} = Transmogrifier.handle_incoming(create_activity) - - {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) - - assert other_user in enabled_receivers - end - - test "it does not send notifications to users who are only cc in new messages" do - user = insert(:user) - other_user = insert(:user) - - create_activity = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [other_user.ap_id], - "actor" => user.ap_id, - "object" => %{ - "type" => "Note", - "content" => "hi everyone", - "attributedTo" => user.ap_id - } - } - - {:ok, activity} = Transmogrifier.handle_incoming(create_activity) - - {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) - - assert other_user not in enabled_receivers - end - - test "it does not send notification to mentioned users in likes" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - - {:ok, activity_one} = - CommonAPI.post(user, %{ - status: "hey @#{other_user.nickname}!" - }) - - {:ok, activity_two} = CommonAPI.favorite(third_user, activity_one.id) - - {enabled_receivers, _disabled_receivers} = - Notification.get_notified_from_activity(activity_two) - - assert other_user not in enabled_receivers - end - - test "it only notifies the post's author in likes" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - - {:ok, activity_one} = - CommonAPI.post(user, %{ - status: "hey @#{other_user.nickname}!" - }) - - {:ok, like_data, _} = Builder.like(third_user, activity_one.object) - - {:ok, like, _} = - like_data - |> Map.put("to", [other_user.ap_id | like_data["to"]]) - |> ActivityPub.persist(local: true) - - {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(like) - - assert other_user not in enabled_receivers - end - - test "it does not send notification to mentioned users in announces" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - - {:ok, activity_one} = - CommonAPI.post(user, %{ - status: "hey @#{other_user.nickname}!" - }) - - {:ok, activity_two} = CommonAPI.repeat(activity_one.id, third_user) - - {enabled_receivers, _disabled_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 - 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) - - assert [] == enabled_receivers - assert [other_user] == disabled_receivers - end - - test "it returns notification-muting recipient in disabled 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) - - assert [] == enabled_receivers - assert [other_user] == disabled_receivers - end - - test "it returns thread-muting recipient in disabled recipients list" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) - - {:ok, _} = CommonAPI.add_mute(other_user, activity) - - {:ok, same_context_activity} = - CommonAPI.post(user, %{ - status: "hey-hey-hey @#{other_user.nickname}!", - in_reply_to_status_id: activity.id - }) - - {enabled_receivers, disabled_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 - blocked_domain = "blocked.domain" - user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"}) - other_user = insert(:user) - - {:ok, other_user} = User.block_domain(other_user, blocked_domain) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) - - {enabled_receivers, disabled_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 - blocked_domain = "blocked.domain" - user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"}) - other_user = insert(:user) - - {:ok, other_user} = User.block_domain(other_user, blocked_domain) - {:ok, other_user} = User.follow(other_user, user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) - - {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) - - assert [other_user] == enabled_receivers - assert [] == disabled_receivers - end - end - - describe "notification lifecycle" do - test "liking an activity results in 1 notification, then 0 if the activity is deleted" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - - assert Enum.empty?(Notification.for_user(user)) - - {:ok, _} = CommonAPI.favorite(other_user, activity.id) - - assert length(Notification.for_user(user)) == 1 - - {:ok, _} = CommonAPI.delete(activity.id, user) - - assert Enum.empty?(Notification.for_user(user)) - end - - test "liking an activity results in 1 notification, then 0 if the activity is unliked" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - - assert Enum.empty?(Notification.for_user(user)) - - {:ok, _} = CommonAPI.favorite(other_user, activity.id) - - assert length(Notification.for_user(user)) == 1 - - {:ok, _} = CommonAPI.unfavorite(activity.id, other_user) - - assert Enum.empty?(Notification.for_user(user)) - end - - test "repeating an activity results in 1 notification, then 0 if the activity is deleted" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - - assert Enum.empty?(Notification.for_user(user)) - - {:ok, _} = CommonAPI.repeat(activity.id, other_user) - - assert length(Notification.for_user(user)) == 1 - - {:ok, _} = CommonAPI.delete(activity.id, user) - - assert Enum.empty?(Notification.for_user(user)) - end - - test "repeating an activity results in 1 notification, then 0 if the activity is unrepeated" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - - assert Enum.empty?(Notification.for_user(user)) - - {:ok, _} = CommonAPI.repeat(activity.id, other_user) - - assert length(Notification.for_user(user)) == 1 - - {:ok, _} = CommonAPI.unrepeat(activity.id, other_user) - - assert Enum.empty?(Notification.for_user(user)) - end - - test "liking an activity which is already deleted does not generate a notification" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - - assert Enum.empty?(Notification.for_user(user)) - - {:ok, _deletion_activity} = CommonAPI.delete(activity.id, user) - - assert Enum.empty?(Notification.for_user(user)) - - {:error, :not_found} = CommonAPI.favorite(other_user, activity.id) - - assert Enum.empty?(Notification.for_user(user)) - end - - test "repeating an activity which is already deleted does not generate a notification" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - - assert Enum.empty?(Notification.for_user(user)) - - {:ok, _deletion_activity} = CommonAPI.delete(activity.id, user) - - assert Enum.empty?(Notification.for_user(user)) - - {:error, _} = CommonAPI.repeat(activity.id, other_user) - - 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) - - {:ok, _activity} = - CommonAPI.post(user, %{status: "hi @#{other_user.nickname}", visibility: "direct"}) - - refute Enum.empty?(Notification.for_user(other_user)) - - {:ok, job} = User.delete(user) - ObanHelpers.perform(job) - - assert Enum.empty?(Notification.for_user(other_user)) - end - - test "notifications are deleted if a remote user is deleted" do - remote_user = insert(:user) - local_user = insert(:user) - - dm_message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Create", - "actor" => remote_user.ap_id, - "id" => remote_user.ap_id <> "/activities/test", - "to" => [local_user.ap_id], - "cc" => [], - "object" => %{ - "type" => "Note", - "content" => "Hello!", - "tag" => [ - %{ - "type" => "Mention", - "href" => local_user.ap_id, - "name" => "@#{local_user.nickname}" - } - ], - "to" => [local_user.ap_id], - "cc" => [], - "attributedTo" => remote_user.ap_id - } - } - - {:ok, _dm_activity} = Transmogrifier.handle_incoming(dm_message) - - refute Enum.empty?(Notification.for_user(local_user)) - - delete_user_message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => remote_user.ap_id <> "/activities/delete", - "actor" => remote_user.ap_id, - "type" => "Delete", - "object" => remote_user.ap_id - } - - remote_user_url = remote_user.ap_id - - Tesla.Mock.mock(fn - %{method: :get, url: ^remote_user_url} -> - %Tesla.Env{status: 404, body: ""} - end) - - {:ok, _delete_activity} = Transmogrifier.handle_incoming(delete_user_message) - ObanHelpers.perform_all() - - assert Enum.empty?(Notification.for_user(local_user)) - end - - @tag capture_log: true - test "move activity generates a notification" 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 = insert(:user) - other_follower = insert(:user, %{allow_following_move: false}) - - User.follow(follower, old_user) - User.follow(other_follower, old_user) - - old_user_url = old_user.ap_id - - body = - File.read!("test/fixtures/users_mock/localhost.json") - |> String.replace("{{nickname}}", old_user.nickname) - |> Jason.encode!() - - Tesla.Mock.mock(fn - %{method: :get, url: ^old_user_url} -> - %Tesla.Env{status: 200, body: body} - end) - - Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) - ObanHelpers.perform_all() - - assert [ - %{ - activity: %{ - data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id} - } - } - ] = Notification.for_user(follower) - - assert [ - %{ - activity: %{ - data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id} - } - } - ] = Notification.for_user(other_follower) - end - end - - describe "for_user" do - setup do - user = insert(:user) - - {:ok, %{user: user}} - end - - test "it returns notifications for muted user without notifications", %{user: user} do - muted = insert(:user) - {:ok, _user_relationships} = User.mute(user, muted, false) - - {:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"}) - - [notification] = Notification.for_user(user) - - assert notification.activity.object - assert notification.seen - end - - test "it doesn't return notifications for muted user with notifications", %{user: user} do - muted = insert(:user) - {:ok, _user_relationships} = User.mute(user, muted) - - {:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"}) - - assert Notification.for_user(user) == [] - end - - test "it doesn't return notifications for blocked user", %{user: user} do - blocked = insert(:user) - {:ok, _user_relationship} = User.block(user, blocked) - - {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) - - assert Notification.for_user(user) == [] - end - - test "it doesn't return notifications for domain-blocked non-followed user", %{user: user} do - blocked = insert(:user, ap_id: "http://some-domain.com") - {:ok, user} = User.block_domain(user, "some-domain.com") - - {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) - - assert Notification.for_user(user) == [] - end - - test "it returns notifications for domain-blocked but followed user" do - user = insert(:user) - blocked = insert(:user, ap_id: "http://some-domain.com") - - {:ok, user} = User.block_domain(user, "some-domain.com") - {:ok, _} = User.follow(user, blocked) - - {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) - - assert length(Notification.for_user(user)) == 1 - end - - test "it doesn't return notifications for muted thread", %{user: user} do - another_user = insert(:user) - - {:ok, activity} = CommonAPI.post(another_user, %{status: "hey @#{user.nickname}"}) - - {:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"]) - assert Notification.for_user(user) == [] - end - - test "it returns 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 - end - - test "it doesn't return notifications from a blocked user when with_muted is set", %{ - user: user - } do - blocked = insert(:user) - {:ok, _user_relationship} = User.block(user, blocked) - - {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) - - assert Enum.empty?(Notification.for_user(user, %{with_muted: true})) - end - - test "when with_muted is set, " <> - "it doesn't return notifications from a domain-blocked non-followed user", - %{user: user} do - blocked = insert(:user, ap_id: "http://some-domain.com") - {:ok, user} = User.block_domain(user, "some-domain.com") - - {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) - - assert Enum.empty?(Notification.for_user(user, %{with_muted: true})) - end - - test "it returns notifications from muted threads when with_muted is set", %{user: user} do - another_user = insert(:user) - - {:ok, activity} = CommonAPI.post(another_user, %{status: "hey @#{user.nickname}"}) - - {:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"]) - assert length(Notification.for_user(user, %{with_muted: true})) == 1 - end - - test "it doesn't return notifications about mentions with filtered word", %{user: user} do - insert(:filter, user: user, phrase: "cofe", hide: true) - another_user = insert(:user) - - {:ok, _activity} = CommonAPI.post(another_user, %{status: "@#{user.nickname} got cofe?"}) - - assert Enum.empty?(Notification.for_user(user)) - end - - test "it returns notifications about mentions with not hidden filtered word", %{user: user} do - insert(:filter, user: user, phrase: "test", hide: false) - another_user = insert(:user) - - {:ok, _} = CommonAPI.post(another_user, %{status: "@#{user.nickname} test"}) - - assert length(Notification.for_user(user)) == 1 - end - - test "it returns notifications about favorites with filtered word", %{user: user} do - insert(:filter, user: user, phrase: "cofe", hide: true) - another_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "Give me my cofe!"}) - {:ok, _} = CommonAPI.favorite(another_user, activity.id) - - assert length(Notification.for_user(user)) == 1 - end - end -end diff --git a/test/object/containment_test.exs b/test/object/containment_test.exs @@ -1,125 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Object.ContainmentTest do - use Pleroma.DataCase - - alias Pleroma.Object.Containment - alias Pleroma.User - - import Pleroma.Factory - import ExUnit.CaptureLog - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe "general origin containment" do - test "works for completely actorless posts" do - assert :error == - Containment.contain_origin("https://glaceon.social/users/monorail", %{ - "deleted" => "2019-10-30T05:48:50.249606Z", - "formerType" => "Note", - "id" => "https://glaceon.social/users/monorail/statuses/103049757364029187", - "type" => "Tombstone" - }) - end - - test "contain_origin_from_id() catches obvious spoofing attempts" do - data = %{ - "id" => "http://example.com/~alyssa/activities/1234.json" - } - - :error = - Containment.contain_origin_from_id( - "http://example.org/~alyssa/activities/1234.json", - data - ) - end - - test "contain_origin_from_id() allows alternate IDs within the same origin domain" do - data = %{ - "id" => "http://example.com/~alyssa/activities/1234.json" - } - - :ok = - Containment.contain_origin_from_id( - "http://example.com/~alyssa/activities/1234", - data - ) - end - - test "contain_origin_from_id() allows matching IDs" do - data = %{ - "id" => "http://example.com/~alyssa/activities/1234.json" - } - - :ok = - Containment.contain_origin_from_id( - "http://example.com/~alyssa/activities/1234.json", - data - ) - end - - test "users cannot be collided through fake direction spoofing attempts" 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"}) - }) - - assert capture_log(fn -> - {:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye") - end) =~ - "[error] Could not decode user at fetch https://n1u.moe/users/rye" - end - - test "contain_origin_from_id() gracefully handles cases where no ID is present" do - data = %{ - "type" => "Create", - "object" => %{ - "id" => "http://example.net/~alyssa/activities/1234", - "attributedTo" => "http://example.org/~alyssa" - }, - "actor" => "http://example.com/~bob" - } - - :error = - Containment.contain_origin_from_id("http://example.net/~alyssa/activities/1234", data) - end - end - - describe "containment of children" do - test "contain_child() catches spoofing attempts" do - data = %{ - "id" => "http://example.com/whatever", - "type" => "Create", - "object" => %{ - "id" => "http://example.net/~alyssa/activities/1234", - "attributedTo" => "http://example.org/~alyssa" - }, - "actor" => "http://example.com/~bob" - } - - :error = Containment.contain_child(data) - end - - test "contain_child() allows correct origins" do - data = %{ - "id" => "http://example.org/~alyssa/activities/5678", - "type" => "Create", - "object" => %{ - "id" => "http://example.org/~alyssa/activities/1234", - "attributedTo" => "http://example.org/~alyssa" - }, - "actor" => "http://example.org/~alyssa" - } - - :ok = Containment.contain_child(data) - end - end -end diff --git a/test/object/fetcher_test.exs b/test/object/fetcher_test.exs @@ -1,245 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Object.FetcherTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Config - alias Pleroma.Object - alias Pleroma.Object.Fetcher - - import Mock - import Tesla.Mock - - setup do - mock(fn - %{method: :get, url: "https://mastodon.example.org/users/userisgone"} -> - %Tesla.Env{status: 410} - - %{method: :get, url: "https://mastodon.example.org/users/userisgone404"} -> - %Tesla.Env{status: 404} - - env -> - apply(HttpRequestMock, :request, [env]) - end) - - :ok - end - - describe "error cases" do - setup do - mock(fn - %{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json") - } - - %{method: :get, url: "https://social.sakamoto.gq/users/eal"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/fetch_mocks/eal.json") - } - - %{method: :get, url: "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/fetch_mocks/104410921027210069.json") - } - - %{method: :get, url: "https://busshi.moe/users/tuxcrafting"} -> - %Tesla.Env{ - status: 500 - } - end) - - :ok - end - - @tag capture_log: true - test "it works when fetching the OP actor errors out" do - # Here we simulate a case where the author of the OP can't be read - assert {:ok, _} = - Fetcher.fetch_object_from_id( - "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM" - ) - end - end - - describe "max thread distance restriction" do - @ap_id "http://mastodon.example.org/@admin/99541947525187367" - setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) - - test "it returns thread depth exceeded error if thread depth is exceeded" do - Config.put([:instance, :federation_incoming_replies_max_depth], 0) - - assert {:error, "Max thread distance exceeded."} = - 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 - Config.put([:instance, :federation_incoming_replies_max_depth], 0) - - assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id) - end - - test "it fetches object if requested depth does not exceed max thread depth" do - Config.put([:instance, :federation_incoming_replies_max_depth], 10) - - assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id, depth: 10) - end - end - - describe "actor origin containment" do - test "it rejects objects with a bogus origin" do - {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json") - end - - test "it rejects objects when attributedTo is wrong (variant 1)" do - {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json") - end - - test "it rejects objects when attributedTo is wrong (variant 2)" do - {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json") - end - end - - describe "fetching an object" do - test "it fetches an object" do - {:ok, object} = - Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") - - assert activity = Activity.get_create_by_object_ap_id(object.data["id"]) - assert activity.data["id"] - - {:ok, object_again} = - Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") - - assert [attachment] = object.data["attachment"] - assert is_list(attachment["url"]) - - assert object == object_again - end - - test "Return MRF reason when fetched status is rejected by one" do - clear_config([:mrf_keyword, :reject], ["yeah"]) - clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) - - assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} == - Fetcher.fetch_object_from_id( - "http://mastodon.example.org/@admin/99541947525187367" - ) - end - end - - describe "implementation quirks" do - test "it can fetch plume articles" do - {:ok, object} = - Fetcher.fetch_object_from_id( - "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/" - ) - - assert object - end - - test "it can fetch peertube videos" do - {:ok, object} = - Fetcher.fetch_object_from_id( - "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" - ) - - assert object - end - - test "it can fetch Mobilizon events" do - {:ok, object} = - Fetcher.fetch_object_from_id( - "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" - ) - - assert object - end - - test "it can fetch wedistribute articles" do - {:ok, object} = - Fetcher.fetch_object_from_id("https://wedistribute.org/wp-json/pterotype/v1/object/85810") - - assert object - end - - test "all objects with fake directions are rejected by the object fetcher" do - assert {:error, _} = - Fetcher.fetch_and_contain_remote_object_from_id( - "https://info.pleroma.site/activity4.json" - ) - end - - test "handle HTTP 410 Gone response" do - assert {:error, "Object has been deleted"} == - 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"} == - Fetcher.fetch_and_contain_remote_object_from_id( - "https://mastodon.example.org/users/userisgone404" - ) - end - - test "it can fetch pleroma polls with attachments" do - {:ok, object} = - Fetcher.fetch_object_from_id("https://patch.cx/objects/tesla_mock/poll_attachment") - - assert object - end - end - - describe "pruning" do - test "it can refetch pruned objects" do - object_id = "http://mastodon.example.org/@admin/99541947525187367" - - {:ok, object} = Fetcher.fetch_object_from_id(object_id) - - assert object - - {:ok, _object} = Object.prune(object) - - refute Object.get_by_ap_id(object_id) - - {:ok, %Object{} = object_two} = Fetcher.fetch_object_from_id(object_id) - - assert object.data["id"] == object_two.data["id"] - assert object.id != object_two.id - end - end - - describe "signed fetches" do - setup do: clear_config([:activitypub, :sign_object_fetches]) - - test_with_mock "it signs fetches when configured to do so", - Pleroma.Signature, - [:passthrough], - [] do - Config.put([:activitypub, :sign_object_fetches], true) - - Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") - - assert called(Pleroma.Signature.sign(:_, :_)) - end - - test_with_mock "it doesn't sign fetches when not configured to do so", - Pleroma.Signature, - [:passthrough], - [] do - Config.put([:activitypub, :sign_object_fetches], false) - - Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") - - refute called(Pleroma.Signature.sign(:_, :_)) - end - end -end diff --git a/test/object_test.exs b/test/object_test.exs @@ -1,402 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ObjectTest do - use Pleroma.DataCase - use Oban.Testing, repo: Pleroma.Repo - import ExUnit.CaptureLog - import Pleroma.Factory - import Tesla.Mock - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.Web.CommonAPI - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "returns an object by it's AP id" do - object = insert(:note) - found_object = Object.get_by_ap_id(object.data["id"]) - - assert object == found_object - end - - describe "generic changeset" do - test "it ensures uniqueness of the id" do - object = insert(:note) - cs = Object.change(%Object{}, %{data: %{id: object.data["id"]}}) - assert cs.valid? - - {:error, _result} = Repo.insert(cs) - end - end - - describe "deletion function" do - test "deletes an object" do - object = insert(:note) - found_object = Object.get_by_ap_id(object.data["id"]) - - assert object == found_object - - Object.delete(found_object) - - found_object = Object.get_by_ap_id(object.data["id"]) - - refute object == found_object - - assert found_object.data["type"] == "Tombstone" - end - - test "ensures cache is cleared for the object" do - object = insert(:note) - cached_object = Object.get_cached_by_ap_id(object.data["id"]) - - assert object == cached_object - - Cachex.put(:web_resp_cache, URI.parse(object.data["id"]).path, "cofe") - - Object.delete(cached_object) - - {:ok, nil} = Cachex.get(:object_cache, "object:#{object.data["id"]}") - {:ok, nil} = Cachex.get(:web_resp_cache, URI.parse(object.data["id"]).path) - - cached_object = Object.get_cached_by_ap_id(object.data["id"]) - - refute object == cached_object - - assert cached_object.data["type"] == "Tombstone" - end - end - - describe "delete attachments" do - setup do: clear_config([Pleroma.Upload]) - setup do: clear_config([:instance, :cleanup_attachments]) - - test "Disabled via config" do - Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) - Pleroma.Config.put([:instance, :cleanup_attachments], false) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - user = insert(:user) - - {:ok, %Object{} = attachment} = - Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) - - %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = - note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) - - uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) - - path = href |> Path.dirname() |> Path.basename() - - assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") - - Object.delete(note) - - ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) - - assert Object.get_by_id(note.id).data["deleted"] - refute Object.get_by_id(attachment.id) == nil - - assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") - end - - test "in subdirectories" do - Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) - Pleroma.Config.put([:instance, :cleanup_attachments], true) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - user = insert(:user) - - {:ok, %Object{} = attachment} = - Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) - - %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = - note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) - - uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) - - path = href |> Path.dirname() |> Path.basename() - - assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") - - Object.delete(note) - - ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) - - assert Object.get_by_id(note.id).data["deleted"] - assert Object.get_by_id(attachment.id) == nil - - assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") - end - - test "with dedupe enabled" do - Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) - Pleroma.Config.put([Pleroma.Upload, :filters], [Pleroma.Upload.Filter.Dedupe]) - Pleroma.Config.put([:instance, :cleanup_attachments], true) - - uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) - - File.mkdir_p!(uploads_dir) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - user = insert(:user) - - {:ok, %Object{} = attachment} = - Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) - - %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = - note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) - - filename = Path.basename(href) - - assert {:ok, files} = File.ls(uploads_dir) - assert filename in files - - Object.delete(note) - - ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) - - assert Object.get_by_id(note.id).data["deleted"] - assert Object.get_by_id(attachment.id) == nil - assert {:ok, files} = File.ls(uploads_dir) - refute filename in files - end - - test "with objects that have legacy data.url attribute" do - Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) - Pleroma.Config.put([:instance, :cleanup_attachments], true) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - user = insert(:user) - - {:ok, %Object{} = attachment} = - Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) - - {:ok, %Object{}} = Object.create(%{url: "https://google.com", actor: user.ap_id}) - - %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = - note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) - - uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) - - path = href |> Path.dirname() |> Path.basename() - - assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") - - Object.delete(note) - - ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) - - assert Object.get_by_id(note.id).data["deleted"] - assert Object.get_by_id(attachment.id) == nil - - assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") - end - - test "With custom base_url" do - Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) - Pleroma.Config.put([Pleroma.Upload, :base_url], "https://sub.domain.tld/dir/") - Pleroma.Config.put([:instance, :cleanup_attachments], true) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - user = insert(:user) - - {:ok, %Object{} = attachment} = - Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) - - %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = - note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) - - uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) - - path = href |> Path.dirname() |> Path.basename() - - assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") - - Object.delete(note) - - ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) - - assert Object.get_by_id(note.id).data["deleted"] - assert Object.get_by_id(attachment.id) == nil - - assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") - end - end - - describe "normalizer" do - test "fetches unknown objects by default" do - %Object{} = - object = Object.normalize("http://mastodon.example.org/@admin/99541947525187367") - - assert object.data["url"] == "http://mastodon.example.org/@admin/99541947525187367" - end - - test "fetches unknown objects when fetch_remote is explicitly true" do - %Object{} = - object = Object.normalize("http://mastodon.example.org/@admin/99541947525187367", true) - - assert object.data["url"] == "http://mastodon.example.org/@admin/99541947525187367" - end - - test "does not fetch unknown objects when fetch_remote is false" do - assert is_nil( - Object.normalize("http://mastodon.example.org/@admin/99541947525187367", false) - ) - end - end - - describe "get_by_id_and_maybe_refetch" do - setup do - mock(fn - %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_original.json")} - - env -> - apply(HttpRequestMock, :request, [env]) - end) - - mock_modified = fn resp -> - mock(fn - %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} -> - resp - - env -> - apply(HttpRequestMock, :request, [env]) - end) - end - - on_exit(fn -> mock(fn env -> apply(HttpRequestMock, :request, [env]) end) end) - - [mock_modified: mock_modified] - end - - test "refetches if the time since the last refetch is greater than the interval", %{ - mock_modified: mock_modified - } do - %Object{} = - object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") - - Object.set_cache(object) - - assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 - assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 - - mock_modified.(%Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/poll_modified.json") - }) - - updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1) - object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) - assert updated_object == object_in_cache - assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8 - assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3 - end - - test "returns the old object if refetch fails", %{mock_modified: mock_modified} do - %Object{} = - object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") - - Object.set_cache(object) - - assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 - assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 - - assert capture_log(fn -> - mock_modified.(%Tesla.Env{status: 404, body: ""}) - - updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1) - object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) - assert updated_object == object_in_cache - assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4 - assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0 - end) =~ - "[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d" - end - - test "does not refetch if the time since the last refetch is greater than the interval", %{ - mock_modified: mock_modified - } do - %Object{} = - object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") - - Object.set_cache(object) - - assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 - assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 - - mock_modified.(%Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/poll_modified.json") - }) - - updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100) - object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) - assert updated_object == object_in_cache - assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4 - assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0 - end - - test "preserves internal fields on refetch", %{mock_modified: mock_modified} do - %Object{} = - object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") - - Object.set_cache(object) - - assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 - assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 - - user = insert(:user) - activity = Activity.get_create_by_object_ap_id(object.data["id"]) - {:ok, activity} = CommonAPI.favorite(user, activity.id) - object = Object.get_by_ap_id(activity.data["object"]) - - assert object.data["like_count"] == 1 - - mock_modified.(%Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/poll_modified.json") - }) - - updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1) - object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) - assert updated_object == object_in_cache - assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8 - assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3 - - assert updated_object.data["like_count"] == 1 - end - end -end diff --git a/test/otp_version_test.exs b/test/otp_version_test.exs @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.OTPVersionTest do - use ExUnit.Case, async: true - - alias Pleroma.OTPVersion - - describe "check/1" do - test "22.4" do - assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.4"]) == - "22.4" - end - - test "22.1" do - assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.1"]) == - "22.1" - end - - test "21.1" do - assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/21.1"]) == - "21.1" - end - - test "23.0" do - assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/23.0"]) == - "23.0" - end - - test "with non existance file" do - assert OTPVersion.get_version_from_files([ - "test/fixtures/warnings/otp_version/non-exising", - "test/fixtures/warnings/otp_version/22.4" - ]) == "22.4" - end - - test "empty paths" do - assert OTPVersion.get_version_from_files([]) == nil - end - end -end diff --git a/test/pagination_test.exs b/test/pagination_test.exs @@ -1,92 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.PaginationTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Object - alias Pleroma.Pagination - - describe "keyset" do - setup do - notes = insert_list(5, :note) - - %{notes: notes} - end - - test "paginates by min_id", %{notes: notes} do - id = Enum.at(notes, 2).id |> Integer.to_string() - - %{total: total, items: paginated} = - Pagination.fetch_paginated(Object, %{min_id: id, total: true}) - - assert length(paginated) == 2 - assert total == 5 - end - - test "paginates by since_id", %{notes: notes} do - id = Enum.at(notes, 2).id |> Integer.to_string() - - %{total: total, items: paginated} = - Pagination.fetch_paginated(Object, %{since_id: id, total: true}) - - assert length(paginated) == 2 - assert total == 5 - end - - test "paginates by max_id", %{notes: notes} do - id = Enum.at(notes, 1).id |> Integer.to_string() - - %{total: total, items: paginated} = - Pagination.fetch_paginated(Object, %{max_id: id, total: true}) - - assert length(paginated) == 1 - assert total == 5 - end - - test "paginates by min_id & limit", %{notes: notes} do - id = Enum.at(notes, 2).id |> Integer.to_string() - - paginated = Pagination.fetch_paginated(Object, %{min_id: id, limit: 1}) - - assert length(paginated) == 1 - end - - test "handles id gracefully", %{notes: notes} do - id = Enum.at(notes, 1).id |> Integer.to_string() - - paginated = - Pagination.fetch_paginated(Object, %{ - id: "9s99Hq44Cnv8PKBwWG", - max_id: id, - limit: 20, - offset: 0 - }) - - assert length(paginated) == 1 - end - end - - describe "offset" do - setup do - notes = insert_list(5, :note) - - %{notes: notes} - end - - test "paginates by limit" do - paginated = Pagination.fetch_paginated(Object, %{limit: 2}, :offset) - - assert length(paginated) == 2 - end - - test "paginates by limit & offset" do - paginated = Pagination.fetch_paginated(Object, %{limit: 2, offset: 4}, :offset) - - assert length(paginated) == 1 - end - end -end diff --git a/test/activity/ir/topics_test.exs b/test/pleroma/activity/ir/topics_test.exs diff --git a/test/activity_test.exs b/test/pleroma/activity_test.exs diff --git a/test/pleroma/application_requirements_test.exs b/test/pleroma/application_requirements_test.exs @@ -0,0 +1,149 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ApplicationRequirementsTest do + use Pleroma.DataCase + + import ExUnit.CaptureLog + import Mock + + alias Pleroma.ApplicationRequirements + alias Pleroma.Config + alias Pleroma.Repo + + describe "check_welcome_message_config!/1" do + setup do: clear_config([:welcome]) + setup do: clear_config([Pleroma.Emails.Mailer]) + + test "raises if welcome email enabled but mail disabled" do + Pleroma.Config.put([:welcome, :email, :enabled], true) + Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) + + assert_raise Pleroma.ApplicationRequirements.VerifyError, "The mail disabled.", fn -> + capture_log(&Pleroma.ApplicationRequirements.verify!/0) + end + end + end + + describe "check_confirmation_accounts!" do + setup_with_mocks([ + {Pleroma.ApplicationRequirements, [:passthrough], + [ + check_migrations_applied!: fn _ -> :ok end + ]} + ]) do + :ok + end + + setup do: clear_config([:instance, :account_activation_required]) + + test "raises if account confirmation is required but mailer isn't enable" do + Pleroma.Config.put([:instance, :account_activation_required], true) + Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) + + assert_raise Pleroma.ApplicationRequirements.VerifyError, + "Account activation enabled, but Mailer is disabled. Cannot send confirmation emails.", + fn -> + capture_log(&Pleroma.ApplicationRequirements.verify!/0) + end + end + + test "doesn't do anything if account confirmation is disabled" do + Pleroma.Config.put([:instance, :account_activation_required], false) + Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) + assert Pleroma.ApplicationRequirements.verify!() == :ok + end + + test "doesn't do anything if account confirmation is required and mailer is enabled" do + Pleroma.Config.put([:instance, :account_activation_required], true) + Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], true) + assert Pleroma.ApplicationRequirements.verify!() == :ok + end + end + + describe "check_rum!" do + setup_with_mocks([ + {Pleroma.ApplicationRequirements, [:passthrough], + [check_migrations_applied!: fn _ -> :ok end]} + ]) do + :ok + end + + setup do: clear_config([:database, :rum_enabled]) + + test "raises if rum is enabled and detects unapplied rum migrations" do + Config.put([:database, :rum_enabled], true) + + with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> false end]}]) do + assert_raise ApplicationRequirements.VerifyError, + "Unapplied RUM Migrations detected", + fn -> + capture_log(&ApplicationRequirements.verify!/0) + end + end + end + + test "raises if rum is disabled and detects rum migrations" do + Config.put([:database, :rum_enabled], false) + + with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> true end]}]) do + assert_raise ApplicationRequirements.VerifyError, + "RUM Migrations detected", + fn -> + capture_log(&ApplicationRequirements.verify!/0) + end + end + end + + test "doesn't do anything if rum enabled and applied migrations" do + Config.put([:database, :rum_enabled], true) + + with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> true end]}]) do + assert ApplicationRequirements.verify!() == :ok + end + end + + test "doesn't do anything if rum disabled" do + Config.put([:database, :rum_enabled], false) + + with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> false end]}]) do + assert ApplicationRequirements.verify!() == :ok + end + end + end + + describe "check_migrations_applied!" do + setup_with_mocks([ + {Ecto.Migrator, [], + [ + with_repo: fn repo, fun -> passthrough([repo, fun]) end, + migrations: fn Repo -> + [ + {:up, 20_191_128_153_944, "fix_missing_following_count"}, + {:up, 20_191_203_043_610, "create_report_notes"}, + {:down, 20_191_220_174_645, "add_scopes_to_pleroma_feo_auth_records"} + ] + end + ]} + ]) do + :ok + end + + setup do: clear_config([:i_am_aware_this_may_cause_data_loss, :disable_migration_check]) + + test "raises if it detects unapplied migrations" do + assert_raise ApplicationRequirements.VerifyError, + "Unapplied Migrations detected", + fn -> + capture_log(&ApplicationRequirements.verify!/0) + end + end + + test "doesn't do anything if disabled" do + Config.put([:i_am_aware_this_may_cause_data_loss, :disable_migration_check], true) + + assert :ok == ApplicationRequirements.verify!() + end + end +end diff --git a/test/bbs/handler_test.exs b/test/pleroma/bbs/handler_test.exs diff --git a/test/bookmark_test.exs b/test/pleroma/bookmark_test.exs diff --git a/test/captcha_test.exs b/test/pleroma/captcha_test.exs diff --git a/test/chat/message_reference_test.exs b/test/pleroma/chat/message_reference_test.exs diff --git a/test/chat_test.exs b/test/pleroma/chat_test.exs diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs @@ -0,0 +1,140 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Config.DeprecationWarningsTest do + use ExUnit.Case + use Pleroma.Tests.Helpers + + import ExUnit.CaptureLog + + alias Pleroma.Config + alias Pleroma.Config.DeprecationWarnings + + test "check_old_mrf_config/0" do + clear_config([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.NoOpPolicy) + clear_config([:instance, :mrf_transparency], true) + clear_config([:instance, :mrf_transparency_exclusions], []) + + assert capture_log(fn -> DeprecationWarnings.check_old_mrf_config() end) =~ + """ + !!!DEPRECATION WARNING!!! + Your config is using old namespaces for MRF configuration. They should work for now, but you are advised to change to new namespaces to prevent possible issues later: + + * `config :pleroma, :instance, rewrite_policy` is now `config :pleroma, :mrf, policies` + * `config :pleroma, :instance, mrf_transparency` is now `config :pleroma, :mrf, transparency` + * `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions` + """ + end + + test "move_namespace_and_warn/2" do + old_group1 = [:group, :key] + old_group2 = [:group, :key2] + old_group3 = [:group, :key3] + + new_group1 = [:another_group, :key4] + new_group2 = [:another_group, :key5] + new_group3 = [:another_group, :key6] + + clear_config(old_group1, 1) + clear_config(old_group2, 2) + clear_config(old_group3, 3) + + clear_config(new_group1) + clear_config(new_group2) + clear_config(new_group3) + + config_map = [ + {old_group1, new_group1, "\n error :key"}, + {old_group2, new_group2, "\n error :key2"}, + {old_group3, new_group3, "\n error :key3"} + ] + + assert capture_log(fn -> + DeprecationWarnings.move_namespace_and_warn( + config_map, + "Warning preface" + ) + end) =~ "Warning preface\n error :key\n error :key2\n error :key3" + + assert Config.get(new_group1) == 1 + assert Config.get(new_group2) == 2 + assert Config.get(new_group3) == 3 + end + + test "check_media_proxy_whitelist_config/0" do + clear_config([:media_proxy, :whitelist], ["https://example.com", "example2.com"]) + + assert capture_log(fn -> + DeprecationWarnings.check_media_proxy_whitelist_config() + end) =~ "Your config is using old format (only domain) for MediaProxy whitelist option" + end + + test "check_welcome_message_config/0" do + clear_config([:instance, :welcome_user_nickname], "LainChan") + + assert capture_log(fn -> + DeprecationWarnings.check_welcome_message_config() + end) =~ "Your config is using the old namespace for Welcome messages configuration." + end + + test "check_hellthread_threshold/0" do + clear_config([:mrf_hellthread, :threshold], 16) + + assert capture_log(fn -> + DeprecationWarnings.check_hellthread_threshold() + end) =~ "You are using the old configuration mechanism for the hellthread filter." + end + + test "check_activity_expiration_config/0" do + clear_config(Pleroma.ActivityExpiration, enabled: true) + + assert capture_log(fn -> + DeprecationWarnings.check_activity_expiration_config() + end) =~ "Your config is using old namespace for activity expiration configuration." + end + + describe "check_gun_pool_options/0" do + test "await_up_timeout" do + config = Config.get(:connections_pool) + clear_config(:connections_pool, Keyword.put(config, :await_up_timeout, 5_000)) + + assert capture_log(fn -> + DeprecationWarnings.check_gun_pool_options() + end) =~ + "Your config is using old setting `config :pleroma, :connections_pool, await_up_timeout`." + end + + test "pool timeout" do + old_config = [ + federation: [ + size: 50, + max_waiting: 10, + timeout: 10_000 + ], + media: [ + size: 50, + max_waiting: 10, + timeout: 10_000 + ], + upload: [ + size: 25, + max_waiting: 5, + timeout: 15_000 + ], + default: [ + size: 10, + max_waiting: 2, + timeout: 5_000 + ] + ] + + clear_config(:pools, old_config) + + assert capture_log(fn -> + DeprecationWarnings.check_gun_pool_options() + end) =~ + "Your config is using old setting name `timeout` instead of `recv_timeout` in pool settings" + end + end +end diff --git a/test/config/holder_test.exs b/test/pleroma/config/holder_test.exs diff --git a/test/config/loader_test.exs b/test/pleroma/config/loader_test.exs diff --git a/test/config/transfer_task_test.exs b/test/pleroma/config/transfer_task_test.exs diff --git a/test/config/config_db_test.exs b/test/pleroma/config_db_test.exs diff --git a/test/config_test.exs b/test/pleroma/config_test.exs diff --git a/test/conversation/participation_test.exs b/test/pleroma/conversation/participation_test.exs diff --git a/test/conversation_test.exs b/test/pleroma/conversation_test.exs diff --git a/test/docs/generator_test.exs b/test/pleroma/docs/generator_test.exs diff --git a/test/earmark_renderer_test.exs b/test/pleroma/earmark_renderer_test.exs diff --git a/test/pleroma/ecto_type/activity_pub/object_validators/date_time_test.exs b/test/pleroma/ecto_type/activity_pub/object_validators/date_time_test.exs @@ -0,0 +1,36 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.DateTimeTest do + alias Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime + use Pleroma.DataCase + + test "it validates an xsd:Datetime" do + valid_strings = [ + "2004-04-12T13:20:00", + "2004-04-12T13:20:15.5", + "2004-04-12T13:20:00-05:00", + "2004-04-12T13:20:00Z" + ] + + invalid_strings = [ + "2004-04-12T13:00", + "2004-04-1213:20:00", + "99-04-12T13:00", + "2004-04-12" + ] + + assert {:ok, "2004-04-01T12:00:00Z"} == DateTime.cast("2004-04-01T12:00:00Z") + + Enum.each(valid_strings, fn date_time -> + result = DateTime.cast(date_time) + assert {:ok, _} = result + end) + + Enum.each(invalid_strings, fn date_time -> + result = DateTime.cast(date_time) + assert :error == result + end) + end +end diff --git a/test/pleroma/ecto_type/activity_pub/object_validators/object_id_test.exs b/test/pleroma/ecto_type/activity_pub/object_validators/object_id_test.exs @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectIDTest do + alias Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID + use Pleroma.DataCase + + @uris [ + "http://lain.com/users/lain", + "http://lain.com", + "https://lain.com/object/1" + ] + + @non_uris [ + "https://", + "rin", + 1, + :x, + %{"1" => 2} + ] + + test "it accepts http uris" do + Enum.each(@uris, fn uri -> + assert {:ok, uri} == ObjectID.cast(uri) + end) + end + + test "it accepts an object with a nested uri id" do + Enum.each(@uris, fn uri -> + assert {:ok, uri} == ObjectID.cast(%{"id" => uri}) + end) + end + + test "it rejects non-uri strings" do + Enum.each(@non_uris, fn non_uri -> + assert :error == ObjectID.cast(non_uri) + assert :error == ObjectID.cast(%{"id" => non_uri}) + end) + end +end diff --git a/test/pleroma/ecto_type/activity_pub/object_validators/recipients_test.exs b/test/pleroma/ecto_type/activity_pub/object_validators/recipients_test.exs @@ -0,0 +1,31 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.RecipientsTest do + alias Pleroma.EctoType.ActivityPub.ObjectValidators.Recipients + use Pleroma.DataCase + + test "it asserts that all elements of the list are object ids" do + list = ["https://lain.com/users/lain", "invalid"] + + assert :error == Recipients.cast(list) + end + + test "it works with a list" do + list = ["https://lain.com/users/lain"] + assert {:ok, list} == Recipients.cast(list) + end + + test "it works with a list with whole objects" do + list = ["https://lain.com/users/lain", %{"id" => "https://gensokyo.2hu/users/raymoo"}] + resulting_list = ["https://gensokyo.2hu/users/raymoo", "https://lain.com/users/lain"] + assert {:ok, resulting_list} == Recipients.cast(list) + end + + test "it turns a single string into a list" do + recipient = "https://lain.com/users/lain" + + assert {:ok, [recipient]} == Recipients.cast(recipient) + end +end diff --git a/test/pleroma/ecto_type/activity_pub/object_validators/safe_text_test.exs b/test/pleroma/ecto_type/activity_pub/object_validators/safe_text_test.exs @@ -0,0 +1,30 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.SafeTextTest do + use Pleroma.DataCase + + alias Pleroma.EctoType.ActivityPub.ObjectValidators.SafeText + + test "it lets normal text go through" do + text = "hey how are you" + assert {:ok, text} == SafeText.cast(text) + end + + test "it removes html tags from text" do + text = "hey look xss <script>alert('foo')</script>" + assert {:ok, "hey look xss alert(&#39;foo&#39;)"} == SafeText.cast(text) + end + + test "it keeps basic html tags" do + text = "hey <a href='http://gensokyo.2hu'>look</a> xss <script>alert('foo')</script>" + + assert {:ok, "hey <a href=\"http://gensokyo.2hu\">look</a> xss alert(&#39;foo&#39;)"} == + SafeText.cast(text) + end + + test "errors for non-text" do + assert :error == SafeText.cast(1) + end +end diff --git a/test/emails/admin_email_test.exs b/test/pleroma/emails/admin_email_test.exs diff --git a/test/emails/mailer_test.exs b/test/pleroma/emails/mailer_test.exs diff --git a/test/emails/user_email_test.exs b/test/pleroma/emails/user_email_test.exs diff --git a/test/emoji/formatter_test.exs b/test/pleroma/emoji/formatter_test.exs diff --git a/test/emoji/loader_test.exs b/test/pleroma/emoji/loader_test.exs diff --git a/test/emoji/pack_test.exs b/test/pleroma/emoji/pack_test.exs diff --git a/test/emoji_test.exs b/test/pleroma/emoji_test.exs diff --git a/test/filter_test.exs b/test/pleroma/filter_test.exs diff --git a/test/following_relationship_test.exs b/test/pleroma/following_relationship_test.exs diff --git a/test/formatter_test.exs b/test/pleroma/formatter_test.exs diff --git a/test/gun/conneciton_pool_test.exs b/test/pleroma/gun/connection_pool_test.exs diff --git a/test/healthcheck_test.exs b/test/pleroma/healthcheck_test.exs diff --git a/test/html_test.exs b/test/pleroma/html_test.exs diff --git a/test/http/adapter_helper/gun_test.exs b/test/pleroma/http/adapter_helper/gun_test.exs diff --git a/test/http/adapter_helper/hackney_test.exs b/test/pleroma/http/adapter_helper/hackney_test.exs diff --git a/test/http/adapter_helper_test.exs b/test/pleroma/http/adapter_helper_test.exs diff --git a/test/http/ex_aws_test.exs b/test/pleroma/http/ex_aws_test.exs diff --git a/test/http/request_builder_test.exs b/test/pleroma/http/request_builder_test.exs diff --git a/test/http/tzdata_test.exs b/test/pleroma/http/tzdata_test.exs diff --git a/test/http_test.exs b/test/pleroma/http_test.exs diff --git a/test/pleroma/instances/instance_test.exs b/test/pleroma/instances/instance_test.exs @@ -0,0 +1,152 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Instances.InstanceTest do + alias Pleroma.Instances.Instance + alias Pleroma.Repo + + use Pleroma.DataCase + + import ExUnit.CaptureLog + import Pleroma.Factory + + setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1) + + describe "set_reachable/1" do + test "clears `unreachable_since` of existing matching Instance record having non-nil `unreachable_since`" do + unreachable_since = NaiveDateTime.to_iso8601(NaiveDateTime.utc_now()) + instance = insert(:instance, unreachable_since: unreachable_since) + + assert {:ok, instance} = Instance.set_reachable(instance.host) + refute instance.unreachable_since + end + + test "keeps nil `unreachable_since` of existing matching Instance record having nil `unreachable_since`" do + instance = insert(:instance, unreachable_since: nil) + + 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 + test "creates new record having `unreachable_since` to current time if record does not exist" do + assert {:ok, instance} = Instance.set_unreachable("https://domain.com/path") + + instance = Repo.get(Instance, instance.id) + assert instance.unreachable_since + assert "domain.com" == instance.host + end + + test "sets `unreachable_since` of existing record having nil `unreachable_since`" do + instance = insert(:instance, unreachable_since: nil) + refute instance.unreachable_since + + assert {:ok, _} = Instance.set_unreachable(instance.host) + + instance = Repo.get(Instance, instance.id) + assert instance.unreachable_since + end + + test "does NOT modify `unreachable_since` value of existing record in case it's present" do + instance = + insert(:instance, unreachable_since: NaiveDateTime.add(NaiveDateTime.utc_now(), -10)) + + assert instance.unreachable_since + initial_value = instance.unreachable_since + + assert {:ok, _} = Instance.set_unreachable(instance.host) + + instance = Repo.get(Instance, instance.id) + assert initial_value == instance.unreachable_since + end + end + + describe "set_unreachable/2" do + test "sets `unreachable_since` value of existing record in case it's newer than supplied value" do + instance = + insert(:instance, unreachable_since: NaiveDateTime.add(NaiveDateTime.utc_now(), -10)) + + assert instance.unreachable_since + + past_value = NaiveDateTime.add(NaiveDateTime.utc_now(), -100) + assert {:ok, _} = Instance.set_unreachable(instance.host, past_value) + + instance = Repo.get(Instance, instance.id) + assert past_value == instance.unreachable_since + end + + test "does NOT modify `unreachable_since` value of existing record in case it's equal to or older than supplied value" do + instance = + insert(:instance, unreachable_since: NaiveDateTime.add(NaiveDateTime.utc_now(), -10)) + + assert instance.unreachable_since + initial_value = instance.unreachable_since + + assert {:ok, _} = Instance.set_unreachable(instance.host, NaiveDateTime.utc_now()) + + instance = Repo.get(Instance, instance.id) + assert initial_value == instance.unreachable_since + end + end + + describe "get_or_update_favicon/1" do + test "Scrapes favicon URLs" do + Tesla.Mock.mock(fn %{url: "https://favicon.example.org/"} -> + %Tesla.Env{ + status: 200, + body: ~s[<html><head><link rel="icon" href="/favicon.png"></head></html>] + } + end) + + assert "https://favicon.example.org/favicon.png" == + Instance.get_or_update_favicon(URI.parse("https://favicon.example.org/")) + end + + test "Returns nil on too long favicon URLs" do + long_favicon_url = + "https://Lorem.ipsum.dolor.sit.amet/consecteturadipiscingelit/Praesentpharetrapurusutaliquamtempus/Mauriseulaoreetarcu/atfacilisisorci/Nullamporttitor/nequesedfeugiatmollis/dolormagnaefficiturlorem/nonpretiumsapienorcieurisus/Nullamveleratsem/Maecenassedaccumsanexnam/favicon.png" + + Tesla.Mock.mock(fn %{url: "https://long-favicon.example.org/"} -> + %Tesla.Env{ + status: 200, + body: + ~s[<html><head><link rel="icon" href="] <> long_favicon_url <> ~s["></head></html>] + } + end) + + assert capture_log(fn -> + assert nil == + Instance.get_or_update_favicon( + URI.parse("https://long-favicon.example.org/") + ) + end) =~ + "Instance.get_or_update_favicon(\"long-favicon.example.org\") error: %Postgrex.Error{" + end + + test "Handles not getting a favicon URL properly" do + Tesla.Mock.mock(fn %{url: "https://no-favicon.example.org/"} -> + %Tesla.Env{ + status: 200, + body: ~s[<html><head><h1>I wil look down and whisper "GNO.."</h1></head></html>] + } + end) + + refute capture_log(fn -> + assert nil == + Instance.get_or_update_favicon( + URI.parse("https://no-favicon.example.org/") + ) + end) =~ "Instance.scrape_favicon(\"https://no-favicon.example.org/\") error: " + end + end +end diff --git a/test/pleroma/instances_test.exs b/test/pleroma/instances_test.exs @@ -0,0 +1,124 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.InstancesTest do + alias Pleroma.Instances + + use Pleroma.DataCase + + setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1) + + describe "reachable?/1" do + test "returns `true` for host / url with unknown reachability status" do + assert Instances.reachable?("unknown.site") + assert Instances.reachable?("http://unknown.site") + end + + test "returns `false` for host / url marked unreachable for at least `reachability_datetime_threshold()`" do + host = "consistently-unreachable.name" + Instances.set_consistently_unreachable(host) + + refute Instances.reachable?(host) + refute Instances.reachable?("http://#{host}/path") + end + + test "returns `true` for host / url marked unreachable for less than `reachability_datetime_threshold()`" do + url = "http://eventually-unreachable.name/path" + + Instances.set_unreachable(url) + + assert Instances.reachable?(url) + assert Instances.reachable?(URI.parse(url).host) + end + + test "returns true on non-binary input" do + assert Instances.reachable?(nil) + assert Instances.reachable?(1) + end + end + + describe "filter_reachable/1" do + setup do + host = "consistently-unreachable.name" + url1 = "http://eventually-unreachable.com/path" + url2 = "http://domain.com/path" + + Instances.set_consistently_unreachable(host) + Instances.set_unreachable(url1) + + result = Instances.filter_reachable([host, url1, url2, nil]) + %{result: result, url1: url1, url2: url2} + end + + test "returns a map with keys containing 'not marked consistently unreachable' elements of supplied list", + %{result: result, url1: url1, url2: url2} do + assert is_map(result) + assert Enum.sort([url1, url2]) == result |> Map.keys() |> Enum.sort() + end + + test "returns a map with `unreachable_since` values for keys", + %{result: result, url1: url1, url2: url2} do + assert is_map(result) + assert %NaiveDateTime{} = result[url1] + assert is_nil(result[url2]) + end + + test "returns an empty map for empty list or list containing no hosts / url" do + assert %{} == Instances.filter_reachable([]) + assert %{} == Instances.filter_reachable([nil]) + end + end + + describe "set_reachable/1" do + test "sets unreachable url or host reachable" do + host = "domain.com" + Instances.set_consistently_unreachable(host) + refute Instances.reachable?(host) + + Instances.set_reachable(host) + assert Instances.reachable?(host) + end + + test "keeps reachable url or host reachable" do + url = "https://site.name?q=" + assert Instances.reachable?(url) + + Instances.set_reachable(url) + assert Instances.reachable?(url) + end + + test "returns error status on non-binary input" do + assert {:error, _} = Instances.set_reachable(nil) + assert {:error, _} = Instances.set_reachable(1) + end + end + + # Note: implementation-specific (e.g. Instance) details of set_unreachable/1 + # should be tested in implementation-specific tests + describe "set_unreachable/1" do + test "returns error status on non-binary input" do + assert {:error, _} = Instances.set_unreachable(nil) + assert {:error, _} = Instances.set_unreachable(1) + end + end + + describe "set_consistently_unreachable/1" do + test "sets reachable url or host unreachable" do + url = "http://domain.com?q=" + assert Instances.reachable?(url) + + Instances.set_consistently_unreachable(url) + refute Instances.reachable?(url) + end + + test "keeps unreachable url or host unreachable" do + host = "site.name" + Instances.set_consistently_unreachable(host) + refute Instances.reachable?(host) + + Instances.set_consistently_unreachable(host) + refute Instances.reachable?(host) + end + end +end diff --git a/test/federation/federation_test.exs b/test/pleroma/integration/federation_test.exs diff --git a/test/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs diff --git a/test/job_queue_monitor_test.exs b/test/pleroma/job_queue_monitor_test.exs diff --git a/test/keys_test.exs b/test/pleroma/keys_test.exs diff --git a/test/list_test.exs b/test/pleroma/list_test.exs diff --git a/test/marker_test.exs b/test/pleroma/marker_test.exs diff --git a/test/pleroma/mfa/backup_codes_test.exs b/test/pleroma/mfa/backup_codes_test.exs @@ -0,0 +1,15 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MFA.BackupCodesTest do + use Pleroma.DataCase + + alias Pleroma.MFA.BackupCodes + + test "generate backup codes" do + codes = BackupCodes.generate(number_of_codes: 2, length: 4) + + assert [<<_::bytes-size(4)>>, <<_::bytes-size(4)>>] = codes + end +end diff --git a/test/pleroma/mfa/totp_test.exs b/test/pleroma/mfa/totp_test.exs @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MFA.TOTPTest do + use Pleroma.DataCase + + alias Pleroma.MFA.TOTP + + test "create provisioning_uri to generate qrcode" do + uri = + TOTP.provisioning_uri("test-secrcet", "test@example.com", + issuer: "Plerome-42", + digits: 8, + period: 60 + ) + + assert uri == + "otpauth://totp/test@example.com?digits=8&issuer=Plerome-42&period=60&secret=test-secrcet" + end +end diff --git a/test/pleroma/mfa_test.exs b/test/pleroma/mfa_test.exs @@ -0,0 +1,52 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MFATest do + use Pleroma.DataCase + + import Pleroma.Factory + alias Pleroma.MFA + + describe "mfa_settings" do + test "returns settings user's" do + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: "xx", confirmed: true} + } + ) + + settings = MFA.mfa_settings(user) + assert match?(^settings, %{enabled: true, totp: true}) + end + end + + describe "generate backup codes" do + test "returns backup codes" do + user = insert(:user) + + {:ok, [code1, code2]} = MFA.generate_backup_codes(user) + updated_user = refresh_record(user) + [hash1, hash2] = updated_user.multi_factor_authentication_settings.backup_codes + assert Pbkdf2.verify_pass(code1, hash1) + assert Pbkdf2.verify_pass(code2, hash2) + end + end + + describe "invalidate_backup_code" do + test "invalid used code" do + user = insert(:user) + + {:ok, _} = MFA.generate_backup_codes(user) + user = refresh_record(user) + assert length(user.multi_factor_authentication_settings.backup_codes) == 2 + [hash_code | _] = user.multi_factor_authentication_settings.backup_codes + + {:ok, user} = MFA.invalidate_backup_code(user, hash_code) + + assert length(user.multi_factor_authentication_settings.backup_codes) == 1 + end + end +end diff --git a/test/pleroma/migration_helper/notification_backfill_test.exs b/test/pleroma/migration_helper/notification_backfill_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MigrationHelper.NotificationBackfillTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.MigrationHelper.NotificationBackfill + alias Pleroma.Notification + alias Pleroma.Repo + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "fill_in_notification_types" do + test "it fills in missing notification types" do + user = insert(:user) + other_user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "yeah, @#{other_user.nickname}"}) + {:ok, chat} = CommonAPI.post_chat_message(user, other_user, "yo") + {:ok, react} = CommonAPI.react_with_emoji(post.id, other_user, "☕") + {:ok, like} = CommonAPI.favorite(other_user, post.id) + {:ok, react_2} = CommonAPI.react_with_emoji(post.id, other_user, "☕") + + data = + react_2.data + |> Map.put("type", "EmojiReaction") + + {:ok, react_2} = + react_2 + |> Activity.change(%{data: data}) + |> Repo.update() + + assert {5, nil} = Repo.update_all(Notification, set: [type: nil]) + + NotificationBackfill.fill_in_notification_types() + + assert %{type: "mention"} = + Repo.get_by(Notification, user_id: other_user.id, activity_id: post.id) + + assert %{type: "favourite"} = + Repo.get_by(Notification, user_id: user.id, activity_id: like.id) + + assert %{type: "pleroma:emoji_reaction"} = + Repo.get_by(Notification, user_id: user.id, activity_id: react.id) + + assert %{type: "pleroma:emoji_reaction"} = + Repo.get_by(Notification, user_id: user.id, activity_id: react_2.id) + + assert %{type: "pleroma:chat_mention"} = + Repo.get_by(Notification, user_id: other_user.id, activity_id: chat.id) + end + end +end diff --git a/test/pleroma/moderation_log_test.exs b/test/pleroma/moderation_log_test.exs @@ -0,0 +1,297 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ModerationLogTest do + alias Pleroma.Activity + alias Pleroma.ModerationLog + + use Pleroma.DataCase + + import Pleroma.Factory + + describe "user moderation" do + setup do + admin = insert(:user, is_admin: true) + moderator = insert(:user, is_moderator: true) + subject1 = insert(:user) + subject2 = insert(:user) + + [admin: admin, moderator: moderator, subject1: subject1, subject2: subject2] + end + + test "logging user deletion by moderator", %{moderator: moderator, subject1: subject1} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + subject: [subject1], + action: "delete" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == "@#{moderator.nickname} deleted users: @#{subject1.nickname}" + end + + test "logging user creation by moderator", %{ + moderator: moderator, + subject1: subject1, + subject2: subject2 + } do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + subjects: [subject1, subject2], + action: "create" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} created users: @#{subject1.nickname}, @#{subject2.nickname}" + end + + test "logging user follow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: admin, + followed: subject1, + follower: subject2, + action: "follow" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{admin.nickname} made @#{subject2.nickname} follow @#{subject1.nickname}" + end + + test "logging user unfollow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: admin, + followed: subject1, + follower: subject2, + action: "unfollow" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{admin.nickname} made @#{subject2.nickname} unfollow @#{subject1.nickname}" + end + + test "logging user tagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: admin, + nicknames: [subject1.nickname, subject2.nickname], + tags: ["foo", "bar"], + action: "tag" + }) + + log = Repo.one(ModerationLog) + + users = + [subject1.nickname, subject2.nickname] + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags = ["foo", "bar"] |> Enum.join(", ") + + assert log.data["message"] == "@#{admin.nickname} added tags: #{tags} to users: #{users}" + end + + test "logging user untagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: admin, + nicknames: [subject1.nickname, subject2.nickname], + tags: ["foo", "bar"], + action: "untag" + }) + + log = Repo.one(ModerationLog) + + users = + [subject1.nickname, subject2.nickname] + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags = ["foo", "bar"] |> Enum.join(", ") + + assert log.data["message"] == + "@#{admin.nickname} removed tags: #{tags} from users: #{users}" + end + + test "logging user grant by moderator", %{moderator: moderator, subject1: subject1} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + subject: [subject1], + action: "grant", + permission: "moderator" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == "@#{moderator.nickname} made @#{subject1.nickname} moderator" + end + + test "logging user revoke by moderator", %{moderator: moderator, subject1: subject1} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + subject: [subject1], + action: "revoke", + permission: "moderator" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} revoked moderator role from @#{subject1.nickname}" + end + + test "logging relay follow", %{moderator: moderator} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "relay_follow", + target: "https://example.org/relay" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} followed relay: https://example.org/relay" + end + + test "logging relay unfollow", %{moderator: moderator} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "relay_unfollow", + target: "https://example.org/relay" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} unfollowed relay: https://example.org/relay" + end + + test "logging report update", %{moderator: moderator} do + report = %Activity{ + id: "9m9I1F4p8ftrTP6QTI", + data: %{ + "type" => "Flag", + "state" => "resolved" + } + } + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "report_update", + subject: report + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} updated report ##{report.id} with 'resolved' state" + end + + test "logging report response", %{moderator: moderator} do + report = %Activity{ + id: "9m9I1F4p8ftrTP6QTI", + data: %{ + "type" => "Note" + } + } + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "report_note", + subject: report, + text: "look at this" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} added note 'look at this' to report ##{report.id}" + end + + test "logging status sensitivity update", %{moderator: moderator} do + note = insert(:note_activity) + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "status_update", + subject: note, + sensitive: "true", + visibility: nil + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true'" + end + + test "logging status visibility update", %{moderator: moderator} do + note = insert(:note_activity) + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "status_update", + subject: note, + sensitive: nil, + visibility: "private" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} updated status ##{note.id}, set visibility: 'private'" + end + + test "logging status sensitivity & visibility update", %{moderator: moderator} do + note = insert(:note_activity) + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "status_update", + subject: note, + sensitive: "true", + visibility: "private" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true', visibility: 'private'" + end + + test "logging status deletion", %{moderator: moderator} do + note = insert(:note_activity) + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "status_delete", + subject_id: note.id + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == "@#{moderator.nickname} deleted status ##{note.id}" + end + end +end diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs @@ -0,0 +1,1144 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.NotificationTest do + use Pleroma.DataCase + + import Pleroma.Factory + import Mock + + alias Pleroma.FollowingRelationship + alias Pleroma.Notification + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.NotificationView + alias Pleroma.Web.Push + alias Pleroma.Web.Streamer + + describe "create_notifications" do + test "never returns nil" do + user = insert(:user) + other_user = insert(:user, %{invisible: true}) + + {:ok, activity} = CommonAPI.post(user, %{status: "yeah"}) + {:ok, activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + + refute {:ok, [nil]} == Notification.create_notifications(activity) + end + + test "creates a notification for an emoji reaction" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "yeah"}) + {:ok, activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + + {:ok, [notification]} = Notification.create_notifications(activity) + + assert notification.user_id == user.id + assert notification.type == "pleroma:emoji_reaction" + end + + test "notifies someone when they are directly addressed" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname} and @#{third_user.nickname}" + }) + + {:ok, [notification, other_notification]} = Notification.create_notifications(activity) + + notified_ids = Enum.sort([notification.user_id, other_notification.user_id]) + assert notified_ids == [other_user.id, third_user.id] + assert notification.activity_id == activity.id + assert notification.type == "mention" + assert other_notification.activity_id == activity.id + + assert [%Pleroma.Marker{unread_count: 2}] = + Pleroma.Marker.get_markers(other_user, ["notifications"]) + end + + test "it creates a notification for subscribed users" do + user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, user) + + {:ok, status} = CommonAPI.post(user, %{status: "Akariiiin"}) + {:ok, [notification]} = Notification.create_notifications(status) + + assert notification.user_id == subscriber.id + end + + test "does not create a notification for subscribed users if status is a reply" do + user = insert(:user) + other_user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, other_user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) + + {:ok, _reply_activity} = + CommonAPI.post(other_user, %{ + status: "test reply", + in_reply_to_status_id: activity.id + }) + + user_notifications = Notification.for_user(user) + assert length(user_notifications) == 1 + + subscriber_notifications = Notification.for_user(subscriber) + assert Enum.empty?(subscriber_notifications) + end + end + + describe "CommonApi.post/2 notification-related functionality" do + test_with_mock "creates but does NOT send notification to blocker user", + Push, + [:passthrough], + [] do + user = insert(:user) + blocker = insert(:user) + {:ok, _user_relationship} = User.block(blocker, user) + + {:ok, _activity} = CommonAPI.post(user, %{status: "hey @#{blocker.nickname}!"}) + + blocker_id = blocker.id + assert [%Notification{user_id: ^blocker_id}] = Repo.all(Notification) + refute called(Push.send(:_)) + end + + 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, _activity} = CommonAPI.post(user, %{status: "hey @#{muter.nickname}!"}) + + muter_id = muter.id + assert [%Notification{user_id: ^muter_id}] = Repo.all(Notification) + refute called(Push.send(:_)) + end + + test_with_mock "creates but does NOT send notification to thread-muter user", + Push, + [:passthrough], + [] 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) + + {:ok, _same_context_activity} = + CommonAPI.post(user, %{ + status: "hey-hey-hey @#{thread_muter.nickname}!", + in_reply_to_status_id: activity.id + }) + + [pre_mute_notification, post_mute_notification] = + Repo.all(from(n in Notification, where: n.user_id == ^thread_muter.id, order_by: n.id)) + + pre_mute_notification_id = pre_mute_notification.id + post_mute_notification_id = post_mute_notification.id + + 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) + ) + ) + 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, false) + + {:ok, activity} = CommonAPI.post(muted, %{status: "Hi @#{muter.nickname}"}) + + assert Notification.create_notification(activity, muter) + end + + 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) + + {:ok, activity} = + CommonAPI.post(other_user, %{ + status: "Hi @#{muter.nickname}", + in_reply_to_status_id: activity.id + }) + + notification = Notification.create_notification(activity, muter) + + assert notification.id + assert notification.seen + end + + test "it disables notifications from strangers" do + follower = insert(:user) + + followed = + insert(:user, + notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true} + ) + + {:ok, activity} = CommonAPI.post(follower, %{status: "hey @#{followed.nickname}"}) + refute Notification.create_notification(activity, followed) + 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"]) + + refute Notification.create_notification(activity, author) + end + + test "it doesn't create duplicate notifications for follow+subscribed users" do + user = insert(:user) + subscriber = insert(:user) + + {:ok, _, _, _} = CommonAPI.follow(subscriber, user) + User.subscribe(subscriber, user) + {:ok, status} = CommonAPI.post(user, %{status: "Akariiiin"}) + {:ok, [_notif]} = Notification.create_notifications(status) + end + + test "it doesn't create subscription notifications if the recipient cannot see the status" do + user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, user) + + {:ok, status} = CommonAPI.post(user, %{status: "inwisible", visibility: "direct"}) + + assert {:ok, []} == Notification.create_notifications(status) + end + + test "it disables notifications from people who are invisible" do + author = insert(:user, invisible: true) + user = insert(:user) + + {:ok, status} = CommonAPI.post(author, %{status: "hey @#{user.nickname}"}) + refute Notification.create_notification(status, user) + end + + test "it doesn't create notifications if content matches with an irreversible filter" do + user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, user) + insert(:filter, user: subscriber, phrase: "cofe", hide: true) + + {:ok, status} = CommonAPI.post(user, %{status: "got cofe?"}) + + assert {:ok, []} == Notification.create_notifications(status) + end + + test "it creates notifications if content matches with a not irreversible filter" do + user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, user) + insert(:filter, user: subscriber, phrase: "cofe", hide: false) + + {:ok, status} = CommonAPI.post(user, %{status: "got cofe?"}) + {:ok, [notification]} = Notification.create_notifications(status) + + assert notification + refute notification.seen + end + + test "it creates notifications when someone likes user's status with a filtered word" do + user = insert(:user) + other_user = insert(:user) + insert(:filter, user: user, phrase: "tesla", hide: true) + + {:ok, activity_one} = CommonAPI.post(user, %{status: "wow tesla"}) + {:ok, activity_two} = CommonAPI.favorite(other_user, activity_one.id) + + {:ok, [notification]} = Notification.create_notifications(activity_two) + + assert notification + refute notification.seen + end + end + + describe "follow / follow_request notifications" do + test "it creates `follow` notification for approved Follow activity" do + user = insert(:user) + followed_user = insert(:user, locked: false) + + {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) + assert FollowingRelationship.following?(user, followed_user) + assert [notification] = Notification.for_user(followed_user) + + assert %{type: "follow"} = + NotificationView.render("show.json", %{ + notification: notification, + for: followed_user + }) + end + + test "it creates `follow_request` notification for pending Follow activity" do + user = insert(:user) + followed_user = insert(:user, locked: true) + + {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) + refute FollowingRelationship.following?(user, followed_user) + assert [notification] = Notification.for_user(followed_user) + + render_opts = %{notification: notification, for: followed_user} + assert %{type: "follow_request"} = NotificationView.render("show.json", render_opts) + + # After request is accepted, the same notification is rendered with type "follow": + assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user) + + notification = + Repo.get(Notification, notification.id) + |> Repo.preload(:activity) + + assert %{type: "follow"} = + NotificationView.render("show.json", notification: notification, for: followed_user) + end + + test "it doesn't create a notification for follow-unfollow-follow chains" do + user = insert(:user) + followed_user = insert(:user, locked: false) + + {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) + assert FollowingRelationship.following?(user, followed_user) + assert [notification] = Notification.for_user(followed_user) + + CommonAPI.unfollow(user, followed_user) + {:ok, _, _, _activity_dupe} = CommonAPI.follow(user, followed_user) + + notification_id = notification.id + assert [%{id: ^notification_id}] = Notification.for_user(followed_user) + end + + test "dismisses the notification on follow request rejection" do + user = insert(:user, locked: true) + follower = insert(:user) + {:ok, _, _, _follow_activity} = CommonAPI.follow(follower, user) + assert [notification] = Notification.for_user(user) + {:ok, _follower} = CommonAPI.reject_follow_request(follower, user) + assert [] = Notification.for_user(user) + end + end + + describe "get notification" do + test "it gets a notification that belongs to the user" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"}) + + {:ok, [notification]} = Notification.create_notifications(activity) + {:ok, notification} = Notification.get(other_user, notification.id) + + assert notification.user_id == other_user.id + end + + test "it returns error if the notification doesn't belong to the user" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"}) + + {:ok, [notification]} = Notification.create_notifications(activity) + {:error, _notification} = Notification.get(user, notification.id) + end + end + + describe "dismiss notification" do + test "it dismisses a notification that belongs to the user" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"}) + + {:ok, [notification]} = Notification.create_notifications(activity) + {:ok, notification} = Notification.dismiss(other_user, notification.id) + + assert notification.user_id == other_user.id + end + + test "it returns error if the notification doesn't belong to the user" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"}) + + {:ok, [notification]} = Notification.create_notifications(activity) + {:error, _notification} = Notification.dismiss(user, notification.id) + end + end + + describe "clear notification" do + test "it clears all notifications belonging to the user" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname} and @#{third_user.nickname} !" + }) + + {:ok, _notifs} = Notification.create_notifications(activity) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "hey again @#{other_user.nickname} and @#{third_user.nickname} !" + }) + + {:ok, _notifs} = Notification.create_notifications(activity) + Notification.clear(other_user) + + assert Notification.for_user(other_user) == [] + assert Notification.for_user(third_user) != [] + 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) + other_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname}!" + }) + + {:ok, _activity} = + CommonAPI.post(user, %{ + status: "hey again @#{other_user.nickname}!" + }) + + [n2, n1] = Notification.for_user(other_user) + + assert n2.id > n1.id + + {:ok, _activity} = + CommonAPI.post(user, %{ + status: "hey yet again @#{other_user.nickname}!" + }) + + [_, read_notification] = Notification.set_read_up_to(other_user, n2.id) + + assert read_notification.activity.object + + [n3, n2, n1] = Notification.for_user(other_user) + + assert n1.seen == true + assert n2.seen == true + assert n3.seen == false + + assert %Pleroma.Marker{} = + m = + Pleroma.Repo.get_by( + Pleroma.Marker, + user_id: other_user.id, + timeline: "notifications" + ) + + assert m.last_read_id == to_string(n2.id) + end + end + + describe "for_user_since/2" do + defp days_ago(days) do + NaiveDateTime.add( + NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), + -days * 60 * 60 * 24, + :second + ) + end + + test "Returns recent notifications" do + user1 = insert(:user) + user2 = insert(:user) + + Enum.each(0..10, fn i -> + {:ok, _activity} = + CommonAPI.post(user1, %{ + status: "hey ##{i} @#{user2.nickname}!" + }) + end) + + {old, new} = Enum.split(Notification.for_user(user2), 5) + + Enum.each(old, fn notification -> + notification + |> cast(%{updated_at: days_ago(10)}, [:updated_at]) + |> Pleroma.Repo.update!() + end) + + recent_notifications_ids = + user2 + |> Notification.for_user_since( + NaiveDateTime.add(NaiveDateTime.utc_now(), -5 * 86_400, :second) + ) + |> Enum.map(& &1.id) + + Enum.each(old, fn %{id: id} -> + refute id in recent_notifications_ids + end) + + Enum.each(new, fn %{id: id} -> + assert id in recent_notifications_ids + end) + end + end + + describe "notification target determination / get_notified_from_activity/2" do + test "it sends notifications to addressed users in new messages" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname}!" + }) + + {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) + + assert other_user in enabled_receivers + end + + test "it sends notifications to mentioned users in new messages" do + user = insert(:user) + other_user = insert(:user) + + create_activity = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "actor" => user.ap_id, + "object" => %{ + "type" => "Note", + "content" => "message with a Mention tag, but no explicit tagging", + "tag" => [ + %{ + "type" => "Mention", + "href" => other_user.ap_id, + "name" => other_user.nickname + } + ], + "attributedTo" => user.ap_id + } + } + + {:ok, activity} = Transmogrifier.handle_incoming(create_activity) + + {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) + + assert other_user in enabled_receivers + end + + test "it does not send notifications to users who are only cc in new messages" do + user = insert(:user) + other_user = insert(:user) + + create_activity = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [other_user.ap_id], + "actor" => user.ap_id, + "object" => %{ + "type" => "Note", + "content" => "hi everyone", + "attributedTo" => user.ap_id + } + } + + {:ok, activity} = Transmogrifier.handle_incoming(create_activity) + + {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) + + assert other_user not in enabled_receivers + end + + test "it does not send notification to mentioned users in likes" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, activity_one} = + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname}!" + }) + + {:ok, activity_two} = CommonAPI.favorite(third_user, activity_one.id) + + {enabled_receivers, _disabled_receivers} = + Notification.get_notified_from_activity(activity_two) + + assert other_user not in enabled_receivers + end + + test "it only notifies the post's author in likes" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, activity_one} = + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname}!" + }) + + {:ok, like_data, _} = Builder.like(third_user, activity_one.object) + + {:ok, like, _} = + like_data + |> Map.put("to", [other_user.ap_id | like_data["to"]]) + |> ActivityPub.persist(local: true) + + {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(like) + + assert other_user not in enabled_receivers + end + + test "it does not send notification to mentioned users in announces" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, activity_one} = + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname}!" + }) + + {:ok, activity_two} = CommonAPI.repeat(activity_one.id, third_user) + + {enabled_receivers, _disabled_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 + 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) + + assert [] == enabled_receivers + assert [other_user] == disabled_receivers + end + + test "it returns notification-muting recipient in disabled 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) + + assert [] == enabled_receivers + assert [other_user] == disabled_receivers + end + + test "it returns thread-muting recipient in disabled recipients list" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) + + {:ok, _} = CommonAPI.add_mute(other_user, activity) + + {:ok, same_context_activity} = + CommonAPI.post(user, %{ + status: "hey-hey-hey @#{other_user.nickname}!", + in_reply_to_status_id: activity.id + }) + + {enabled_receivers, disabled_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 + blocked_domain = "blocked.domain" + user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"}) + other_user = insert(:user) + + {:ok, other_user} = User.block_domain(other_user, blocked_domain) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) + + {enabled_receivers, disabled_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 + blocked_domain = "blocked.domain" + user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"}) + other_user = insert(:user) + + {:ok, other_user} = User.block_domain(other_user, blocked_domain) + {:ok, other_user} = User.follow(other_user, user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) + + {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) + + assert [other_user] == enabled_receivers + assert [] == disabled_receivers + end + end + + describe "notification lifecycle" do + test "liking an activity results in 1 notification, then 0 if the activity is deleted" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) + + assert Enum.empty?(Notification.for_user(user)) + + {:ok, _} = CommonAPI.favorite(other_user, activity.id) + + assert length(Notification.for_user(user)) == 1 + + {:ok, _} = CommonAPI.delete(activity.id, user) + + assert Enum.empty?(Notification.for_user(user)) + end + + test "liking an activity results in 1 notification, then 0 if the activity is unliked" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) + + assert Enum.empty?(Notification.for_user(user)) + + {:ok, _} = CommonAPI.favorite(other_user, activity.id) + + assert length(Notification.for_user(user)) == 1 + + {:ok, _} = CommonAPI.unfavorite(activity.id, other_user) + + assert Enum.empty?(Notification.for_user(user)) + end + + test "repeating an activity results in 1 notification, then 0 if the activity is deleted" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) + + assert Enum.empty?(Notification.for_user(user)) + + {:ok, _} = CommonAPI.repeat(activity.id, other_user) + + assert length(Notification.for_user(user)) == 1 + + {:ok, _} = CommonAPI.delete(activity.id, user) + + assert Enum.empty?(Notification.for_user(user)) + end + + test "repeating an activity results in 1 notification, then 0 if the activity is unrepeated" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) + + assert Enum.empty?(Notification.for_user(user)) + + {:ok, _} = CommonAPI.repeat(activity.id, other_user) + + assert length(Notification.for_user(user)) == 1 + + {:ok, _} = CommonAPI.unrepeat(activity.id, other_user) + + assert Enum.empty?(Notification.for_user(user)) + end + + test "liking an activity which is already deleted does not generate a notification" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) + + assert Enum.empty?(Notification.for_user(user)) + + {:ok, _deletion_activity} = CommonAPI.delete(activity.id, user) + + assert Enum.empty?(Notification.for_user(user)) + + {:error, :not_found} = CommonAPI.favorite(other_user, activity.id) + + assert Enum.empty?(Notification.for_user(user)) + end + + test "repeating an activity which is already deleted does not generate a notification" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) + + assert Enum.empty?(Notification.for_user(user)) + + {:ok, _deletion_activity} = CommonAPI.delete(activity.id, user) + + assert Enum.empty?(Notification.for_user(user)) + + {:error, _} = CommonAPI.repeat(activity.id, other_user) + + 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) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "hi @#{other_user.nickname}", visibility: "direct"}) + + refute Enum.empty?(Notification.for_user(other_user)) + + {:ok, job} = User.delete(user) + ObanHelpers.perform(job) + + assert Enum.empty?(Notification.for_user(other_user)) + end + + test "notifications are deleted if a remote user is deleted" do + remote_user = insert(:user) + local_user = insert(:user) + + dm_message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "actor" => remote_user.ap_id, + "id" => remote_user.ap_id <> "/activities/test", + "to" => [local_user.ap_id], + "cc" => [], + "object" => %{ + "type" => "Note", + "content" => "Hello!", + "tag" => [ + %{ + "type" => "Mention", + "href" => local_user.ap_id, + "name" => "@#{local_user.nickname}" + } + ], + "to" => [local_user.ap_id], + "cc" => [], + "attributedTo" => remote_user.ap_id + } + } + + {:ok, _dm_activity} = Transmogrifier.handle_incoming(dm_message) + + refute Enum.empty?(Notification.for_user(local_user)) + + delete_user_message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => remote_user.ap_id <> "/activities/delete", + "actor" => remote_user.ap_id, + "type" => "Delete", + "object" => remote_user.ap_id + } + + remote_user_url = remote_user.ap_id + + Tesla.Mock.mock(fn + %{method: :get, url: ^remote_user_url} -> + %Tesla.Env{status: 404, body: ""} + end) + + {:ok, _delete_activity} = Transmogrifier.handle_incoming(delete_user_message) + ObanHelpers.perform_all() + + assert Enum.empty?(Notification.for_user(local_user)) + end + + @tag capture_log: true + test "move activity generates a notification" 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 = insert(:user) + other_follower = insert(:user, %{allow_following_move: false}) + + User.follow(follower, old_user) + User.follow(other_follower, old_user) + + old_user_url = old_user.ap_id + + body = + File.read!("test/fixtures/users_mock/localhost.json") + |> String.replace("{{nickname}}", old_user.nickname) + |> Jason.encode!() + + Tesla.Mock.mock(fn + %{method: :get, url: ^old_user_url} -> + %Tesla.Env{status: 200, body: body} + end) + + Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) + ObanHelpers.perform_all() + + assert [ + %{ + activity: %{ + data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id} + } + } + ] = Notification.for_user(follower) + + assert [ + %{ + activity: %{ + data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id} + } + } + ] = Notification.for_user(other_follower) + end + end + + describe "for_user" do + setup do + user = insert(:user) + + {:ok, %{user: user}} + end + + test "it returns notifications for muted user without notifications", %{user: user} do + muted = insert(:user) + {:ok, _user_relationships} = User.mute(user, muted, false) + + {:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"}) + + [notification] = Notification.for_user(user) + + assert notification.activity.object + assert notification.seen + end + + test "it doesn't return notifications for muted user with notifications", %{user: user} do + muted = insert(:user) + {:ok, _user_relationships} = User.mute(user, muted) + + {:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"}) + + assert Notification.for_user(user) == [] + end + + test "it doesn't return notifications for blocked user", %{user: user} do + blocked = insert(:user) + {:ok, _user_relationship} = User.block(user, blocked) + + {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) + + assert Notification.for_user(user) == [] + end + + test "it doesn't return notifications for domain-blocked non-followed user", %{user: user} do + blocked = insert(:user, ap_id: "http://some-domain.com") + {:ok, user} = User.block_domain(user, "some-domain.com") + + {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) + + assert Notification.for_user(user) == [] + end + + test "it returns notifications for domain-blocked but followed user" do + user = insert(:user) + blocked = insert(:user, ap_id: "http://some-domain.com") + + {:ok, user} = User.block_domain(user, "some-domain.com") + {:ok, _} = User.follow(user, blocked) + + {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) + + assert length(Notification.for_user(user)) == 1 + end + + test "it doesn't return notifications for muted thread", %{user: user} do + another_user = insert(:user) + + {:ok, activity} = CommonAPI.post(another_user, %{status: "hey @#{user.nickname}"}) + + {:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"]) + assert Notification.for_user(user) == [] + end + + test "it returns 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 + end + + test "it doesn't return notifications from a blocked user when with_muted is set", %{ + user: user + } do + blocked = insert(:user) + {:ok, _user_relationship} = User.block(user, blocked) + + {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) + + assert Enum.empty?(Notification.for_user(user, %{with_muted: true})) + end + + test "when with_muted is set, " <> + "it doesn't return notifications from a domain-blocked non-followed user", + %{user: user} do + blocked = insert(:user, ap_id: "http://some-domain.com") + {:ok, user} = User.block_domain(user, "some-domain.com") + + {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) + + assert Enum.empty?(Notification.for_user(user, %{with_muted: true})) + end + + test "it returns notifications from muted threads when with_muted is set", %{user: user} do + another_user = insert(:user) + + {:ok, activity} = CommonAPI.post(another_user, %{status: "hey @#{user.nickname}"}) + + {:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"]) + assert length(Notification.for_user(user, %{with_muted: true})) == 1 + end + + test "it doesn't return notifications about mentions with filtered word", %{user: user} do + insert(:filter, user: user, phrase: "cofe", hide: true) + another_user = insert(:user) + + {:ok, _activity} = CommonAPI.post(another_user, %{status: "@#{user.nickname} got cofe?"}) + + assert Enum.empty?(Notification.for_user(user)) + end + + test "it returns notifications about mentions with not hidden filtered word", %{user: user} do + insert(:filter, user: user, phrase: "test", hide: false) + another_user = insert(:user) + + {:ok, _} = CommonAPI.post(another_user, %{status: "@#{user.nickname} test"}) + + assert length(Notification.for_user(user)) == 1 + end + + test "it returns notifications about favorites with filtered word", %{user: user} do + insert(:filter, user: user, phrase: "cofe", hide: true) + another_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "Give me my cofe!"}) + {:ok, _} = CommonAPI.favorite(another_user, activity.id) + + assert length(Notification.for_user(user)) == 1 + end + end +end diff --git a/test/pleroma/object/containment_test.exs b/test/pleroma/object/containment_test.exs @@ -0,0 +1,125 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Object.ContainmentTest do + use Pleroma.DataCase + + alias Pleroma.Object.Containment + alias Pleroma.User + + import Pleroma.Factory + import ExUnit.CaptureLog + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "general origin containment" do + test "works for completely actorless posts" do + assert :error == + Containment.contain_origin("https://glaceon.social/users/monorail", %{ + "deleted" => "2019-10-30T05:48:50.249606Z", + "formerType" => "Note", + "id" => "https://glaceon.social/users/monorail/statuses/103049757364029187", + "type" => "Tombstone" + }) + end + + test "contain_origin_from_id() catches obvious spoofing attempts" do + data = %{ + "id" => "http://example.com/~alyssa/activities/1234.json" + } + + :error = + Containment.contain_origin_from_id( + "http://example.org/~alyssa/activities/1234.json", + data + ) + end + + test "contain_origin_from_id() allows alternate IDs within the same origin domain" do + data = %{ + "id" => "http://example.com/~alyssa/activities/1234.json" + } + + :ok = + Containment.contain_origin_from_id( + "http://example.com/~alyssa/activities/1234", + data + ) + end + + test "contain_origin_from_id() allows matching IDs" do + data = %{ + "id" => "http://example.com/~alyssa/activities/1234.json" + } + + :ok = + Containment.contain_origin_from_id( + "http://example.com/~alyssa/activities/1234.json", + data + ) + end + + test "users cannot be collided through fake direction spoofing attempts" 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"}) + }) + + assert capture_log(fn -> + {:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye") + end) =~ + "[error] Could not decode user at fetch https://n1u.moe/users/rye" + end + + test "contain_origin_from_id() gracefully handles cases where no ID is present" do + data = %{ + "type" => "Create", + "object" => %{ + "id" => "http://example.net/~alyssa/activities/1234", + "attributedTo" => "http://example.org/~alyssa" + }, + "actor" => "http://example.com/~bob" + } + + :error = + Containment.contain_origin_from_id("http://example.net/~alyssa/activities/1234", data) + end + end + + describe "containment of children" do + test "contain_child() catches spoofing attempts" do + data = %{ + "id" => "http://example.com/whatever", + "type" => "Create", + "object" => %{ + "id" => "http://example.net/~alyssa/activities/1234", + "attributedTo" => "http://example.org/~alyssa" + }, + "actor" => "http://example.com/~bob" + } + + :error = Containment.contain_child(data) + end + + test "contain_child() allows correct origins" do + data = %{ + "id" => "http://example.org/~alyssa/activities/5678", + "type" => "Create", + "object" => %{ + "id" => "http://example.org/~alyssa/activities/1234", + "attributedTo" => "http://example.org/~alyssa" + }, + "actor" => "http://example.org/~alyssa" + } + + :ok = Containment.contain_child(data) + end + end +end diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs @@ -0,0 +1,245 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Object.FetcherTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.Object + alias Pleroma.Object.Fetcher + + import Mock + import Tesla.Mock + + setup do + mock(fn + %{method: :get, url: "https://mastodon.example.org/users/userisgone"} -> + %Tesla.Env{status: 410} + + %{method: :get, url: "https://mastodon.example.org/users/userisgone404"} -> + %Tesla.Env{status: 404} + + env -> + apply(HttpRequestMock, :request, [env]) + end) + + :ok + end + + describe "error cases" do + setup do + mock(fn + %{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json") + } + + %{method: :get, url: "https://social.sakamoto.gq/users/eal"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/fetch_mocks/eal.json") + } + + %{method: :get, url: "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/fetch_mocks/104410921027210069.json") + } + + %{method: :get, url: "https://busshi.moe/users/tuxcrafting"} -> + %Tesla.Env{ + status: 500 + } + end) + + :ok + end + + @tag capture_log: true + test "it works when fetching the OP actor errors out" do + # Here we simulate a case where the author of the OP can't be read + assert {:ok, _} = + Fetcher.fetch_object_from_id( + "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM" + ) + end + end + + describe "max thread distance restriction" do + @ap_id "http://mastodon.example.org/@admin/99541947525187367" + setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) + + test "it returns thread depth exceeded error if thread depth is exceeded" do + Config.put([:instance, :federation_incoming_replies_max_depth], 0) + + assert {:error, "Max thread distance exceeded."} = + 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 + Config.put([:instance, :federation_incoming_replies_max_depth], 0) + + assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id) + end + + test "it fetches object if requested depth does not exceed max thread depth" do + Config.put([:instance, :federation_incoming_replies_max_depth], 10) + + assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id, depth: 10) + end + end + + describe "actor origin containment" do + test "it rejects objects with a bogus origin" do + {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json") + end + + test "it rejects objects when attributedTo is wrong (variant 1)" do + {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json") + end + + test "it rejects objects when attributedTo is wrong (variant 2)" do + {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json") + end + end + + describe "fetching an object" do + test "it fetches an object" do + {:ok, object} = + Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") + + assert activity = Activity.get_create_by_object_ap_id(object.data["id"]) + assert activity.data["id"] + + {:ok, object_again} = + Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") + + assert [attachment] = object.data["attachment"] + assert is_list(attachment["url"]) + + assert object == object_again + end + + test "Return MRF reason when fetched status is rejected by one" do + clear_config([:mrf_keyword, :reject], ["yeah"]) + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) + + assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} == + Fetcher.fetch_object_from_id( + "http://mastodon.example.org/@admin/99541947525187367" + ) + end + end + + describe "implementation quirks" do + test "it can fetch plume articles" do + {:ok, object} = + Fetcher.fetch_object_from_id( + "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/" + ) + + assert object + end + + test "it can fetch peertube videos" do + {:ok, object} = + Fetcher.fetch_object_from_id( + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" + ) + + assert object + end + + test "it can fetch Mobilizon events" do + {:ok, object} = + Fetcher.fetch_object_from_id( + "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" + ) + + assert object + end + + test "it can fetch wedistribute articles" do + {:ok, object} = + Fetcher.fetch_object_from_id("https://wedistribute.org/wp-json/pterotype/v1/object/85810") + + assert object + end + + test "all objects with fake directions are rejected by the object fetcher" do + assert {:error, _} = + Fetcher.fetch_and_contain_remote_object_from_id( + "https://info.pleroma.site/activity4.json" + ) + end + + test "handle HTTP 410 Gone response" do + assert {:error, "Object has been deleted"} == + 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"} == + Fetcher.fetch_and_contain_remote_object_from_id( + "https://mastodon.example.org/users/userisgone404" + ) + end + + test "it can fetch pleroma polls with attachments" do + {:ok, object} = + Fetcher.fetch_object_from_id("https://patch.cx/objects/tesla_mock/poll_attachment") + + assert object + end + end + + describe "pruning" do + test "it can refetch pruned objects" do + object_id = "http://mastodon.example.org/@admin/99541947525187367" + + {:ok, object} = Fetcher.fetch_object_from_id(object_id) + + assert object + + {:ok, _object} = Object.prune(object) + + refute Object.get_by_ap_id(object_id) + + {:ok, %Object{} = object_two} = Fetcher.fetch_object_from_id(object_id) + + assert object.data["id"] == object_two.data["id"] + assert object.id != object_two.id + end + end + + describe "signed fetches" do + setup do: clear_config([:activitypub, :sign_object_fetches]) + + test_with_mock "it signs fetches when configured to do so", + Pleroma.Signature, + [:passthrough], + [] do + Config.put([:activitypub, :sign_object_fetches], true) + + Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") + + assert called(Pleroma.Signature.sign(:_, :_)) + end + + test_with_mock "it doesn't sign fetches when not configured to do so", + Pleroma.Signature, + [:passthrough], + [] do + Config.put([:activitypub, :sign_object_fetches], false) + + Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") + + refute called(Pleroma.Signature.sign(:_, :_)) + end + end +end diff --git a/test/pleroma/object_test.exs b/test/pleroma/object_test.exs @@ -0,0 +1,402 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ObjectTest do + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + import ExUnit.CaptureLog + import Pleroma.Factory + import Tesla.Mock + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.Web.CommonAPI + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "returns an object by it's AP id" do + object = insert(:note) + found_object = Object.get_by_ap_id(object.data["id"]) + + assert object == found_object + end + + describe "generic changeset" do + test "it ensures uniqueness of the id" do + object = insert(:note) + cs = Object.change(%Object{}, %{data: %{id: object.data["id"]}}) + assert cs.valid? + + {:error, _result} = Repo.insert(cs) + end + end + + describe "deletion function" do + test "deletes an object" do + object = insert(:note) + found_object = Object.get_by_ap_id(object.data["id"]) + + assert object == found_object + + Object.delete(found_object) + + found_object = Object.get_by_ap_id(object.data["id"]) + + refute object == found_object + + assert found_object.data["type"] == "Tombstone" + end + + test "ensures cache is cleared for the object" do + object = insert(:note) + cached_object = Object.get_cached_by_ap_id(object.data["id"]) + + assert object == cached_object + + Cachex.put(:web_resp_cache, URI.parse(object.data["id"]).path, "cofe") + + Object.delete(cached_object) + + {:ok, nil} = Cachex.get(:object_cache, "object:#{object.data["id"]}") + {:ok, nil} = Cachex.get(:web_resp_cache, URI.parse(object.data["id"]).path) + + cached_object = Object.get_cached_by_ap_id(object.data["id"]) + + refute object == cached_object + + assert cached_object.data["type"] == "Tombstone" + end + end + + describe "delete attachments" do + setup do: clear_config([Pleroma.Upload]) + setup do: clear_config([:instance, :cleanup_attachments]) + + test "Disabled via config" do + Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + Pleroma.Config.put([:instance, :cleanup_attachments], false) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + user = insert(:user) + + {:ok, %Object{} = attachment} = + Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) + + %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = + note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) + + uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) + + path = href |> Path.dirname() |> Path.basename() + + assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") + + Object.delete(note) + + ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) + + assert Object.get_by_id(note.id).data["deleted"] + refute Object.get_by_id(attachment.id) == nil + + assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") + end + + test "in subdirectories" do + Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + Pleroma.Config.put([:instance, :cleanup_attachments], true) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + user = insert(:user) + + {:ok, %Object{} = attachment} = + Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) + + %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = + note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) + + uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) + + path = href |> Path.dirname() |> Path.basename() + + assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") + + Object.delete(note) + + ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) + + assert Object.get_by_id(note.id).data["deleted"] + assert Object.get_by_id(attachment.id) == nil + + assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") + end + + test "with dedupe enabled" do + Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + Pleroma.Config.put([Pleroma.Upload, :filters], [Pleroma.Upload.Filter.Dedupe]) + Pleroma.Config.put([:instance, :cleanup_attachments], true) + + uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) + + File.mkdir_p!(uploads_dir) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + user = insert(:user) + + {:ok, %Object{} = attachment} = + Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) + + %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = + note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) + + filename = Path.basename(href) + + assert {:ok, files} = File.ls(uploads_dir) + assert filename in files + + Object.delete(note) + + ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) + + assert Object.get_by_id(note.id).data["deleted"] + assert Object.get_by_id(attachment.id) == nil + assert {:ok, files} = File.ls(uploads_dir) + refute filename in files + end + + test "with objects that have legacy data.url attribute" do + Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + Pleroma.Config.put([:instance, :cleanup_attachments], true) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + user = insert(:user) + + {:ok, %Object{} = attachment} = + Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) + + {:ok, %Object{}} = Object.create(%{url: "https://google.com", actor: user.ap_id}) + + %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = + note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) + + uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) + + path = href |> Path.dirname() |> Path.basename() + + assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") + + Object.delete(note) + + ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) + + assert Object.get_by_id(note.id).data["deleted"] + assert Object.get_by_id(attachment.id) == nil + + assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") + end + + test "With custom base_url" do + Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + Pleroma.Config.put([Pleroma.Upload, :base_url], "https://sub.domain.tld/dir/") + Pleroma.Config.put([:instance, :cleanup_attachments], true) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + user = insert(:user) + + {:ok, %Object{} = attachment} = + Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) + + %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = + note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) + + uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) + + path = href |> Path.dirname() |> Path.basename() + + assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") + + Object.delete(note) + + ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) + + assert Object.get_by_id(note.id).data["deleted"] + assert Object.get_by_id(attachment.id) == nil + + assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") + end + end + + describe "normalizer" do + test "fetches unknown objects by default" do + %Object{} = + object = Object.normalize("http://mastodon.example.org/@admin/99541947525187367") + + assert object.data["url"] == "http://mastodon.example.org/@admin/99541947525187367" + end + + test "fetches unknown objects when fetch_remote is explicitly true" do + %Object{} = + object = Object.normalize("http://mastodon.example.org/@admin/99541947525187367", true) + + assert object.data["url"] == "http://mastodon.example.org/@admin/99541947525187367" + end + + test "does not fetch unknown objects when fetch_remote is false" do + assert is_nil( + Object.normalize("http://mastodon.example.org/@admin/99541947525187367", false) + ) + end + end + + describe "get_by_id_and_maybe_refetch" do + setup do + mock(fn + %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_original.json")} + + env -> + apply(HttpRequestMock, :request, [env]) + end) + + mock_modified = fn resp -> + mock(fn + %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} -> + resp + + env -> + apply(HttpRequestMock, :request, [env]) + end) + end + + on_exit(fn -> mock(fn env -> apply(HttpRequestMock, :request, [env]) end) end) + + [mock_modified: mock_modified] + end + + test "refetches if the time since the last refetch is greater than the interval", %{ + mock_modified: mock_modified + } do + %Object{} = + object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") + + Object.set_cache(object) + + assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 + assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 + + mock_modified.(%Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/poll_modified.json") + }) + + updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1) + object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) + assert updated_object == object_in_cache + assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8 + assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3 + end + + test "returns the old object if refetch fails", %{mock_modified: mock_modified} do + %Object{} = + object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") + + Object.set_cache(object) + + assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 + assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 + + assert capture_log(fn -> + mock_modified.(%Tesla.Env{status: 404, body: ""}) + + updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1) + object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) + assert updated_object == object_in_cache + assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4 + assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0 + end) =~ + "[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d" + end + + test "does not refetch if the time since the last refetch is greater than the interval", %{ + mock_modified: mock_modified + } do + %Object{} = + object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") + + Object.set_cache(object) + + assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 + assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 + + mock_modified.(%Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/poll_modified.json") + }) + + updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100) + object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) + assert updated_object == object_in_cache + assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4 + assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0 + end + + test "preserves internal fields on refetch", %{mock_modified: mock_modified} do + %Object{} = + object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") + + Object.set_cache(object) + + assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 + assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 + + user = insert(:user) + activity = Activity.get_create_by_object_ap_id(object.data["id"]) + {:ok, activity} = CommonAPI.favorite(user, activity.id) + object = Object.get_by_ap_id(activity.data["object"]) + + assert object.data["like_count"] == 1 + + mock_modified.(%Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/poll_modified.json") + }) + + updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1) + object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) + assert updated_object == object_in_cache + assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8 + assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3 + + assert updated_object.data["like_count"] == 1 + end + end +end diff --git a/test/pleroma/otp_version_test.exs b/test/pleroma/otp_version_test.exs @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.OTPVersionTest do + use ExUnit.Case, async: true + + alias Pleroma.OTPVersion + + describe "check/1" do + test "22.4" do + assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.4"]) == + "22.4" + end + + test "22.1" do + assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.1"]) == + "22.1" + end + + test "21.1" do + assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/21.1"]) == + "21.1" + end + + test "23.0" do + assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/23.0"]) == + "23.0" + end + + test "with non existance file" do + assert OTPVersion.get_version_from_files([ + "test/fixtures/warnings/otp_version/non-exising", + "test/fixtures/warnings/otp_version/22.4" + ]) == "22.4" + end + + test "empty paths" do + assert OTPVersion.get_version_from_files([]) == nil + end + end +end diff --git a/test/pleroma/pagination_test.exs b/test/pleroma/pagination_test.exs @@ -0,0 +1,92 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.PaginationTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Object + alias Pleroma.Pagination + + describe "keyset" do + setup do + notes = insert_list(5, :note) + + %{notes: notes} + end + + test "paginates by min_id", %{notes: notes} do + id = Enum.at(notes, 2).id |> Integer.to_string() + + %{total: total, items: paginated} = + Pagination.fetch_paginated(Object, %{min_id: id, total: true}) + + assert length(paginated) == 2 + assert total == 5 + end + + test "paginates by since_id", %{notes: notes} do + id = Enum.at(notes, 2).id |> Integer.to_string() + + %{total: total, items: paginated} = + Pagination.fetch_paginated(Object, %{since_id: id, total: true}) + + assert length(paginated) == 2 + assert total == 5 + end + + test "paginates by max_id", %{notes: notes} do + id = Enum.at(notes, 1).id |> Integer.to_string() + + %{total: total, items: paginated} = + Pagination.fetch_paginated(Object, %{max_id: id, total: true}) + + assert length(paginated) == 1 + assert total == 5 + end + + test "paginates by min_id & limit", %{notes: notes} do + id = Enum.at(notes, 2).id |> Integer.to_string() + + paginated = Pagination.fetch_paginated(Object, %{min_id: id, limit: 1}) + + assert length(paginated) == 1 + end + + test "handles id gracefully", %{notes: notes} do + id = Enum.at(notes, 1).id |> Integer.to_string() + + paginated = + Pagination.fetch_paginated(Object, %{ + id: "9s99Hq44Cnv8PKBwWG", + max_id: id, + limit: 20, + offset: 0 + }) + + assert length(paginated) == 1 + end + end + + describe "offset" do + setup do + notes = insert_list(5, :note) + + %{notes: notes} + end + + test "paginates by limit" do + paginated = Pagination.fetch_paginated(Object, %{limit: 2}, :offset) + + assert length(paginated) == 2 + end + + test "paginates by limit & offset" do + paginated = Pagination.fetch_paginated(Object, %{limit: 2, offset: 4}, :offset) + + assert length(paginated) == 1 + end + end +end diff --git a/test/pleroma/registration_test.exs b/test/pleroma/registration_test.exs @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.RegistrationTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Registration + alias Pleroma.Repo + + describe "generic changeset" do + test "requires :provider, :uid" do + registration = build(:registration, provider: nil, uid: nil) + + cs = Registration.changeset(registration, %{}) + refute cs.valid? + + assert [ + provider: {"can't be blank", [validation: :required]}, + uid: {"can't be blank", [validation: :required]} + ] == cs.errors + end + + test "ensures uniqueness of [:provider, :uid]" do + registration = insert(:registration) + registration2 = build(:registration, provider: registration.provider, uid: registration.uid) + + cs = Registration.changeset(registration2, %{}) + assert cs.valid? + + assert {:error, + %Ecto.Changeset{ + errors: [ + uid: + {"has already been taken", + [constraint: :unique, constraint_name: "registrations_provider_uid_index"]} + ] + }} = Repo.insert(cs) + + # Note: multiple :uid values per [:user_id, :provider] are intentionally allowed + cs2 = Registration.changeset(registration2, %{uid: "available.uid"}) + assert cs2.valid? + assert {:ok, _} = Repo.insert(cs2) + + cs3 = Registration.changeset(registration2, %{provider: "provider2"}) + assert cs3.valid? + assert {:ok, _} = Repo.insert(cs3) + end + + test "allows `nil` :user_id (user-unbound registration)" do + registration = build(:registration, user_id: nil) + cs = Registration.changeset(registration, %{}) + assert cs.valid? + assert {:ok, _} = Repo.insert(cs) + end + end +end diff --git a/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs b/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.AutolinkerToLinkifyTest do + use Pleroma.DataCase + import Pleroma.Factory + import Pleroma.Tests.Helpers + alias Pleroma.ConfigDB + + setup do: clear_config(Pleroma.Formatter) + setup_all do: require_migration("20200716195806_autolinker_to_linkify") + + test "change/0 converts auto_linker opts for Pleroma.Formatter", %{migration: migration} do + autolinker_opts = [ + extra: true, + validate_tld: true, + class: false, + strip_prefix: false, + new_window: false, + rel: "testing" + ] + + insert(:config, group: :auto_linker, key: :opts, value: autolinker_opts) + + migration.change() + + assert nil == ConfigDB.get_by_params(%{group: :auto_linker, key: :opts}) + + %{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter}) + + assert new_opts == [ + class: false, + extra: true, + new_window: false, + rel: "testing", + strip_prefix: false + ] + + Pleroma.Config.put(Pleroma.Formatter, new_opts) + assert new_opts == Pleroma.Config.get(Pleroma.Formatter) + + {text, _mentions, []} = + Pleroma.Formatter.linkify( + "https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7\n\nOmg will COVID finally end Black Friday???" + ) + + assert text == + "<a href=\"https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7\" rel=\"testing\">https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7</a>\n\nOmg will COVID finally end Black Friday???" + end + + test "transform_opts/1 returns a list of compatible opts", %{migration: migration} do + old_opts = [ + extra: true, + validate_tld: true, + class: false, + strip_prefix: false, + new_window: false, + rel: "qqq" + ] + + expected_opts = [ + class: false, + extra: true, + new_window: false, + rel: "qqq", + strip_prefix: false + ] + + assert migration.transform_opts(old_opts) == expected_opts + end +end diff --git a/test/pleroma/repo/migrations/fix_legacy_tags_test.exs b/test/pleroma/repo/migrations/fix_legacy_tags_test.exs @@ -0,0 +1,28 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.FixLegacyTagsTest do + alias Pleroma.User + use Pleroma.DataCase + import Pleroma.Factory + import Pleroma.Tests.Helpers + + setup_all do: require_migration("20200802170532_fix_legacy_tags") + + test "change/0 converts legacy user tags into correct values", %{migration: migration} do + user = insert(:user, tags: ["force_nsfw", "force_unlisted", "verified"]) + user2 = insert(:user) + + assert :ok == migration.change() + + fixed_user = User.get_by_id(user.id) + fixed_user2 = User.get_by_id(user2.id) + + assert fixed_user.tags == ["mrf_tag:media-force-nsfw", "mrf_tag:force-unlisted", "verified"] + assert fixed_user2.tags == [] + + # user2 should not have been updated + assert fixed_user2.updated_at == fixed_user2.inserted_at + 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 @@ -0,0 +1,70 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.FixMalformedFormatterConfigTest do + use Pleroma.DataCase + import Pleroma.Factory + import Pleroma.Tests.Helpers + alias Pleroma.ConfigDB + + setup do: clear_config(Pleroma.Formatter) + setup_all do: require_migration("20200722185515_fix_malformed_formatter_config") + + test "change/0 converts a map into a list", %{migration: migration} do + incorrect_opts = %{ + class: false, + extra: true, + new_window: false, + rel: "F", + strip_prefix: false + } + + insert(:config, group: :pleroma, key: Pleroma.Formatter, value: incorrect_opts) + + assert :ok == migration.change() + + %{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter}) + + assert new_opts == [ + class: false, + extra: true, + new_window: false, + rel: "F", + strip_prefix: false + ] + + Pleroma.Config.put(Pleroma.Formatter, new_opts) + assert new_opts == Pleroma.Config.get(Pleroma.Formatter) + + {text, _mentions, []} = + Pleroma.Formatter.linkify( + "https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7\n\nOmg will COVID finally end Black Friday???" + ) + + assert text == + "<a href=\"https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7\" rel=\"F\">https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7</a>\n\nOmg will COVID finally end Black Friday???" + end + + test "change/0 skips if Pleroma.Formatter config is already a list", %{migration: migration} do + opts = [ + class: false, + extra: true, + new_window: false, + rel: "ugc", + strip_prefix: false + ] + + insert(:config, group: :pleroma, key: Pleroma.Formatter, value: opts) + + assert :skipped == migration.change() + + %{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter}) + + assert new_opts == opts + end + + test "change/0 skips if Pleroma.Formatter is empty", %{migration: migration} do + assert :skipped == migration.change() + end +end diff --git a/test/pleroma/repo/migrations/move_welcome_settings_test.exs b/test/pleroma/repo/migrations/move_welcome_settings_test.exs @@ -0,0 +1,144 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.MoveWelcomeSettingsTest do + use Pleroma.DataCase + import Pleroma.Factory + import Pleroma.Tests.Helpers + alias Pleroma.ConfigDB + + setup_all do: require_migration("20200724133313_move_welcome_settings") + + describe "up/0" do + test "converts welcome settings", %{migration: migration} do + insert(:config, + group: :pleroma, + key: :instance, + value: [ + welcome_message: "Test message", + welcome_user_nickname: "jimm", + name: "Pleroma" + ] + ) + + migration.up() + instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) + welcome_config = ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) + + assert instance_config.value == [name: "Pleroma"] + + assert welcome_config.value == [ + direct_message: %{ + enabled: true, + message: "Test message", + sender_nickname: "jimm" + }, + email: %{ + enabled: false, + html: "Welcome to <%= instance_name %>", + sender: nil, + subject: "Welcome to <%= instance_name %>", + text: "Welcome to <%= instance_name %>" + } + ] + end + + test "does nothing when message empty", %{migration: migration} do + insert(:config, + group: :pleroma, + key: :instance, + value: [ + welcome_message: "", + welcome_user_nickname: "jimm", + name: "Pleroma" + ] + ) + + migration.up() + instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) + refute ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) + assert instance_config.value == [name: "Pleroma"] + end + + test "does nothing when welcome_message not set", %{migration: migration} do + insert(:config, + group: :pleroma, + key: :instance, + value: [welcome_user_nickname: "jimm", name: "Pleroma"] + ) + + migration.up() + instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) + refute ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) + assert instance_config.value == [name: "Pleroma"] + end + end + + describe "down/0" do + test "revert new settings to old when instance setting not exists", %{migration: migration} do + insert(:config, + group: :pleroma, + key: :welcome, + value: [ + direct_message: %{ + enabled: true, + message: "Test message", + sender_nickname: "jimm" + }, + email: %{ + enabled: false, + html: "Welcome to <%= instance_name %>", + sender: nil, + subject: "Welcome to <%= instance_name %>", + text: "Welcome to <%= instance_name %>" + } + ] + ) + + migration.down() + + refute ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) + instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) + + assert instance_config.value == [ + welcome_user_nickname: "jimm", + welcome_message: "Test message" + ] + end + + test "revert new settings to old when instance setting exists", %{migration: migration} do + insert(:config, group: :pleroma, key: :instance, value: [name: "Pleroma App"]) + + insert(:config, + group: :pleroma, + key: :welcome, + value: [ + direct_message: %{ + enabled: true, + message: "Test message", + sender_nickname: "jimm" + }, + email: %{ + enabled: false, + html: "Welcome to <%= instance_name %>", + sender: nil, + subject: "Welcome to <%= instance_name %>", + text: "Welcome to <%= instance_name %>" + } + ] + ) + + migration.down() + + refute ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) + instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) + + assert instance_config.value == [ + name: "Pleroma App", + welcome_user_nickname: "jimm", + welcome_message: "Test message" + ] + end + end +end diff --git a/test/pleroma/repo_test.exs b/test/pleroma/repo_test.exs @@ -0,0 +1,80 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.RepoTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.User + + describe "find_resource/1" do + test "returns user" do + user = insert(:user) + query = from(t in User, where: t.id == ^user.id) + assert Repo.find_resource(query) == {:ok, user} + end + + test "returns not_found" do + query = from(t in User, where: t.id == ^"9gBuXNpD2NyDmmxxdw") + assert Repo.find_resource(query) == {:error, :not_found} + end + end + + describe "get_assoc/2" do + test "get assoc from preloaded data" do + user = %User{name: "Agent Smith"} + token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user} + assert Repo.get_assoc(token, :user) == {:ok, user} + end + + test "get one-to-one assoc from repo" do + user = insert(:user, name: "Jimi Hendrix") + token = refresh_record(insert(:oauth_token, user: user)) + + assert Repo.get_assoc(token, :user) == {:ok, user} + end + + test "get one-to-many assoc from repo" do + user = insert(:user) + + notification = + refresh_record(insert(:notification, user: user, activity: insert(:note_activity))) + + assert Repo.get_assoc(user, :notifications) == {:ok, [notification]} + end + + test "return error if has not assoc " do + token = insert(:oauth_token, user: nil) + assert Repo.get_assoc(token, :user) == {:error, :not_found} + end + end + + describe "chunk_stream/3" do + test "fetch records one-by-one" do + users = insert_list(50, :user) + + {fetch_users, 50} = + from(t in User) + |> Repo.chunk_stream(5) + |> Enum.reduce({[], 0}, fn %User{} = user, {acc, count} -> + {acc ++ [user], count + 1} + end) + + assert users == fetch_users + end + + test "fetch records in bulk" do + users = insert_list(50, :user) + + {fetch_users, 10} = + from(t in User) + |> Repo.chunk_stream(5, :batches) + |> Enum.reduce({[], 0}, fn users, {acc, count} -> + {acc ++ users, count + 1} + end) + + assert users == fetch_users + end + end +end diff --git a/test/pleroma/report_note_test.exs b/test/pleroma/report_note_test.exs @@ -0,0 +1,16 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ReportNoteTest do + alias Pleroma.ReportNote + use Pleroma.DataCase + import Pleroma.Factory + + test "create/3" do + user = insert(:user) + report = insert(:report_activity) + assert {:ok, note} = ReportNote.create(user.id, report.id, "naughty boy") + assert note.content == "naughty boy" + end +end diff --git a/test/pleroma/reverse_proxy_test.exs b/test/pleroma/reverse_proxy_test.exs @@ -0,0 +1,336 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ReverseProxyTest do + use Pleroma.Web.ConnCase, async: true + + import ExUnit.CaptureLog + import Mox + + alias Pleroma.ReverseProxy + alias Pleroma.ReverseProxy.ClientMock + alias Plug.Conn + + setup_all do + {:ok, _} = Registry.start_link(keys: :unique, name: ClientMock) + :ok + end + + setup :verify_on_exit! + + defp user_agent_mock(user_agent, invokes) do + json = Jason.encode!(%{"user-agent": user_agent}) + + ClientMock + |> expect(:request, fn :get, url, _, _, _ -> + Registry.register(ClientMock, url, 0) + + {:ok, 200, + [ + {"content-type", "application/json"}, + {"content-length", byte_size(json) |> to_string()} + ], %{url: url}} + end) + |> expect(:stream_body, invokes, fn %{url: url} = client -> + case Registry.lookup(ClientMock, url) do + [{_, 0}] -> + Registry.update_value(ClientMock, url, &(&1 + 1)) + {:ok, json, client} + + [{_, 1}] -> + Registry.unregister(ClientMock, url) + :done + end + end) + end + + describe "reverse proxy" do + test "do not track successful request", %{conn: conn} do + user_agent_mock("hackney/1.15.1", 2) + url = "/success" + + conn = ReverseProxy.call(conn, url) + + assert conn.status == 200 + assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, nil} + end + end + + describe "user-agent" do + test "don't keep", %{conn: conn} do + user_agent_mock("hackney/1.15.1", 2) + conn = ReverseProxy.call(conn, "/user-agent") + assert json_response(conn, 200) == %{"user-agent" => "hackney/1.15.1"} + end + + test "keep", %{conn: conn} do + user_agent_mock(Pleroma.Application.user_agent(), 2) + conn = ReverseProxy.call(conn, "/user-agent-keep", keep_user_agent: true) + assert json_response(conn, 200) == %{"user-agent" => Pleroma.Application.user_agent()} + end + end + + test "closed connection", %{conn: conn} do + ClientMock + |> expect(:request, fn :get, "/closed", _, _, _ -> {:ok, 200, [], %{}} end) + |> expect(:stream_body, fn _ -> {:error, :closed} end) + |> expect(:close, fn _ -> :ok end) + + conn = ReverseProxy.call(conn, "/closed") + assert conn.halted + end + + defp stream_mock(invokes, with_close? \\ false) do + ClientMock + |> expect(:request, fn :get, "/stream-bytes/" <> length, _, _, _ -> + Registry.register(ClientMock, "/stream-bytes/" <> length, 0) + + {:ok, 200, [{"content-type", "application/octet-stream"}], + %{url: "/stream-bytes/" <> length}} + end) + |> expect(:stream_body, invokes, fn %{url: "/stream-bytes/" <> length} = client -> + max = String.to_integer(length) + + case Registry.lookup(ClientMock, "/stream-bytes/" <> length) do + [{_, current}] when current < max -> + Registry.update_value( + ClientMock, + "/stream-bytes/" <> length, + &(&1 + 10) + ) + + {:ok, "0123456789", client} + + [{_, ^max}] -> + Registry.unregister(ClientMock, "/stream-bytes/" <> length) + :done + end + end) + + if with_close? do + expect(ClientMock, :close, fn _ -> :ok end) + end + end + + describe "max_body" do + test "length returns error if content-length more than option", %{conn: conn} do + user_agent_mock("hackney/1.15.1", 0) + + assert capture_log(fn -> + ReverseProxy.call(conn, "/huge-file", max_body_length: 4) + end) =~ + "[error] Elixir.Pleroma.ReverseProxy: request to \"/huge-file\" failed: :body_too_large" + + assert {:ok, true} == Cachex.get(:failed_proxy_url_cache, "/huge-file") + + assert capture_log(fn -> + ReverseProxy.call(conn, "/huge-file", max_body_length: 4) + end) == "" + end + + test "max_body_length returns error if streaming body more than that option", %{conn: conn} do + stream_mock(3, true) + + assert capture_log(fn -> + ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 30) + end) =~ + "[warn] Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large" + end + end + + describe "HEAD requests" do + test "common", %{conn: conn} do + ClientMock + |> expect(:request, fn :head, "/head", _, _, _ -> + {:ok, 200, [{"content-type", "text/html; charset=utf-8"}]} + end) + + conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), "/head") + assert html_response(conn, 200) == "" + end + end + + defp error_mock(status) when is_integer(status) do + ClientMock + |> expect(:request, fn :get, "/status/" <> _, _, _, _ -> + {:error, status} + end) + end + + describe "returns error on" do + test "500", %{conn: conn} do + error_mock(500) + url = "/status/500" + + capture_log(fn -> ReverseProxy.call(conn, url) end) =~ + "[error] Elixir.Pleroma.ReverseProxy: request to /status/500 failed with HTTP status 500" + + assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true} + + {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url) + assert ttl <= 60_000 + end + + test "400", %{conn: conn} do + error_mock(400) + url = "/status/400" + + capture_log(fn -> ReverseProxy.call(conn, url) end) =~ + "[error] Elixir.Pleroma.ReverseProxy: request to /status/400 failed with HTTP status 400" + + assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true} + assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil} + end + + test "403", %{conn: conn} do + error_mock(403) + url = "/status/403" + + capture_log(fn -> + ReverseProxy.call(conn, url, failed_request_ttl: :timer.seconds(120)) + end) =~ + "[error] Elixir.Pleroma.ReverseProxy: request to /status/403 failed with HTTP status 403" + + {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url) + assert ttl > 100_000 + end + + test "204", %{conn: conn} do + url = "/status/204" + expect(ClientMock, :request, fn :get, _url, _, _, _ -> {:ok, 204, [], %{}} end) + + capture_log(fn -> + conn = ReverseProxy.call(conn, url) + assert conn.resp_body == "Request failed: No Content" + assert conn.halted + end) =~ + "[error] Elixir.Pleroma.ReverseProxy: request to \"/status/204\" failed with HTTP status 204" + + assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true} + assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil} + end + end + + test "streaming", %{conn: conn} do + stream_mock(21) + conn = ReverseProxy.call(conn, "/stream-bytes/200") + assert conn.state == :chunked + assert byte_size(conn.resp_body) == 200 + assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"] + end + + defp headers_mock(_) do + ClientMock + |> expect(:request, fn :get, "/headers", headers, _, _ -> + Registry.register(ClientMock, "/headers", 0) + {:ok, 200, [{"content-type", "application/json"}], %{url: "/headers", headers: headers}} + end) + |> expect(:stream_body, 2, fn %{url: url, headers: headers} = client -> + case Registry.lookup(ClientMock, url) do + [{_, 0}] -> + Registry.update_value(ClientMock, url, &(&1 + 1)) + headers = for {k, v} <- headers, into: %{}, do: {String.capitalize(k), v} + {:ok, Jason.encode!(%{headers: headers}), client} + + [{_, 1}] -> + Registry.unregister(ClientMock, url) + :done + end + end) + + :ok + end + + describe "keep request headers" do + setup [:headers_mock] + + test "header passes", %{conn: conn} do + conn = + Conn.put_req_header( + conn, + "accept", + "text/html" + ) + |> ReverseProxy.call("/headers") + + %{"headers" => headers} = json_response(conn, 200) + assert headers["Accept"] == "text/html" + end + + test "header is filtered", %{conn: conn} do + conn = + Conn.put_req_header( + conn, + "accept-language", + "en-US" + ) + |> ReverseProxy.call("/headers") + + %{"headers" => headers} = json_response(conn, 200) + refute headers["Accept-Language"] + end + end + + test "returns 400 on non GET, HEAD requests", %{conn: conn} do + conn = ReverseProxy.call(Map.put(conn, :method, "POST"), "/ip") + assert conn.status == 400 + end + + describe "cache resp headers" do + test "add cache-control", %{conn: conn} do + ClientMock + |> expect(:request, fn :get, "/cache", _, _, _ -> + {:ok, 200, [{"ETag", "some ETag"}], %{}} + end) + |> expect(:stream_body, fn _ -> :done end) + + conn = ReverseProxy.call(conn, "/cache") + assert {"cache-control", "public, max-age=1209600"} in conn.resp_headers + end + end + + defp disposition_headers_mock(headers) do + ClientMock + |> expect(:request, fn :get, "/disposition", _, _, _ -> + Registry.register(ClientMock, "/disposition", 0) + + {:ok, 200, headers, %{url: "/disposition"}} + end) + |> expect(:stream_body, 2, fn %{url: "/disposition"} = client -> + case Registry.lookup(ClientMock, "/disposition") do + [{_, 0}] -> + Registry.update_value(ClientMock, "/disposition", &(&1 + 1)) + {:ok, "", client} + + [{_, 1}] -> + Registry.unregister(ClientMock, "/disposition") + :done + end + end) + end + + describe "response content disposition header" do + test "not atachment", %{conn: conn} do + disposition_headers_mock([ + {"content-type", "image/gif"}, + {"content-length", "0"} + ]) + + conn = ReverseProxy.call(conn, "/disposition") + + assert {"content-type", "image/gif"} in conn.resp_headers + end + + test "with content-disposition header", %{conn: conn} do + disposition_headers_mock([ + {"content-disposition", "attachment; filename=\"filename.jpg\""}, + {"content-length", "0"} + ]) + + conn = ReverseProxy.call(conn, "/disposition") + + assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers + end + end +end diff --git a/test/pleroma/runtime_test.exs b/test/pleroma/runtime_test.exs @@ -0,0 +1,12 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.RuntimeTest do + use ExUnit.Case, async: true + + test "it loads custom runtime modules" do + assert {:module, Fixtures.Modules.RuntimeModule} == + Code.ensure_compiled(Fixtures.Modules.RuntimeModule) + end +end diff --git a/test/pleroma/safe_jsonb_set_test.exs b/test/pleroma/safe_jsonb_set_test.exs @@ -0,0 +1,16 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.SafeJsonbSetTest do + use Pleroma.DataCase + + test "it doesn't wipe the object when asked to set the value to NULL" do + assert %{rows: [[%{"key" => "value", "test" => nil}]]} = + Ecto.Adapters.SQL.query!( + Pleroma.Repo, + "select safe_jsonb_set('{\"key\": \"value\"}'::jsonb, '{test}', NULL);", + [] + ) + end +end diff --git a/test/pleroma/scheduled_activity_test.exs b/test/pleroma/scheduled_activity_test.exs @@ -0,0 +1,105 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ScheduledActivityTest do + use Pleroma.DataCase + alias Pleroma.DataCase + alias Pleroma.ScheduledActivity + import Pleroma.Factory + + setup do: clear_config([ScheduledActivity, :enabled]) + + setup context do + DataCase.ensure_local_uploader(context) + end + + describe "creation" do + test "scheduled activities with jobs when ScheduledActivity enabled" do + Pleroma.Config.put([ScheduledActivity, :enabled], true) + user = insert(:user) + + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + + attrs = %{params: %{}, scheduled_at: today} + {:ok, sa1} = ScheduledActivity.create(user, attrs) + {:ok, sa2} = ScheduledActivity.create(user, attrs) + + jobs = + Repo.all(from(j in Oban.Job, where: j.queue == "scheduled_activities", select: j.args)) + + assert jobs == [%{"activity_id" => sa1.id}, %{"activity_id" => sa2.id}] + end + + test "scheduled activities without jobs when ScheduledActivity disabled" do + Pleroma.Config.put([ScheduledActivity, :enabled], false) + user = insert(:user) + + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + + attrs = %{params: %{}, scheduled_at: today} + {:ok, _sa1} = ScheduledActivity.create(user, attrs) + {:ok, _sa2} = ScheduledActivity.create(user, attrs) + + jobs = + Repo.all(from(j in Oban.Job, where: j.queue == "scheduled_activities", select: j.args)) + + assert jobs == [] + end + + test "when daily user limit is exceeded" do + user = insert(:user) + + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + + attrs = %{params: %{}, scheduled_at: today} + {:ok, _} = ScheduledActivity.create(user, attrs) + {:ok, _} = ScheduledActivity.create(user, attrs) + + {:error, changeset} = ScheduledActivity.create(user, attrs) + assert changeset.errors == [scheduled_at: {"daily limit exceeded", []}] + end + + test "when total user limit is exceeded" do + user = insert(:user) + + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + + tomorrow = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.hours(36), :millisecond) + |> NaiveDateTime.to_iso8601() + + {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: today}) + {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: today}) + {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) + {:error, changeset} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) + assert changeset.errors == [scheduled_at: {"total limit exceeded", []}] + end + + test "when scheduled_at is earlier than 5 minute from now" do + user = insert(:user) + + scheduled_at = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(4), :millisecond) + |> NaiveDateTime.to_iso8601() + + attrs = %{params: %{}, scheduled_at: scheduled_at} + {:error, changeset} = ScheduledActivity.create(user, attrs) + assert changeset.errors == [scheduled_at: {"must be at least 5 minutes from now", []}] + end + end +end diff --git a/test/pleroma/signature_test.exs b/test/pleroma/signature_test.exs @@ -0,0 +1,134 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.SignatureTest do + use Pleroma.DataCase + + import ExUnit.CaptureLog + import Pleroma.Factory + import Tesla.Mock + import Mock + + alias Pleroma.Signature + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + @private_key "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA48qb4v6kqigZutO9Ot0wkp27GIF2LiVaADgxQORZozZR63jH\nTaoOrS3Xhngbgc8SSOhfXET3omzeCLqaLNfXnZ8OXmuhJfJSU6mPUvmZ9QdT332j\nfN/g3iWGhYMf/M9ftCKh96nvFVO/tMruzS9xx7tkrfJjehdxh/3LlJMMImPtwcD7\nkFXwyt1qZTAU6Si4oQAJxRDQXHp1ttLl3Ob829VM7IKkrVmY8TD+JSlV0jtVJPj6\n1J19ytKTx/7UaucYvb9HIiBpkuiy5n/irDqKLVf5QEdZoNCdojOZlKJmTLqHhzKP\n3E9TxsUjhrf4/EqegNc/j982RvOxeu4i40zMQwIDAQABAoIBAQDH5DXjfh21i7b4\ncXJuw0cqget617CDUhemdakTDs9yH+rHPZd3mbGDWuT0hVVuFe4vuGpmJ8c+61X0\nRvugOlBlavxK8xvYlsqTzAmPgKUPljyNtEzQ+gz0I+3mH2jkin2rL3D+SksZZgKm\nfiYMPIQWB2WUF04gB46DDb2mRVuymGHyBOQjIx3WC0KW2mzfoFUFRlZEF+Nt8Ilw\nT+g/u0aZ1IWoszbsVFOEdghgZET0HEarum0B2Je/ozcPYtwmU10iBANGMKdLqaP/\nj954BPunrUf6gmlnLZKIKklJj0advx0NA+cL79+zeVB3zexRYSA5o9q0WPhiuTwR\n/aedWHnBAoGBAP0sDWBAM1Y4TRAf8ZI9PcztwLyHPzfEIqzbObJJnx1icUMt7BWi\n+/RMOnhrlPGE1kMhOqSxvXYN3u+eSmWTqai2sSH5Hdw2EqnrISSTnwNUPINX7fHH\njEkgmXQ6ixE48SuBZnb4w1EjdB/BA6/sjL+FNhggOc87tizLTkMXmMtTAoGBAOZV\n+wPuAMBDBXmbmxCuDIjoVmgSlgeRunB1SA8RCPAFAiUo3+/zEgzW2Oz8kgI+xVwM\n33XkLKrWG1Orhpp6Hm57MjIc5MG+zF4/YRDpE/KNG9qU1tiz0UD5hOpIU9pP4bR/\ngxgPxZzvbk4h5BfHWLpjlk8UUpgk6uxqfti48c1RAoGBALBOKDZ6HwYRCSGMjUcg\n3NPEUi84JD8qmFc2B7Tv7h2he2ykIz9iFAGpwCIyETQsJKX1Ewi0OlNnD3RhEEAy\nl7jFGQ+mkzPSeCbadmcpYlgIJmf1KN/x7fDTAepeBpCEzfZVE80QKbxsaybd3Dp8\nCfwpwWUFtBxr4c7J+gNhAGe/AoGAPn8ZyqkrPv9wXtyfqFjxQbx4pWhVmNwrkBPi\nZ2Qh3q4dNOPwTvTO8vjghvzIyR8rAZzkjOJKVFgftgYWUZfM5gE7T2mTkBYq8W+U\n8LetF+S9qAM2gDnaDx0kuUTCq7t87DKk6URuQ/SbI0wCzYjjRD99KxvChVGPBHKo\n1DjqMuECgYEAgJGNm7/lJCS2wk81whfy/ttKGsEIkyhPFYQmdGzSYC5aDc2gp1R3\nxtOkYEvdjfaLfDGEa4UX8CHHF+w3t9u8hBtcdhMH6GYb9iv6z0VBTt4A/11HUR49\n3Z7TQ18Iyh3jAUCzFV9IJlLIExq5Y7P4B3ojWFBN607sDCt8BMPbDYs=\n-----END RSA PRIVATE KEY-----" + + @public_key "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw0P/Tq4gb4G/QVuMGbJo\nC/AfMNcv+m7NfrlOwkVzcU47jgESuYI4UtJayissCdBycHUnfVUd9qol+eznSODz\nCJhfJloqEIC+aSnuEPGA0POtWad6DU0E6/Ho5zQn5WAWUwbRQqowbrsm/GHo2+3v\neR5jGenwA6sYhINg/c3QQbksyV0uJ20Umyx88w8+TJuv53twOfmyDWuYNoQ3y5cc\nHKOZcLHxYOhvwg3PFaGfFHMFiNmF40dTXt9K96r7sbzc44iLD+VphbMPJEjkMuf8\nPGEFOBzy8pm3wJZw2v32RNW2VESwMYyqDzwHXGSq1a73cS7hEnc79gXlELsK04L9\nQQIDAQAB\n-----END PUBLIC KEY-----\n" + + @rsa_public_key { + :RSAPublicKey, + 24_650_000_183_914_698_290_885_268_529_673_621_967_457_234_469_123_179_408_466_269_598_577_505_928_170_923_974_132_111_403_341_217_239_999_189_084_572_368_839_502_170_501_850_920_051_662_384_964_248_315_257_926_552_945_648_828_895_432_624_227_029_881_278_113_244_073_644_360_744_504_606_177_648_469_825_063_267_913_017_309_199_785_535_546_734_904_379_798_564_556_494_962_268_682_532_371_146_333_972_821_570_577_277_375_020_977_087_539_994_500_097_107_935_618_711_808_260_846_821_077_839_605_098_669_707_417_692_791_905_543_116_911_754_774_323_678_879_466_618_738_207_538_013_885_607_095_203_516_030_057_611_111_308_904_599_045_146_148_350_745_339_208_006_497_478_057_622_336_882_506_112_530_056_970_653_403_292_123_624_453_213_574_011_183_684_739_084_105_206_483_178_943_532_208_537_215_396_831_110_268_758_639_826_369_857, + # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength + 65_537 + } + + defp make_fake_signature(key_id), do: "keyId=\"#{key_id}\"" + + defp make_fake_conn(key_id), + do: %Plug.Conn{req_headers: %{"signature" => make_fake_signature(key_id <> "#main-key")}} + + describe "fetch_public_key/1" do + test "it returns key" do + expected_result = {:ok, @rsa_public_key} + + user = insert(:user, public_key: @public_key) + + assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result + 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" + end + + test "it returns error if public key is nil" do + user = insert(:user, public_key: nil) + + assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == {:error, :error} + end + end + + describe "refetch_public_key/1" do + test "it returns key" do + ap_id = "https://mastodon.social/users/lambadalambda" + + assert Signature.refetch_public_key(make_fake_conn(ap_id)) == {:ok, @rsa_public_key} + end + + test "it returns error when not found user" do + assert capture_log(fn -> + {:error, _} = Signature.refetch_public_key(make_fake_conn("https://test-ap_id")) + end) =~ "[error] Could not decode user" + end + end + + describe "sign/2" do + test "it returns signature headers" do + user = + insert(:user, %{ + ap_id: "https://mastodon.social/users/lambadalambda", + keys: @private_key + }) + + assert Signature.sign( + user, + %{ + host: "test.test", + "content-length": 100 + } + ) == + "keyId=\"https://mastodon.social/users/lambadalambda#main-key\",algorithm=\"rsa-sha256\",headers=\"content-length host\",signature=\"sibUOoqsFfTDerquAkyprxzDjmJm6erYc42W5w1IyyxusWngSinq5ILTjaBxFvfarvc7ci1xAi+5gkBwtshRMWm7S+Uqix24Yg5EYafXRun9P25XVnYBEIH4XQ+wlnnzNIXQkU3PU9e6D8aajDZVp3hPJNeYt1gIPOA81bROI8/glzb1SAwQVGRbqUHHHKcwR8keiR/W2h7BwG3pVRy4JgnIZRSW7fQogKedDg02gzRXwUDFDk0pr2p3q6bUWHUXNV8cZIzlMK+v9NlyFbVYBTHctAR26GIAN6Hz0eV0mAQAePHDY1mXppbA8Gpp6hqaMuYfwifcXmcc+QFm4e+n3A==\"" + end + + test "it returns error" do + user = insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", keys: ""}) + + assert Signature.sign( + user, + %{host: "test.test", "content-length": 100} + ) == {:error, []} + end + end + + describe "key_id_to_actor_id/1" do + test "it properly deduces the actor id for misskey" do + assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") == + {:ok, "https://example.com/users/1234"} + end + + test "it properly deduces the actor id for mastodon and pleroma" 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 + ) do + assert Signature.key_id_to_actor_id("acct:raymoo@gensokyo.2hu") == + {:ok, "https://gensokyo.2hu/users/raymoo"} + end + end + end + + describe "signed_date" do + test "it returns formatted current date" do + with_mock(NaiveDateTime, utc_now: fn -> ~N[2019-08-23 18:11:24.822233] end) do + assert Signature.signed_date() == "Fri, 23 Aug 2019 18:11:24 GMT" + end + end + + test "it returns formatted date" do + assert Signature.signed_date(~N[2019-08-23 08:11:24.822233]) == + "Fri, 23 Aug 2019 08:11:24 GMT" + end + end +end diff --git a/test/pleroma/stats_test.exs b/test/pleroma/stats_test.exs @@ -0,0 +1,122 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.StatsTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Stats + alias Pleroma.Web.CommonAPI + + describe "user count" do + test "it ignores internal users" do + _user = insert(:user, local: true) + _internal = insert(:user, local: true, nickname: nil) + _internal = Pleroma.Web.ActivityPub.Relay.get_actor() + + assert match?(%{stats: %{user_count: 1}}, Stats.calculate_stat_data()) + end + end + + describe "status visibility sum count" do + test "on new status" do + instance2 = "instance2.tld" + user = insert(:user) + other_user = insert(:user, %{ap_id: "https://#{instance2}/@actor"}) + + CommonAPI.post(user, %{visibility: "public", status: "hey"}) + + Enum.each(0..1, fn _ -> + CommonAPI.post(user, %{ + visibility: "unlisted", + status: "hey" + }) + end) + + Enum.each(0..2, fn _ -> + CommonAPI.post(user, %{ + visibility: "direct", + status: "hey @#{other_user.nickname}" + }) + end) + + Enum.each(0..3, fn _ -> + CommonAPI.post(user, %{ + visibility: "private", + status: "hey" + }) + end) + + assert %{"direct" => 3, "private" => 4, "public" => 1, "unlisted" => 2} = + Stats.get_status_visibility_count() + end + + test "on status delete" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{visibility: "public", status: "hey"}) + assert %{"public" => 1} = Stats.get_status_visibility_count() + CommonAPI.delete(activity.id, user) + assert %{"public" => 0} = Stats.get_status_visibility_count() + end + + test "on status visibility update" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{visibility: "public", status: "hey"}) + assert %{"public" => 1, "private" => 0} = Stats.get_status_visibility_count() + {:ok, _} = CommonAPI.update_activity_scope(activity.id, %{visibility: "private"}) + assert %{"public" => 0, "private" => 1} = Stats.get_status_visibility_count() + end + + test "doesn't count unrelated activities" do + user = insert(:user) + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{visibility: "public", status: "hey"}) + _ = CommonAPI.follow(user, other_user) + CommonAPI.favorite(other_user, activity.id) + CommonAPI.repeat(activity.id, other_user) + + assert %{"direct" => 0, "private" => 0, "public" => 1, "unlisted" => 0} = + Stats.get_status_visibility_count() + end + end + + describe "status visibility by instance count" do + test "single instance" do + local_instance = Pleroma.Web.Endpoint.url() |> String.split("//") |> Enum.at(1) + instance2 = "instance2.tld" + user1 = insert(:user) + user2 = insert(:user, %{ap_id: "https://#{instance2}/@actor"}) + + CommonAPI.post(user1, %{visibility: "public", status: "hey"}) + + Enum.each(1..5, fn _ -> + CommonAPI.post(user1, %{ + visibility: "unlisted", + status: "hey" + }) + end) + + Enum.each(1..10, fn _ -> + CommonAPI.post(user1, %{ + visibility: "direct", + status: "hey @#{user2.nickname}" + }) + end) + + Enum.each(1..20, fn _ -> + CommonAPI.post(user2, %{ + visibility: "private", + status: "hey" + }) + end) + + assert %{"direct" => 10, "private" => 0, "public" => 1, "unlisted" => 5} = + Stats.get_status_visibility_count(local_instance) + + assert %{"direct" => 0, "private" => 20, "public" => 0, "unlisted" => 0} = + Stats.get_status_visibility_count(instance2) + end + end +end diff --git a/test/pleroma/upload/filter/anonymize_filename_test.exs b/test/pleroma/upload/filter/anonymize_filename_test.exs @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.Upload + + setup do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + upload_file = %Upload{ + name: "an… image.jpg", + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg") + } + + %{upload_file: upload_file} + end + + setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text]) + + test "it replaces filename on pre-defined text", %{upload_file: upload_file} do + Config.put([Upload.Filter.AnonymizeFilename, :text], "custom-file.png") + {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) + assert name == "custom-file.png" + end + + test "it replaces filename on pre-defined text expression", %{upload_file: upload_file} do + Config.put([Upload.Filter.AnonymizeFilename, :text], "custom-file.{extension}") + {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) + assert name == "custom-file.jpg" + end + + test "it replaces filename on random text", %{upload_file: upload_file} do + {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) + assert <<_::bytes-size(14)>> <> ".jpg" = name + refute name == "an… image.jpg" + end +end diff --git a/test/pleroma/upload/filter/dedupe_test.exs b/test/pleroma/upload/filter/dedupe_test.exs @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.DedupeTest do + use Pleroma.DataCase + + alias Pleroma.Upload + alias Pleroma.Upload.Filter.Dedupe + + @shasum "e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781" + + test "adds shasum" do + File.cp!( + "test/fixtures/image.jpg", + "test/fixtures/image_tmp.jpg" + ) + + upload = %Upload{ + name: "an… image.jpg", + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + tempfile: Path.absname("test/fixtures/image_tmp.jpg") + } + + assert { + :ok, + :filtered, + %Pleroma.Upload{id: @shasum, path: @shasum <> ".jpg"} + } = Dedupe.filter(upload) + end +end diff --git a/test/pleroma/upload/filter/exiftool_test.exs b/test/pleroma/upload/filter/exiftool_test.exs @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.ExiftoolTest do + use Pleroma.DataCase + alias Pleroma.Upload.Filter + + test "apply exiftool filter" do + assert Pleroma.Utils.command_available?("exiftool") + + File.cp!( + "test/fixtures/DSCN0010.jpg", + "test/fixtures/DSCN0010_tmp.jpg" + ) + + upload = %Pleroma.Upload{ + name: "image_with_GPS_data.jpg", + content_type: "image/jpg", + path: Path.absname("test/fixtures/DSCN0010.jpg"), + tempfile: Path.absname("test/fixtures/DSCN0010_tmp.jpg") + } + + assert Filter.Exiftool.filter(upload) == {:ok, :filtered} + + {exif_original, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010.jpg"]) + {exif_filtered, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010_tmp.jpg"]) + + refute exif_original == exif_filtered + assert String.match?(exif_original, ~r/GPS/) + 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.filter(upload) == {:ok, :noop} + end +end diff --git a/test/pleroma/upload/filter/mogrifun_test.exs b/test/pleroma/upload/filter/mogrifun_test.exs @@ -0,0 +1,44 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.MogrifunTest do + use Pleroma.DataCase + import Mock + + alias Pleroma.Upload + alias Pleroma.Upload.Filter + + test "apply mogrify filter" do + File.cp!( + "test/fixtures/image.jpg", + "test/fixtures/image_tmp.jpg" + ) + + upload = %Upload{ + name: "an… image.jpg", + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + tempfile: Path.absname("test/fixtures/image_tmp.jpg") + } + + task = + Task.async(fn -> + assert_receive {:apply_filter, {}}, 4_000 + end) + + with_mocks([ + {Mogrify, [], + [ + open: fn _f -> %Mogrify.Image{} end, + custom: fn _m, _a -> send(task.pid, {:apply_filter, {}}) end, + custom: fn _m, _a, _o -> send(task.pid, {:apply_filter, {}}) end, + save: fn _f, _o -> :ok end + ]} + ]) do + assert Filter.Mogrifun.filter(upload) == {:ok, :filtered} + end + + Task.await(task) + end +end diff --git a/test/pleroma/upload/filter/mogrify_test.exs b/test/pleroma/upload/filter/mogrify_test.exs @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.MogrifyTest do + use Pleroma.DataCase + import Mock + + alias Pleroma.Upload.Filter + + test "apply mogrify filter" do + clear_config(Filter.Mogrify, args: [{"tint", "40"}]) + + File.cp!( + "test/fixtures/image.jpg", + "test/fixtures/image_tmp.jpg" + ) + + upload = %Pleroma.Upload{ + name: "an… image.jpg", + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + tempfile: Path.absname("test/fixtures/image_tmp.jpg") + } + + task = + Task.async(fn -> + assert_receive {:apply_filter, {_, "tint", "40"}}, 4_000 + end) + + with_mock Mogrify, + open: fn _f -> %Mogrify.Image{} end, + custom: fn _m, _a -> :ok end, + custom: fn m, a, o -> send(task.pid, {:apply_filter, {m, a, o}}) end, + save: fn _f, _o -> :ok end do + assert Filter.Mogrify.filter(upload) == {:ok, :filtered} + end + + Task.await(task) + end +end diff --git a/test/pleroma/upload/filter_test.exs b/test/pleroma/upload/filter_test.exs @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.FilterTest do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.Upload.Filter + + setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text]) + + test "applies filters" do + Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], "custom-file.png") + + File.cp!( + "test/fixtures/image.jpg", + "test/fixtures/image_tmp.jpg" + ) + + upload = %Pleroma.Upload{ + name: "an… image.jpg", + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + tempfile: Path.absname("test/fixtures/image_tmp.jpg") + } + + assert Filter.filter([], upload) == {:ok, upload} + + assert {:ok, upload} = Filter.filter([Pleroma.Upload.Filter.AnonymizeFilename], upload) + assert upload.name == "custom-file.png" + end +end diff --git a/test/pleroma/upload_test.exs b/test/pleroma/upload_test.exs @@ -0,0 +1,287 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UploadTest do + use Pleroma.DataCase + + import ExUnit.CaptureLog + + alias Pleroma.Upload + alias Pleroma.Uploaders.Uploader + + @upload_file %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "image.jpg" + } + + defmodule TestUploaderBase do + def put_file(%{path: path} = _upload, module_name) do + task_pid = + Task.async(fn -> + :timer.sleep(10) + + {Uploader, path} + |> :global.whereis_name() + |> send({Uploader, self(), {:test}, %{}}) + + assert_receive {Uploader, {:test}}, 4_000 + end) + + Agent.start(fn -> task_pid end, name: module_name) + + :wait_callback + end + end + + describe "Tried storing a file when http callback response success result" do + defmodule TestUploaderSuccess do + def http_callback(conn, _params), + do: {:ok, conn, {:file, "post-process-file.jpg"}} + + def put_file(upload), do: TestUploaderBase.put_file(upload, __MODULE__) + end + + setup do: [uploader: TestUploaderSuccess] + setup [:ensure_local_uploader] + + 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" + } + ] + }} + + Task.await(Agent.get(TestUploaderSuccess, fn task_pid -> task_pid end)) + end + end + + describe "Tried storing a file when http callback response error" do + defmodule TestUploaderError do + def http_callback(conn, _params), do: {:error, conn, "Errors"} + + def put_file(upload), do: TestUploaderBase.put_file(upload, __MODULE__) + end + + setup do: [uploader: TestUploaderError] + setup [:ensure_local_uploader] + + test "it returns error" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + assert capture_log(fn -> + assert Upload.store(@upload_file) == {:error, "Errors"} + Task.await(Agent.get(TestUploaderError, fn task_pid -> task_pid end)) + end) =~ + "[error] Elixir.Pleroma.Upload store (using Pleroma.UploadTest.TestUploaderError) failed: \"Errors\"" + end + end + + describe "Tried storing a file when http callback doesn't response by timeout" do + defmodule(TestUploader, do: def(put_file(_upload), do: :wait_callback)) + setup do: [uploader: TestUploader] + setup [:ensure_local_uploader] + + test "it returns error" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + assert capture_log(fn -> + assert Upload.store(@upload_file) == {:error, "Uploader callback timeout"} + end) =~ + "[error] Elixir.Pleroma.Upload store (using Pleroma.UploadTest.TestUploader) failed: \"Uploader callback timeout\"" + end + end + + describe "Storing a file with the Local uploader" do + setup [:ensure_local_uploader] + + test "does not allow descriptions longer than the post limit" do + clear_config([:instance, :description_limit], 2) + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "image.jpg" + } + + {:error, :description_too_long} = Upload.store(file, description: "123") + end + + test "returns a media url" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "image.jpg" + } + + {:ok, data} = Upload.store(file) + + assert %{"url" => [%{"href" => url}]} = data + + assert String.starts_with?(url, Pleroma.Web.base_url() <> "/media/") + end + + test "copies the file to the configured folder with deduping" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "an [image.jpg" + } + + {:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.Dedupe]) + + assert List.first(data["url"])["href"] == + Pleroma.Web.base_url() <> + "/media/e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781.jpg" + end + + test "copies the file to the configured folder without deduping" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "an [image.jpg" + } + + {:ok, data} = Upload.store(file) + assert data["name"] == "an [image.jpg" + end + + test "fixes incorrect content type" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "application/octet-stream", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "an [image.jpg" + } + + {:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.Dedupe]) + assert hd(data["url"])["mediaType"] == "image/jpeg" + end + + test "adds missing extension" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "an [image" + } + + {:ok, data} = Upload.store(file) + assert data["name"] == "an [image.jpg" + end + + test "fixes incorrect file extension" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "an [image.blah" + } + + {:ok, data} = Upload.store(file) + assert data["name"] == "an [image.jpg" + end + + test "don't modify filename of an unknown type" do + File.cp("test/fixtures/test.txt", "test/fixtures/test_tmp.txt") + + file = %Plug.Upload{ + content_type: "text/plain", + path: Path.absname("test/fixtures/test_tmp.txt"), + filename: "test.txt" + } + + {:ok, data} = Upload.store(file) + assert data["name"] == "test.txt" + end + + test "copies the file to the configured folder with anonymizing filename" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "an [image.jpg" + } + + {:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.AnonymizeFilename]) + + refute data["name"] == "an [image.jpg" + end + + test "escapes invalid characters in url" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "an… image.jpg" + } + + {:ok, data} = Upload.store(file) + [attachment_url | _] = data["url"] + + assert Path.basename(attachment_url["href"]) == "an%E2%80%A6%20image.jpg" + end + + test "escapes reserved uri characters" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: ":?#[]@!$&\\'()*+,;=.jpg" + } + + {:ok, data} = Upload.store(file) + [attachment_url | _] = data["url"] + + assert Path.basename(attachment_url["href"]) == + "%3A%3F%23%5B%5D%40%21%24%26%5C%27%28%29%2A%2B%2C%3B%3D.jpg" + end + end + + describe "Setting a custom base_url for uploaded media" do + setup do: clear_config([Pleroma.Upload, :base_url], "https://cache.pleroma.social") + + test "returns a media url with configured base_url" do + base_url = Pleroma.Config.get([Pleroma.Upload, :base_url]) + + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "image.jpg" + } + + {:ok, data} = Upload.store(file, base_url: base_url) + + assert %{"url" => [%{"href" => url}]} = data + + refute String.starts_with?(url, base_url <> "/media/") + end + end +end diff --git a/test/pleroma/uploaders/local_test.exs b/test/pleroma/uploaders/local_test.exs @@ -0,0 +1,55 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Uploaders.LocalTest do + use Pleroma.DataCase + alias Pleroma.Uploaders.Local + + describe "get_file/1" do + test "it returns path to local folder for files" do + assert Local.get_file("") == {:ok, {:static_dir, "test/uploads"}} + end + end + + describe "put_file/1" do + test "put file to local folder" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + file_path = "local_upload/files/image.jpg" + + file = %Pleroma.Upload{ + name: "image.jpg", + content_type: "image/jpg", + path: file_path, + tempfile: Path.absname("test/fixtures/image_tmp.jpg") + } + + assert Local.put_file(file) == :ok + + assert Path.join([Local.upload_path(), file_path]) + |> File.exists?() + end + end + + describe "delete_file/1" do + test "deletes local file" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + file_path = "local_upload/files/image.jpg" + + file = %Pleroma.Upload{ + name: "image.jpg", + content_type: "image/jpg", + path: file_path, + tempfile: Path.absname("test/fixtures/image_tmp.jpg") + } + + :ok = Local.put_file(file) + local_path = Path.join([Local.upload_path(), file_path]) + assert File.exists?(local_path) + + Local.delete_file(file_path) + + refute File.exists?(local_path) + end + end +end diff --git a/test/pleroma/uploaders/s3_test.exs b/test/pleroma/uploaders/s3_test.exs @@ -0,0 +1,88 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Uploaders.S3Test do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.Uploaders.S3 + + import Mock + import ExUnit.CaptureLog + + setup do: + clear_config(Pleroma.Uploaders.S3, + bucket: "test_bucket", + public_endpoint: "https://s3.amazonaws.com" + ) + + describe "get_file/1" do + test "it returns path to local folder for files" do + assert S3.get_file("test_image.jpg") == { + :ok, + {:url, "https://s3.amazonaws.com/test_bucket/test_image.jpg"} + } + end + + test "it returns path without bucket when truncated_namespace set to ''" do + Config.put([Pleroma.Uploaders.S3], + bucket: "test_bucket", + public_endpoint: "https://s3.amazonaws.com", + truncated_namespace: "" + ) + + assert S3.get_file("test_image.jpg") == { + :ok, + {:url, "https://s3.amazonaws.com/test_image.jpg"} + } + end + + test "it returns path with bucket namespace when namespace is set" do + Config.put([Pleroma.Uploaders.S3], + bucket: "test_bucket", + public_endpoint: "https://s3.amazonaws.com", + bucket_namespace: "family" + ) + + assert S3.get_file("test_image.jpg") == { + :ok, + {:url, "https://s3.amazonaws.com/family:test_bucket/test_image.jpg"} + } + end + end + + describe "put_file/1" do + setup do + file_upload = %Pleroma.Upload{ + name: "image-tet.jpg", + content_type: "image/jpg", + path: "test_folder/image-tet.jpg", + tempfile: Path.absname("test/instance_static/add/shortcode.png") + } + + [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 + 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 + end + end + + describe "delete_file/1" do + test_with_mock "deletes file", ExAws, request: fn _req -> {:ok, %{status_code: 204}} end do + assert :ok = S3.delete_file("image.jpg") + assert_called(ExAws.request(:_)) + end + end +end diff --git a/test/pleroma/user/import_test.exs b/test/pleroma/user/import_test.exs @@ -0,0 +1,76 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.ImportTest do + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "follow_import" do + test "it imports user followings from list" do + [user1, user2, user3] = insert_list(3, :user) + + identifiers = [ + user2.ap_id, + user3.nickname + ] + + {:ok, job} = User.Import.follow_import(user1, identifiers) + + assert {:ok, result} = ObanHelpers.perform(job) + assert is_list(result) + assert result == [user2, user3] + assert User.following?(user1, user2) + assert User.following?(user1, user3) + end + end + + describe "blocks_import" do + test "it imports user blocks from list" do + [user1, user2, user3] = insert_list(3, :user) + + identifiers = [ + user2.ap_id, + user3.nickname + ] + + {:ok, job} = User.Import.blocks_import(user1, identifiers) + + assert {:ok, result} = ObanHelpers.perform(job) + assert is_list(result) + assert result == [user2, user3] + assert User.blocks?(user1, user2) + assert User.blocks?(user1, user3) + end + end + + describe "mutes_import" do + test "it imports user mutes from list" do + [user1, user2, user3] = insert_list(3, :user) + + identifiers = [ + user2.ap_id, + user3.nickname + ] + + {:ok, job} = User.Import.mutes_import(user1, identifiers) + + assert {:ok, result} = ObanHelpers.perform(job) + assert is_list(result) + assert result == [user2, user3] + assert User.mutes?(user1, user2) + assert User.mutes?(user1, user3) + end + end +end diff --git a/test/pleroma/user/notification_setting_test.exs b/test/pleroma/user/notification_setting_test.exs @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.NotificationSettingTest do + use Pleroma.DataCase + + alias Pleroma.User.NotificationSetting + + describe "changeset/2" do + test "sets option to hide notification contents" do + changeset = + NotificationSetting.changeset( + %NotificationSetting{}, + %{"hide_notification_contents" => true} + ) + + assert %Ecto.Changeset{valid?: true} = changeset + end + end +end diff --git a/test/pleroma/user/query_test.exs b/test/pleroma/user/query_test.exs @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.QueryTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.User.Query + alias Pleroma.Web.ActivityPub.InternalFetchActor + + import Pleroma.Factory + + describe "internal users" do + test "it filters out internal users by default" do + %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor() + + assert [_user] = User |> Repo.all() + assert [] == %{} |> Query.build() |> Repo.all() + end + + test "it filters out users without nickname by default" do + insert(:user, %{nickname: nil}) + + assert [_user] = User |> Repo.all() + assert [] == %{} |> Query.build() |> Repo.all() + end + + test "it returns internal users when enabled" do + %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor() + insert(:user, %{nickname: nil}) + + assert %{internal: true} |> Query.build() |> Repo.aggregate(:count) == 2 + end + end +end diff --git a/test/pleroma/user/welcome_chat_message_test.exs b/test/pleroma/user/welcome_chat_message_test.exs @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.WelcomeChatMessageTest do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.User.WelcomeChatMessage + + import Pleroma.Factory + + setup do: clear_config([:welcome]) + + describe "post_message/1" do + test "send a chat welcome message" do + welcome_user = insert(:user, name: "mewmew") + user = insert(:user) + + Config.put([:welcome, :chat_message, :enabled], true) + Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname) + + Config.put( + [:welcome, :chat_message, :message], + "Hello, welcome to Blob/Cat!" + ) + + {:ok, %Pleroma.Activity{} = activity} = WelcomeChatMessage.post_message(user) + + assert user.ap_id in activity.recipients + assert Pleroma.Object.normalize(activity).data["type"] == "ChatMessage" + assert Pleroma.Object.normalize(activity).data["content"] == "Hello, welcome to Blob/Cat!" + end + end +end diff --git a/test/pleroma/user/welcome_email_test.exs b/test/pleroma/user/welcome_email_test.exs @@ -0,0 +1,61 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.WelcomeEmailTest do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User.WelcomeEmail + + import Pleroma.Factory + import Swoosh.TestAssertions + + setup do: clear_config([:welcome]) + + describe "send_email/1" do + test "send a welcome email" do + user = insert(:user, name: "Jimm") + + Config.put([:welcome, :email, :enabled], true) + Config.put([:welcome, :email, :sender], "welcome@pleroma.app") + + Config.put( + [:welcome, :email, :subject], + "Hello, welcome to pleroma: <%= instance_name %>" + ) + + Config.put( + [:welcome, :email, :html], + "<h1>Hello <%= user.name %>.</h1> <p>Welcome to <%= instance_name %></p>" + ) + + instance_name = Config.get([:instance, :name]) + + {:ok, _job} = WelcomeEmail.send_email(user) + + ObanHelpers.perform_all() + + assert_email_sent( + from: {instance_name, "welcome@pleroma.app"}, + to: {user.name, user.email}, + subject: "Hello, welcome to pleroma: #{instance_name}", + html_body: "<h1>Hello #{user.name}.</h1> <p>Welcome to #{instance_name}</p>" + ) + + Config.put([:welcome, :email, :sender], {"Pleroma App", "welcome@pleroma.app"}) + + {:ok, _job} = WelcomeEmail.send_email(user) + + ObanHelpers.perform_all() + + assert_email_sent( + from: {"Pleroma App", "welcome@pleroma.app"}, + to: {user.name, user.email}, + subject: "Hello, welcome to pleroma: #{instance_name}", + html_body: "<h1>Hello #{user.name}.</h1> <p>Welcome to #{instance_name}</p>" + ) + end + end +end diff --git a/test/pleroma/user/welcome_message_test.exs b/test/pleroma/user/welcome_message_test.exs @@ -0,0 +1,34 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.WelcomeMessageTest do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.User.WelcomeMessage + + import Pleroma.Factory + + setup do: clear_config([:welcome]) + + describe "post_message/1" do + test "send a direct welcome message" do + welcome_user = insert(:user) + user = insert(:user, name: "Jimm") + + Config.put([:welcome, :direct_message, :enabled], true) + Config.put([:welcome, :direct_message, :sender_nickname], welcome_user.nickname) + + Config.put( + [:welcome, :direct_message, :message], + "Hello. Welcome to Pleroma" + ) + + {:ok, %Pleroma.Activity{} = activity} = WelcomeMessage.post_message(user) + assert user.ap_id in activity.recipients + assert activity.data["directMessage"] == true + assert Pleroma.Object.normalize(activity).data["content"] =~ "Hello. Welcome to Pleroma" + end + end +end diff --git a/test/pleroma/user_invite_token_test.exs b/test/pleroma/user_invite_token_test.exs @@ -0,0 +1,96 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UserInviteTokenTest do + use ExUnit.Case, async: true + alias Pleroma.UserInviteToken + + describe "valid_invite?/1 one time invites" do + setup do + invite = %UserInviteToken{invite_type: "one_time"} + + {:ok, invite: invite} + end + + test "not used returns true", %{invite: invite} do + invite = %{invite | used: false} + assert UserInviteToken.valid_invite?(invite) + end + + test "used returns false", %{invite: invite} do + invite = %{invite | used: true} + refute UserInviteToken.valid_invite?(invite) + end + end + + describe "valid_invite?/1 reusable invites" do + setup do + invite = %UserInviteToken{ + invite_type: "reusable", + max_use: 5 + } + + {:ok, invite: invite} + end + + test "with less uses then max use returns true", %{invite: invite} do + invite = %{invite | uses: 4} + assert UserInviteToken.valid_invite?(invite) + end + + test "with equal or more uses then max use returns false", %{invite: invite} do + invite = %{invite | uses: 5} + + refute UserInviteToken.valid_invite?(invite) + + invite = %{invite | uses: 6} + + refute UserInviteToken.valid_invite?(invite) + end + end + + describe "valid_token?/1 date limited invites" do + setup do + invite = %UserInviteToken{invite_type: "date_limited"} + {:ok, invite: invite} + end + + test "expires today returns true", %{invite: invite} do + invite = %{invite | expires_at: Date.utc_today()} + assert UserInviteToken.valid_invite?(invite) + end + + test "expires yesterday returns false", %{invite: invite} do + invite = %{invite | expires_at: Date.add(Date.utc_today(), -1)} + refute UserInviteToken.valid_invite?(invite) + end + end + + describe "valid_token?/1 reusable date limited invites" do + setup do + invite = %UserInviteToken{invite_type: "reusable_date_limited", max_use: 5} + {:ok, invite: invite} + end + + test "not overdue date and less uses returns true", %{invite: invite} do + invite = %{invite | expires_at: Date.utc_today(), uses: 4} + assert UserInviteToken.valid_invite?(invite) + end + + test "overdue date and less uses returns false", %{invite: invite} do + invite = %{invite | expires_at: Date.add(Date.utc_today(), -1)} + refute UserInviteToken.valid_invite?(invite) + end + + test "not overdue date with more uses returns false", %{invite: invite} do + invite = %{invite | expires_at: Date.utc_today(), uses: 5} + refute UserInviteToken.valid_invite?(invite) + end + + test "overdue date with more uses returns false", %{invite: invite} do + invite = %{invite | expires_at: Date.add(Date.utc_today(), -1), uses: 5} + refute UserInviteToken.valid_invite?(invite) + end + end +end diff --git a/test/pleroma/user_relationship_test.exs b/test/pleroma/user_relationship_test.exs @@ -0,0 +1,130 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UserRelationshipTest do + alias Pleroma.UserRelationship + + use Pleroma.DataCase + + import Pleroma.Factory + + describe "*_exists?/2" do + setup do + {:ok, users: insert_list(2, :user)} + end + + test "returns false if record doesn't exist", %{users: [user1, user2]} do + refute UserRelationship.block_exists?(user1, user2) + refute UserRelationship.mute_exists?(user1, user2) + refute UserRelationship.notification_mute_exists?(user1, user2) + refute UserRelationship.reblog_mute_exists?(user1, user2) + refute UserRelationship.inverse_subscription_exists?(user1, user2) + end + + test "returns true if record exists", %{users: [user1, user2]} do + for relationship_type <- [ + :block, + :mute, + :notification_mute, + :reblog_mute, + :inverse_subscription + ] do + insert(:user_relationship, + source: user1, + target: user2, + relationship_type: relationship_type + ) + end + + assert UserRelationship.block_exists?(user1, user2) + assert UserRelationship.mute_exists?(user1, user2) + assert UserRelationship.notification_mute_exists?(user1, user2) + assert UserRelationship.reblog_mute_exists?(user1, user2) + assert UserRelationship.inverse_subscription_exists?(user1, user2) + end + end + + describe "create_*/2" do + setup do + {:ok, users: insert_list(2, :user)} + end + + test "creates user relationship record if it doesn't exist", %{users: [user1, user2]} do + for relationship_type <- [ + :block, + :mute, + :notification_mute, + :reblog_mute, + :inverse_subscription + ] do + insert(:user_relationship, + source: user1, + target: user2, + relationship_type: relationship_type + ) + end + + UserRelationship.create_block(user1, user2) + UserRelationship.create_mute(user1, user2) + UserRelationship.create_notification_mute(user1, user2) + UserRelationship.create_reblog_mute(user1, user2) + UserRelationship.create_inverse_subscription(user1, user2) + + assert UserRelationship.block_exists?(user1, user2) + assert UserRelationship.mute_exists?(user1, user2) + assert UserRelationship.notification_mute_exists?(user1, user2) + assert UserRelationship.reblog_mute_exists?(user1, user2) + assert UserRelationship.inverse_subscription_exists?(user1, user2) + end + + test "if record already exists, returns it", %{users: [user1, user2]} do + user_block = UserRelationship.create_block(user1, user2) + assert user_block == UserRelationship.create_block(user1, user2) + end + end + + describe "delete_*/2" do + setup do + {:ok, users: insert_list(2, :user)} + end + + test "deletes user relationship record if it exists", %{users: [user1, user2]} do + for relationship_type <- [ + :block, + :mute, + :notification_mute, + :reblog_mute, + :inverse_subscription + ] do + insert(:user_relationship, + source: user1, + target: user2, + relationship_type: relationship_type + ) + end + + assert {:ok, %UserRelationship{}} = UserRelationship.delete_block(user1, user2) + assert {:ok, %UserRelationship{}} = UserRelationship.delete_mute(user1, user2) + assert {:ok, %UserRelationship{}} = UserRelationship.delete_notification_mute(user1, user2) + assert {:ok, %UserRelationship{}} = UserRelationship.delete_reblog_mute(user1, user2) + + assert {:ok, %UserRelationship{}} = + UserRelationship.delete_inverse_subscription(user1, user2) + + refute UserRelationship.block_exists?(user1, user2) + refute UserRelationship.mute_exists?(user1, user2) + refute UserRelationship.notification_mute_exists?(user1, user2) + refute UserRelationship.reblog_mute_exists?(user1, user2) + refute UserRelationship.inverse_subscription_exists?(user1, user2) + end + + test "if record does not exist, returns {:ok, nil}", %{users: [user1, user2]} do + assert {:ok, nil} = UserRelationship.delete_block(user1, user2) + assert {:ok, nil} = UserRelationship.delete_mute(user1, user2) + assert {:ok, nil} = UserRelationship.delete_notification_mute(user1, user2) + assert {:ok, nil} = UserRelationship.delete_reblog_mute(user1, user2) + assert {:ok, nil} = UserRelationship.delete_inverse_subscription(user1, user2) + end + end +end diff --git a/test/pleroma/user_search_test.exs b/test/pleroma/user_search_test.exs @@ -0,0 +1,362 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UserSearchTest do + alias Pleroma.Repo + alias Pleroma.User + use Pleroma.DataCase + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "User.search" do + setup do: clear_config([:instance, :limit_to_local_content]) + + test "returns a resolved user as the first result" do + Pleroma.Config.put([:instance, :limit_to_local_content], false) + user = insert(:user, %{nickname: "no_relation", ap_id: "https://lain.com/users/lain"}) + _user = insert(:user, %{nickname: "com_user"}) + + [first_user, _second_user] = User.search("https://lain.com/users/lain", resolve: true) + + assert first_user.id == user.id + end + + test "returns a user with matching ap_id as the first result" do + user = insert(:user, %{nickname: "no_relation", ap_id: "https://lain.com/users/lain"}) + _user = insert(:user, %{nickname: "com_user"}) + + [first_user, _second_user] = User.search("https://lain.com/users/lain") + + assert first_user.id == user.id + end + + test "doesn't die if two users have the same uri" do + insert(:user, %{uri: "https://gensokyo.2hu/@raymoo"}) + insert(:user, %{uri: "https://gensokyo.2hu/@raymoo"}) + assert [_first_user, _second_user] = User.search("https://gensokyo.2hu/@raymoo") + end + + test "returns a user with matching uri as the first result" do + user = + insert(:user, %{ + nickname: "no_relation", + ap_id: "https://lain.com/users/lain", + uri: "https://lain.com/@lain" + }) + + _user = insert(:user, %{nickname: "com_user"}) + + [first_user, _second_user] = User.search("https://lain.com/@lain") + + assert first_user.id == user.id + end + + test "excludes invisible users from results" do + user = insert(:user, %{nickname: "john t1000"}) + insert(:user, %{invisible: true, nickname: "john t800"}) + + [found_user] = User.search("john") + assert found_user.id == user.id + end + + test "excludes users when discoverable is false" do + insert(:user, %{nickname: "john 3000", discoverable: false}) + insert(:user, %{nickname: "john 3001"}) + + users = User.search("john") + assert Enum.count(users) == 1 + end + + test "excludes service actors from results" do + insert(:user, actor_type: "Application", nickname: "user1") + service = insert(:user, actor_type: "Service", nickname: "user2") + person = insert(:user, actor_type: "Person", nickname: "user3") + + assert [found_user1, found_user2] = User.search("user") + assert [found_user1.id, found_user2.id] -- [service.id, person.id] == [] + end + + test "accepts limit parameter" do + Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"})) + assert length(User.search("john", limit: 3)) == 3 + assert length(User.search("john")) == 5 + end + + test "accepts offset parameter" do + Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"})) + assert length(User.search("john", limit: 3)) == 3 + assert length(User.search("john", limit: 3, offset: 3)) == 2 + end + + defp clear_virtual_fields(user) do + Map.merge(user, %{search_rank: nil, search_type: nil}) + end + + test "finds a user by full nickname or its leading fragment" do + user = insert(:user, %{nickname: "john"}) + + Enum.each(["john", "jo", "j"], fn query -> + assert user == + User.search(query) + |> List.first() + |> clear_virtual_fields() + end) + end + + test "finds a user by full name or leading fragment(s) of its words" do + user = insert(:user, %{name: "John Doe"}) + + Enum.each(["John Doe", "JOHN", "doe", "j d", "j", "d"], fn query -> + assert user == + User.search(query) + |> List.first() + |> clear_virtual_fields() + end) + end + + test "matches by leading fragment of user domain" do + user = insert(:user, %{nickname: "arandom@dude.com"}) + insert(:user, %{nickname: "iamthedude"}) + + assert [user.id] == User.search("dud") |> Enum.map(& &1.id) + end + + test "ranks full nickname match higher than full name match" do + nicknamed_user = insert(:user, %{nickname: "hj@shigusegubu.club"}) + named_user = insert(:user, %{nickname: "xyz@sample.com", name: "HJ"}) + + results = User.search("hj") + + assert [nicknamed_user.id, named_user.id] == Enum.map(results, & &1.id) + assert Enum.at(results, 0).search_rank > Enum.at(results, 1).search_rank + end + + test "finds users, considering density of matched tokens" do + u1 = insert(:user, %{name: "Bar Bar plus Word Word"}) + u2 = insert(:user, %{name: "Word Word Bar Bar Bar"}) + + assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id) + end + + test "finds users, boosting ranks of friends and followers" do + u1 = insert(:user) + u2 = insert(:user, %{name: "Doe"}) + follower = insert(:user, %{name: "Doe"}) + friend = insert(:user, %{name: "Doe"}) + + {:ok, follower} = User.follow(follower, u1) + {:ok, u1} = User.follow(u1, friend) + + assert [friend.id, follower.id, u2.id] -- + Enum.map(User.search("doe", resolve: false, for_user: u1), & &1.id) == [] + end + + test "finds followings of user by partial name" do + lizz = insert(:user, %{name: "Lizz"}) + jimi = insert(:user, %{name: "Jimi"}) + following_lizz = insert(:user, %{name: "Jimi Hendrix"}) + following_jimi = insert(:user, %{name: "Lizz Wright"}) + follower_lizz = insert(:user, %{name: "Jimi"}) + + {:ok, lizz} = User.follow(lizz, following_lizz) + {:ok, _jimi} = User.follow(jimi, following_jimi) + {:ok, _follower_lizz} = User.follow(follower_lizz, lizz) + + assert Enum.map(User.search("jimi", following: true, for_user: lizz), & &1.id) == [ + following_lizz.id + ] + + assert User.search("lizz", following: true, for_user: lizz) == [] + end + + test "find local and remote users for authenticated users" do + u1 = insert(:user, %{name: "lain"}) + u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false}) + u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false}) + + results = + "lain" + |> User.search(for_user: u1) + |> Enum.map(& &1.id) + |> Enum.sort() + + assert [u1.id, u2.id, u3.id] == results + end + + test "find only local users for unauthenticated users" do + %{id: id} = insert(:user, %{name: "lain"}) + insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false}) + insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false}) + + assert [%{id: ^id}] = User.search("lain") + end + + test "find only local users for authenticated users when `limit_to_local_content` is `:all`" do + Pleroma.Config.put([:instance, :limit_to_local_content], :all) + + %{id: id} = insert(:user, %{name: "lain"}) + insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false}) + insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false}) + + assert [%{id: ^id}] = User.search("lain") + end + + test "find all users for unauthenticated users when `limit_to_local_content` is `false`" do + Pleroma.Config.put([:instance, :limit_to_local_content], false) + + u1 = insert(:user, %{name: "lain"}) + u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false}) + u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false}) + + results = + "lain" + |> User.search() + |> Enum.map(& &1.id) + |> Enum.sort() + + assert [u1.id, u2.id, u3.id] == results + end + + test "does not yield false-positive matches" do + insert(:user, %{name: "John Doe"}) + + Enum.each(["mary", "a", ""], fn query -> + assert [] == User.search(query) + end) + end + + test "works with URIs" do + user = insert(:user) + + results = + User.search("http://mastodon.example.org/users/admin", resolve: true, for_user: user) + + result = results |> List.first() + + user = User.get_cached_by_ap_id("http://mastodon.example.org/users/admin") + + assert length(results) == 1 + + expected = + result + |> Map.put(:search_rank, nil) + |> Map.put(:search_type, nil) + |> Map.put(:last_digest_emailed_at, nil) + |> Map.put(:multi_factor_authentication_settings, nil) + |> Map.put(:notification_settings, nil) + + assert user == expected + end + + test "excludes a blocked users from search result" do + user = insert(:user, %{nickname: "Bill"}) + + [blocked_user | users] = Enum.map(0..3, &insert(:user, %{nickname: "john#{&1}"})) + + blocked_user2 = + insert( + :user, + %{nickname: "john awful", ap_id: "https://awful-and-rude-instance.com/user/bully"} + ) + + User.block_domain(user, "awful-and-rude-instance.com") + User.block(user, blocked_user) + + account_ids = User.search("john", for_user: refresh_record(user)) |> collect_ids + + assert account_ids == collect_ids(users) + refute Enum.member?(account_ids, blocked_user.id) + refute Enum.member?(account_ids, blocked_user2.id) + assert length(account_ids) == 3 + end + + test "local user has the same search_rank as for users with the same nickname, but another domain" do + user = insert(:user) + insert(:user, nickname: "lain@mastodon.social") + insert(:user, nickname: "lain") + insert(:user, nickname: "lain@pleroma.social") + + assert User.search("lain@localhost", resolve: true, for_user: user) + |> Enum.each(fn u -> u.search_rank == 0.5 end) + end + + test "localhost is the part of the domain" do + user = insert(:user) + insert(:user, nickname: "another@somedomain") + insert(:user, nickname: "lain") + insert(:user, nickname: "lain@examplelocalhost") + + result = User.search("lain@examplelocalhost", resolve: true, for_user: user) + assert Enum.each(result, fn u -> u.search_rank == 0.5 end) + assert length(result) == 2 + end + + test "local user search with users" do + user = insert(:user) + local_user = insert(:user, nickname: "lain") + insert(:user, nickname: "another@localhost.com") + insert(:user, nickname: "localhost@localhost.com") + + [result] = User.search("lain@localhost", resolve: true, for_user: user) + assert Map.put(result, :search_rank, nil) |> Map.put(:search_type, nil) == local_user + end + + test "works with idna domains" do + user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな"))) + + results = User.search("lain@zetsubou.みんな", resolve: false, for_user: user) + + result = List.first(results) + + assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) + end + + test "works with idna domains converted input" do + user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな"))) + + results = + User.search("lain@zetsubou." <> to_string(:idna.encode("zetsubou.みんな")), + resolve: false, + for_user: user + ) + + result = List.first(results) + + assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) + end + + test "works with idna domains and bad chars in domain" do + user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな"))) + + results = + User.search("lain@zetsubou!@#$%^&*()+,-/:;<=>?[]'_{}|~`.みんな", + resolve: false, + for_user: user + ) + + result = List.first(results) + + assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) + end + + test "works with idna domains and query as link" do + user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな"))) + + results = + User.search("https://zetsubou.みんな/users/lain", + resolve: false, + for_user: user + ) + + result = List.first(results) + + assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) + end + end +end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs @@ -0,0 +1,2124 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UserTest do + alias Pleroma.Activity + alias Pleroma.Builders.UserBuilder + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + import ExUnit.CaptureLog + import Swoosh.TestAssertions + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup do: clear_config([:instance, :account_activation_required]) + + describe "service actors" do + test "returns updated invisible actor" do + uri = "#{Pleroma.Web.Endpoint.url()}/relay" + followers_uri = "#{uri}/followers" + + insert( + :user, + %{ + nickname: "relay", + invisible: false, + local: true, + ap_id: uri, + follower_address: followers_uri + } + ) + + actor = User.get_or_create_service_actor_by_ap_id(uri, "relay") + assert actor.invisible + end + + test "returns relay user" do + uri = "#{Pleroma.Web.Endpoint.url()}/relay" + followers_uri = "#{uri}/followers" + + assert %User{ + nickname: "relay", + invisible: true, + local: true, + ap_id: ^uri, + follower_address: ^followers_uri + } = User.get_or_create_service_actor_by_ap_id(uri, "relay") + + assert capture_log(fn -> + refute User.get_or_create_service_actor_by_ap_id("/relay", "relay") + end) =~ "Cannot create service actor:" + end + + test "returns invisible actor" do + uri = "#{Pleroma.Web.Endpoint.url()}/internal/fetch-test" + followers_uri = "#{uri}/followers" + user = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test") + + assert %User{ + nickname: "internal.fetch-test", + invisible: true, + local: true, + ap_id: ^uri, + follower_address: ^followers_uri + } = user + + user2 = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test") + assert user.id == user2.id + end + end + + describe "AP ID user relationships" do + setup do + {:ok, user: insert(:user)} + end + + test "outgoing_relationships_ap_ids/1", %{user: user} do + rel_types = [:block, :mute, :notification_mute, :reblog_mute, :inverse_subscription] + + ap_ids_by_rel = + Enum.into( + rel_types, + %{}, + fn rel_type -> + rel_records = + insert_list(2, :user_relationship, %{source: user, relationship_type: rel_type}) + + ap_ids = Enum.map(rel_records, fn rr -> Repo.preload(rr, :target).target.ap_id end) + {rel_type, Enum.sort(ap_ids)} + end + ) + + assert ap_ids_by_rel[:block] == Enum.sort(User.blocked_users_ap_ids(user)) + assert ap_ids_by_rel[:block] == Enum.sort(Enum.map(User.blocked_users(user), & &1.ap_id)) + + assert ap_ids_by_rel[:mute] == Enum.sort(User.muted_users_ap_ids(user)) + assert ap_ids_by_rel[:mute] == Enum.sort(Enum.map(User.muted_users(user), & &1.ap_id)) + + assert ap_ids_by_rel[:notification_mute] == + Enum.sort(User.notification_muted_users_ap_ids(user)) + + assert ap_ids_by_rel[:notification_mute] == + Enum.sort(Enum.map(User.notification_muted_users(user), & &1.ap_id)) + + assert ap_ids_by_rel[:reblog_mute] == Enum.sort(User.reblog_muted_users_ap_ids(user)) + + assert ap_ids_by_rel[:reblog_mute] == + Enum.sort(Enum.map(User.reblog_muted_users(user), & &1.ap_id)) + + assert ap_ids_by_rel[:inverse_subscription] == Enum.sort(User.subscriber_users_ap_ids(user)) + + assert ap_ids_by_rel[:inverse_subscription] == + Enum.sort(Enum.map(User.subscriber_users(user), & &1.ap_id)) + + outgoing_relationships_ap_ids = User.outgoing_relationships_ap_ids(user, rel_types) + + assert ap_ids_by_rel == + Enum.into(outgoing_relationships_ap_ids, %{}, fn {k, v} -> {k, Enum.sort(v)} end) + end + end + + describe "when tags are nil" do + test "tagging a user" do + user = insert(:user, %{tags: nil}) + user = User.tag(user, ["cool", "dude"]) + + assert "cool" in user.tags + assert "dude" in user.tags + end + + test "untagging a user" do + user = insert(:user, %{tags: nil}) + user = User.untag(user, ["cool", "dude"]) + + assert user.tags == [] + end + end + + test "ap_id returns the activity pub id for the user" do + user = UserBuilder.build() + + expected_ap_id = "#{Pleroma.Web.base_url()}/users/#{user.nickname}" + + assert expected_ap_id == User.ap_id(user) + end + + test "ap_followers returns the followers collection for the user" do + user = UserBuilder.build() + + expected_followers_collection = "#{User.ap_id(user)}/followers" + + assert expected_followers_collection == User.ap_followers(user) + end + + test "ap_following returns the following collection for the user" do + user = UserBuilder.build() + + expected_followers_collection = "#{User.ap_id(user)}/following" + + assert expected_followers_collection == User.ap_following(user) + end + + test "returns all pending follow requests" do + unlocked = insert(:user) + locked = insert(:user, locked: true) + follower = insert(:user) + + CommonAPI.follow(follower, unlocked) + CommonAPI.follow(follower, locked) + + assert [] = User.get_follow_requests(unlocked) + assert [activity] = User.get_follow_requests(locked) + + assert activity + end + + test "doesn't return already accepted or duplicate follow requests" do + locked = insert(:user, locked: true) + pending_follower = insert(:user) + accepted_follower = insert(:user) + + CommonAPI.follow(pending_follower, locked) + CommonAPI.follow(pending_follower, locked) + CommonAPI.follow(accepted_follower, locked) + + Pleroma.FollowingRelationship.update(accepted_follower, locked, :follow_accept) + + assert [^pending_follower] = User.get_follow_requests(locked) + end + + test "doesn't return follow requests for deactivated accounts" do + locked = insert(:user, locked: true) + pending_follower = insert(:user, %{deactivated: true}) + + CommonAPI.follow(pending_follower, locked) + + assert true == pending_follower.deactivated + assert [] = User.get_follow_requests(locked) + end + + test "clears follow requests when requester is blocked" do + followed = insert(:user, locked: true) + follower = insert(:user) + + CommonAPI.follow(follower, followed) + assert [_activity] = User.get_follow_requests(followed) + + {:ok, _user_relationship} = User.block(followed, follower) + assert [] = User.get_follow_requests(followed) + end + + test "follow_all follows mutliple users" do + user = insert(:user) + followed_zero = insert(:user) + followed_one = insert(:user) + followed_two = insert(:user) + blocked = insert(:user) + not_followed = insert(:user) + reverse_blocked = insert(:user) + + {:ok, _user_relationship} = User.block(user, blocked) + {:ok, _user_relationship} = User.block(reverse_blocked, user) + + {:ok, user} = User.follow(user, followed_zero) + + {:ok, user} = User.follow_all(user, [followed_one, followed_two, blocked, reverse_blocked]) + + assert User.following?(user, followed_one) + assert User.following?(user, followed_two) + assert User.following?(user, followed_zero) + refute User.following?(user, not_followed) + refute User.following?(user, blocked) + refute User.following?(user, reverse_blocked) + end + + test "follow_all follows mutliple users without duplicating" do + user = insert(:user) + followed_zero = insert(:user) + followed_one = insert(:user) + followed_two = insert(:user) + + {:ok, user} = User.follow_all(user, [followed_zero, followed_one]) + assert length(User.following(user)) == 3 + + {:ok, user} = User.follow_all(user, [followed_one, followed_two]) + assert length(User.following(user)) == 4 + end + + test "follow takes a user and another user" do + user = insert(:user) + followed = insert(:user) + + {:ok, user} = User.follow(user, followed) + + user = User.get_cached_by_id(user.id) + followed = User.get_cached_by_ap_id(followed.ap_id) + + assert followed.follower_count == 1 + assert user.following_count == 1 + + assert User.ap_followers(followed) in User.following(user) + end + + test "can't follow a deactivated users" do + user = insert(:user) + followed = insert(:user, %{deactivated: true}) + + {:error, _} = User.follow(user, followed) + end + + test "can't follow a user who blocked us" do + blocker = insert(:user) + blockee = insert(:user) + + {:ok, _user_relationship} = User.block(blocker, blockee) + + {:error, _} = User.follow(blockee, blocker) + end + + test "can't subscribe to a user who blocked us" do + blocker = insert(:user) + blocked = insert(:user) + + {:ok, _user_relationship} = User.block(blocker, blocked) + + {:error, _} = User.subscribe(blocked, blocker) + end + + test "local users do not automatically follow local locked accounts" do + follower = insert(:user, locked: true) + followed = insert(:user, locked: true) + + {:ok, follower} = User.maybe_direct_follow(follower, followed) + + refute User.following?(follower, followed) + end + + describe "unfollow/2" do + setup do: clear_config([:instance, :external_user_synchronization]) + + test "unfollow with syncronizes external user" do + Pleroma.Config.put([:instance, :external_user_synchronization], true) + + followed = + insert(:user, + nickname: "fuser1", + follower_address: "http://localhost:4001/users/fuser1/followers", + following_address: "http://localhost:4001/users/fuser1/following", + ap_id: "http://localhost:4001/users/fuser1" + ) + + user = + insert(:user, %{ + local: false, + nickname: "fuser2", + ap_id: "http://localhost:4001/users/fuser2", + follower_address: "http://localhost:4001/users/fuser2/followers", + following_address: "http://localhost:4001/users/fuser2/following" + }) + + {:ok, user} = User.follow(user, followed, :follow_accept) + + {:ok, user, _activity} = User.unfollow(user, followed) + + user = User.get_cached_by_id(user.id) + + assert User.following(user) == [] + end + + test "unfollow takes a user and another user" do + followed = insert(:user) + user = insert(:user) + + {:ok, user} = User.follow(user, followed, :follow_accept) + + assert User.following(user) == [user.follower_address, followed.follower_address] + + {:ok, user, _activity} = User.unfollow(user, followed) + + assert User.following(user) == [user.follower_address] + end + + test "unfollow doesn't unfollow yourself" do + user = insert(:user) + + {:error, _} = User.unfollow(user, user) + + assert User.following(user) == [user.follower_address] + end + end + + test "test if a user is following another user" do + followed = insert(:user) + user = insert(:user) + User.follow(user, followed, :follow_accept) + + assert User.following?(user, followed) + refute User.following?(followed, user) + end + + test "fetches correct profile for nickname beginning with number" do + # Use old-style integer ID to try to reproduce the problem + user = insert(:user, %{id: 1080}) + user_with_numbers = insert(:user, %{nickname: "#{user.id}garbage"}) + assert user_with_numbers == User.get_cached_by_nickname_or_id(user_with_numbers.nickname) + end + + describe "user registration" do + @full_user_data %{ + bio: "A guy", + name: "my name", + nickname: "nick", + password: "test", + password_confirmation: "test", + email: "email@example.com" + } + + setup do: clear_config([:instance, :autofollowed_nicknames]) + setup do: clear_config([:welcome]) + setup do: clear_config([:instance, :account_activation_required]) + + test "it autofollows accounts that are set for it" do + user = insert(:user) + remote_user = insert(:user, %{local: false}) + + Pleroma.Config.put([:instance, :autofollowed_nicknames], [ + user.nickname, + remote_user.nickname + ]) + + cng = User.register_changeset(%User{}, @full_user_data) + + {:ok, registered_user} = User.register(cng) + + assert User.following?(registered_user, user) + refute User.following?(registered_user, remote_user) + end + + test "it sends a welcome message if it is set" do + welcome_user = insert(:user) + Pleroma.Config.put([:welcome, :direct_message, :enabled], true) + Pleroma.Config.put([:welcome, :direct_message, :sender_nickname], welcome_user.nickname) + Pleroma.Config.put([:welcome, :direct_message, :message], "Hello, this is a direct message") + + cng = User.register_changeset(%User{}, @full_user_data) + {:ok, registered_user} = User.register(cng) + ObanHelpers.perform_all() + + activity = Repo.one(Pleroma.Activity) + assert registered_user.ap_id in activity.recipients + assert Object.normalize(activity).data["content"] =~ "direct message" + assert activity.actor == welcome_user.ap_id + end + + test "it sends a welcome chat message if it is set" do + welcome_user = insert(:user) + Pleroma.Config.put([:welcome, :chat_message, :enabled], true) + Pleroma.Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname) + Pleroma.Config.put([:welcome, :chat_message, :message], "Hello, this is a chat message") + + cng = User.register_changeset(%User{}, @full_user_data) + {:ok, registered_user} = User.register(cng) + ObanHelpers.perform_all() + + activity = Repo.one(Pleroma.Activity) + assert registered_user.ap_id in activity.recipients + assert Object.normalize(activity).data["content"] =~ "chat message" + assert activity.actor == welcome_user.ap_id + end + + setup do: + clear_config(:mrf_simple, + media_removal: [], + media_nsfw: [], + federated_timeline_removal: [], + report_removal: [], + reject: [], + followers_only: [], + accept: [], + avatar_removal: [], + banner_removal: [], + reject_deletes: [] + ) + + 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 + Pleroma.Config.put([:mrf_simple, :media_nsfw], ["localhost"]) + + welcome_user = insert(:user) + Pleroma.Config.put([:welcome, :chat_message, :enabled], true) + Pleroma.Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname) + Pleroma.Config.put([:welcome, :chat_message, :message], "Hello, this is a chat message") + + cng = User.register_changeset(%User{}, @full_user_data) + {:ok, registered_user} = User.register(cng) + ObanHelpers.perform_all() + + activity = Repo.one(Pleroma.Activity) + assert registered_user.ap_id in activity.recipients + assert Object.normalize(activity).data["content"] =~ "chat message" + assert activity.actor == welcome_user.ap_id + end + + test "it sends a welcome email message if it is set" do + welcome_user = insert(:user) + Pleroma.Config.put([:welcome, :email, :enabled], true) + Pleroma.Config.put([:welcome, :email, :sender], welcome_user.email) + + Pleroma.Config.put( + [:welcome, :email, :subject], + "Hello, welcome to cool site: <%= instance_name %>" + ) + + instance_name = Pleroma.Config.get([:instance, :name]) + + cng = User.register_changeset(%User{}, @full_user_data) + {:ok, registered_user} = User.register(cng) + ObanHelpers.perform_all() + + assert_email_sent( + from: {instance_name, welcome_user.email}, + to: {registered_user.name, registered_user.email}, + subject: "Hello, welcome to cool site: #{instance_name}", + html_body: "Welcome to #{instance_name}" + ) + end + + test "it sends a confirm email" do + Pleroma.Config.put([:instance, :account_activation_required], true) + + cng = User.register_changeset(%User{}, @full_user_data) + {:ok, registered_user} = User.register(cng) + ObanHelpers.perform_all() + + Pleroma.Emails.UserEmail.account_confirmation_email(registered_user) + # temporary hackney fix until hackney max_connections bug is fixed + # https://git.pleroma.social/pleroma/pleroma/-/issues/2101 + |> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]]) + |> assert_email_sent() + end + + test "it requires an email, name, nickname and password, bio is optional when account_activation_required is enabled" do + Pleroma.Config.put([:instance, :account_activation_required], true) + + @full_user_data + |> Map.keys() + |> Enum.each(fn key -> + params = Map.delete(@full_user_data, key) + changeset = User.register_changeset(%User{}, params) + + assert if key == :bio, do: changeset.valid?, else: not changeset.valid? + end) + end + + test "it requires an name, nickname and password, bio and email are optional when account_activation_required is disabled" do + Pleroma.Config.put([:instance, :account_activation_required], false) + + @full_user_data + |> Map.keys() + |> Enum.each(fn key -> + params = Map.delete(@full_user_data, key) + changeset = User.register_changeset(%User{}, params) + + assert if key in [:bio, :email], do: changeset.valid?, else: not changeset.valid? + end) + end + + test "it restricts certain nicknames" do + [restricted_name | _] = Pleroma.Config.get([User, :restricted_nicknames]) + + assert is_bitstring(restricted_name) + + params = + @full_user_data + |> Map.put(:nickname, restricted_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"]) + + # Block with 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) + refute changeset.valid? + + # Pass with different domains that are similar + params = Map.put(@full_user_data, :email, "troll@gnomestrolling.world") + changeset = User.register_changeset(%User{}, params) + assert changeset.valid? + + params = Map.put(@full_user_data, :email, "troll@trolling.world.us") + changeset = User.register_changeset(%User{}, params) + assert changeset.valid? + end + + test "it sets the password_hash and ap_id" do + changeset = User.register_changeset(%User{}, @full_user_data) + + assert changeset.valid? + + assert is_binary(changeset.changes[:password_hash]) + 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 + + test "it sets the 'accepts_chat_messages' set to true" do + changeset = User.register_changeset(%User{}, @full_user_data) + assert changeset.valid? + + {:ok, user} = Repo.insert(changeset) + + assert user.accepts_chat_messages + end + + test "it creates a confirmed user" do + changeset = User.register_changeset(%User{}, @full_user_data) + assert changeset.valid? + + {:ok, user} = Repo.insert(changeset) + + refute user.confirmation_pending + end + end + + describe "user registration, with :account_activation_required" do + @full_user_data %{ + bio: "A guy", + name: "my name", + nickname: "nick", + password: "test", + password_confirmation: "test", + email: "email@example.com" + } + setup do: clear_config([:instance, :account_activation_required], true) + + test "it creates unconfirmed user" do + changeset = User.register_changeset(%User{}, @full_user_data) + assert changeset.valid? + + {:ok, user} = Repo.insert(changeset) + + assert user.confirmation_pending + assert user.confirmation_token + end + + test "it creates confirmed user if :confirmed option is given" do + changeset = User.register_changeset(%User{}, @full_user_data, need_confirmation: false) + assert changeset.valid? + + {:ok, user} = Repo.insert(changeset) + + refute user.confirmation_pending + refute user.confirmation_token + end + end + + describe "user registration, with :account_approval_required" do + @full_user_data %{ + bio: "A guy", + name: "my name", + nickname: "nick", + password: "test", + password_confirmation: "test", + email: "email@example.com", + registration_reason: "I'm a cool guy :)" + } + setup do: clear_config([:instance, :account_approval_required], true) + + test "it creates unapproved user" do + changeset = User.register_changeset(%User{}, @full_user_data) + assert changeset.valid? + + {:ok, user} = Repo.insert(changeset) + + assert user.approval_pending + assert user.registration_reason == "I'm a cool guy :)" + end + + test "it restricts length of registration reason" do + reason_limit = Pleroma.Config.get([:instance, :registration_reason_length]) + + assert is_integer(reason_limit) + + params = + @full_user_data + |> Map.put( + :registration_reason, + "Quia et nesciunt dolores numquam ipsam nisi sapiente soluta. Ullam repudiandae nisi quam porro officiis officiis ad. Consequatur animi velit ex quia. Odit voluptatem perferendis quia ut nisi. Dignissimos sit soluta atque aliquid dolorem ut dolorum ut. Labore voluptates iste iusto amet voluptatum earum. Ad fugit illum nam eos ut nemo. Pariatur ea fuga non aspernatur. Dignissimos debitis officia corporis est nisi ab et. Atque itaque alias eius voluptas minus. Accusamus numquam tempore occaecati in." + ) + + changeset = User.register_changeset(%User{}, params) + + refute changeset.valid? + end + end + + describe "get_or_fetch/1" do + test "gets an existing user by nickname" do + user = insert(:user) + {:ok, fetched_user} = User.get_or_fetch(user.nickname) + + assert user == fetched_user + end + + test "gets an existing user by ap_id" do + ap_id = "http://mastodon.example.org/users/admin" + + user = + insert( + :user, + local: false, + nickname: "admin@mastodon.example.org", + ap_id: ap_id + ) + + {:ok, fetched_user} = User.get_or_fetch(ap_id) + freshed_user = refresh_record(user) + assert freshed_user == fetched_user + end + end + + describe "fetching a user from nickname or trying to build one" do + test "gets an existing user" do + user = insert(:user) + {:ok, fetched_user} = User.get_or_fetch_by_nickname(user.nickname) + + assert user == fetched_user + end + + test "gets an existing user, case insensitive" do + user = insert(:user, nickname: "nick") + {:ok, fetched_user} = User.get_or_fetch_by_nickname("NICK") + + assert user == fetched_user + end + + test "gets an existing user by fully qualified nickname" do + user = insert(:user) + + {:ok, fetched_user} = + User.get_or_fetch_by_nickname(user.nickname <> "@" <> Pleroma.Web.Endpoint.host()) + + assert user == fetched_user + end + + test "gets an existing user by fully qualified nickname, case insensitive" do + user = insert(:user, nickname: "nick") + casing_altered_fqn = String.upcase(user.nickname <> "@" <> Pleroma.Web.Endpoint.host()) + + {:ok, fetched_user} = User.get_or_fetch_by_nickname(casing_altered_fqn) + + assert user == fetched_user + end + + @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" + 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" + end + + test "updates an existing user, if stale" do + a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) + + orig_user = + insert( + :user, + local: false, + nickname: "admin@mastodon.example.org", + ap_id: "http://mastodon.example.org/users/admin", + last_refreshed_at: a_week_ago + ) + + assert orig_user.last_refreshed_at == a_week_ago + + {:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin") + + assert user.inbox + + refute user.last_refreshed_at == orig_user.last_refreshed_at + end + + test "if nicknames clash, the old user gets a prefix with the old id to the nickname" do + a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) + + orig_user = + insert( + :user, + local: false, + nickname: "admin@mastodon.example.org", + ap_id: "http://mastodon.example.org/users/harinezumigari", + last_refreshed_at: a_week_ago + ) + + assert orig_user.last_refreshed_at == a_week_ago + + {:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin") + + assert user.inbox + + refute user.id == orig_user.id + + orig_user = User.get_by_id(orig_user.id) + + assert orig_user.nickname == "#{orig_user.id}.admin@mastodon.example.org" + end + + @tag capture_log: true + test "it returns the old user if stale, but unfetchable" do + a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) + + orig_user = + insert( + :user, + local: false, + nickname: "admin@mastodon.example.org", + ap_id: "http://mastodon.example.org/users/raymoo", + last_refreshed_at: a_week_ago + ) + + assert orig_user.last_refreshed_at == a_week_ago + + {:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/raymoo") + + assert user.last_refreshed_at == orig_user.last_refreshed_at + end + end + + test "returns an ap_id for a user" do + user = insert(:user) + + assert User.ap_id(user) == + Pleroma.Web.Router.Helpers.user_feed_url( + Pleroma.Web.Endpoint, + :feed_redirect, + user.nickname + ) + end + + test "returns an ap_followers link for a user" do + user = insert(:user) + + assert User.ap_followers(user) == + Pleroma.Web.Router.Helpers.user_feed_url( + Pleroma.Web.Endpoint, + :feed_redirect, + user.nickname + ) <> "/followers" + end + + describe "remote user changeset" do + @valid_remote %{ + bio: "hello", + name: "Someone", + nickname: "a@b.de", + ap_id: "http...", + avatar: %{some: "avatar"} + } + setup do: clear_config([:instance, :user_bio_length]) + setup do: clear_config([:instance, :user_name_length]) + + test "it confirms validity" do + cs = User.remote_user_changeset(@valid_remote) + assert cs.valid? + end + + test "it sets the follower_adress" do + cs = User.remote_user_changeset(@valid_remote) + # remote users get a fake local follower address + assert cs.changes.follower_address == + User.ap_followers(%User{nickname: @valid_remote[:nickname]}) + end + + test "it enforces the fqn format for nicknames" do + cs = User.remote_user_changeset(%{@valid_remote | nickname: "bla"}) + assert Ecto.Changeset.get_field(cs, :local) == false + assert cs.changes.avatar + refute cs.valid? + end + + test "it has required fields" do + [:ap_id] + |> Enum.each(fn field -> + cs = User.remote_user_changeset(Map.delete(@valid_remote, field)) + refute cs.valid? + end) + end + end + + describe "followers and friends" do + test "gets all followers for a given user" do + user = insert(:user) + follower_one = insert(:user) + follower_two = insert(:user) + not_follower = insert(:user) + + {:ok, follower_one} = User.follow(follower_one, user) + {:ok, follower_two} = User.follow(follower_two, user) + + res = User.get_followers(user) + + assert Enum.member?(res, follower_one) + assert Enum.member?(res, follower_two) + refute Enum.member?(res, not_follower) + end + + test "gets all friends (followed users) for a given user" do + user = insert(:user) + followed_one = insert(:user) + followed_two = insert(:user) + not_followed = insert(:user) + + {:ok, user} = User.follow(user, followed_one) + {:ok, user} = User.follow(user, followed_two) + + res = User.get_friends(user) + + followed_one = User.get_cached_by_ap_id(followed_one.ap_id) + followed_two = User.get_cached_by_ap_id(followed_two.ap_id) + assert Enum.member?(res, followed_one) + assert Enum.member?(res, followed_two) + refute Enum.member?(res, not_followed) + end + end + + describe "updating note and follower count" do + test "it sets the note_count property" do + note = insert(:note) + + user = User.get_cached_by_ap_id(note.data["actor"]) + + assert user.note_count == 0 + + {:ok, user} = User.update_note_count(user) + + assert user.note_count == 1 + end + + test "it increases the note_count property" do + note = insert(:note) + user = User.get_cached_by_ap_id(note.data["actor"]) + + assert user.note_count == 0 + + {:ok, user} = User.increase_note_count(user) + + assert user.note_count == 1 + + {:ok, user} = User.increase_note_count(user) + + assert user.note_count == 2 + end + + test "it decreases the note_count property" do + note = insert(:note) + user = User.get_cached_by_ap_id(note.data["actor"]) + + assert user.note_count == 0 + + {:ok, user} = User.increase_note_count(user) + + assert user.note_count == 1 + + {:ok, user} = User.decrease_note_count(user) + + assert user.note_count == 0 + + {:ok, user} = User.decrease_note_count(user) + + assert user.note_count == 0 + end + + test "it sets the follower_count property" do + user = insert(:user) + follower = insert(:user) + + User.follow(follower, user) + + assert user.follower_count == 0 + + {:ok, user} = User.update_follower_count(user) + + assert user.follower_count == 1 + end + end + + describe "mutes" do + test "it mutes people" do + user = insert(:user) + muted_user = insert(:user) + + refute User.mutes?(user, muted_user) + refute User.muted_notifications?(user, muted_user) + + {:ok, _user_relationships} = User.mute(user, muted_user) + + assert User.mutes?(user, muted_user) + assert User.muted_notifications?(user, muted_user) + end + + test "it unmutes users" do + user = insert(:user) + muted_user = insert(:user) + + {:ok, _user_relationships} = User.mute(user, muted_user) + {:ok, _user_mute} = User.unmute(user, muted_user) + + refute User.mutes?(user, muted_user) + refute User.muted_notifications?(user, muted_user) + end + + test "it mutes user without notifications" do + user = insert(:user) + muted_user = insert(:user) + + refute User.mutes?(user, muted_user) + refute User.muted_notifications?(user, muted_user) + + {:ok, _user_relationships} = User.mute(user, muted_user, false) + + assert User.mutes?(user, muted_user) + refute User.muted_notifications?(user, muted_user) + end + end + + describe "blocks" do + test "it blocks people" do + user = insert(:user) + blocked_user = insert(:user) + + refute User.blocks?(user, blocked_user) + + {:ok, _user_relationship} = User.block(user, blocked_user) + + assert User.blocks?(user, blocked_user) + end + + test "it unblocks users" do + user = insert(:user) + blocked_user = insert(:user) + + {:ok, _user_relationship} = User.block(user, blocked_user) + {:ok, _user_block} = User.unblock(user, blocked_user) + + refute User.blocks?(user, blocked_user) + end + + test "blocks tear down cyclical follow relationships" do + blocker = insert(:user) + blocked = insert(:user) + + {:ok, blocker} = User.follow(blocker, blocked) + {:ok, blocked} = User.follow(blocked, blocker) + + assert User.following?(blocker, blocked) + assert User.following?(blocked, blocker) + + {:ok, _user_relationship} = User.block(blocker, blocked) + blocked = User.get_cached_by_id(blocked.id) + + assert User.blocks?(blocker, blocked) + + refute User.following?(blocker, blocked) + refute User.following?(blocked, blocker) + end + + test "blocks tear down blocker->blocked follow relationships" do + blocker = insert(:user) + blocked = insert(:user) + + {:ok, blocker} = User.follow(blocker, blocked) + + assert User.following?(blocker, blocked) + refute User.following?(blocked, blocker) + + {:ok, _user_relationship} = User.block(blocker, blocked) + blocked = User.get_cached_by_id(blocked.id) + + assert User.blocks?(blocker, blocked) + + refute User.following?(blocker, blocked) + refute User.following?(blocked, blocker) + end + + test "blocks tear down blocked->blocker follow relationships" do + blocker = insert(:user) + blocked = insert(:user) + + {:ok, blocked} = User.follow(blocked, blocker) + + refute User.following?(blocker, blocked) + assert User.following?(blocked, blocker) + + {:ok, _user_relationship} = User.block(blocker, blocked) + blocked = User.get_cached_by_id(blocked.id) + + assert User.blocks?(blocker, blocked) + + refute User.following?(blocker, blocked) + refute User.following?(blocked, blocker) + end + + test "blocks tear down blocked->blocker subscription relationships" do + blocker = insert(:user) + blocked = insert(:user) + + {:ok, _subscription} = User.subscribe(blocked, blocker) + + assert User.subscribed_to?(blocked, blocker) + refute User.subscribed_to?(blocker, blocked) + + {:ok, _user_relationship} = User.block(blocker, blocked) + + assert User.blocks?(blocker, blocked) + refute User.subscribed_to?(blocker, blocked) + refute User.subscribed_to?(blocked, blocker) + end + end + + describe "domain blocking" do + test "blocks domains" do + user = insert(:user) + collateral_user = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"}) + + {:ok, user} = User.block_domain(user, "awful-and-rude-instance.com") + + assert User.blocks?(user, collateral_user) + end + + test "does not block domain with same end" do + user = insert(:user) + + collateral_user = + insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"}) + + {:ok, user} = User.block_domain(user, "awful-and-rude-instance.com") + + refute User.blocks?(user, collateral_user) + end + + test "does not block domain with same end if wildcard added" do + user = insert(:user) + + collateral_user = + insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"}) + + {:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com") + + refute User.blocks?(user, collateral_user) + end + + test "blocks domain with wildcard for subdomain" do + user = insert(:user) + + user_from_subdomain = + insert(:user, %{ap_id: "https://subdomain.awful-and-rude-instance.com/user/bully"}) + + user_with_two_subdomains = + insert(:user, %{ + ap_id: "https://subdomain.second_subdomain.awful-and-rude-instance.com/user/bully" + }) + + user_domain = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"}) + + {:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com") + + assert User.blocks?(user, user_from_subdomain) + assert User.blocks?(user, user_with_two_subdomains) + assert User.blocks?(user, user_domain) + end + + test "unblocks domains" do + user = insert(:user) + collateral_user = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"}) + + {:ok, user} = User.block_domain(user, "awful-and-rude-instance.com") + {:ok, user} = User.unblock_domain(user, "awful-and-rude-instance.com") + + refute User.blocks?(user, collateral_user) + end + + test "follows take precedence over domain blocks" do + user = insert(:user) + good_eggo = insert(:user, %{ap_id: "https://meanies.social/user/cuteposter"}) + + {:ok, user} = User.block_domain(user, "meanies.social") + {:ok, user} = User.follow(user, good_eggo) + + refute User.blocks?(user, good_eggo) + end + end + + describe "get_recipients_from_activity" do + test "works for announces" do + actor = insert(:user) + user = insert(:user, local: true) + + {:ok, activity} = CommonAPI.post(actor, %{status: "hello"}) + {:ok, announce} = CommonAPI.repeat(activity.id, user) + + recipients = User.get_recipients_from_activity(announce) + + assert user in recipients + end + + test "get recipients" do + actor = insert(:user) + user = insert(:user, local: true) + user_two = insert(:user, local: false) + addressed = insert(:user, local: true) + addressed_remote = insert(:user, local: false) + + {:ok, activity} = + CommonAPI.post(actor, %{ + status: "hey @#{addressed.nickname} @#{addressed_remote.nickname}" + }) + + assert Enum.map([actor, addressed], & &1.ap_id) -- + Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == [] + + {:ok, user} = User.follow(user, actor) + {:ok, _user_two} = User.follow(user_two, actor) + recipients = User.get_recipients_from_activity(activity) + assert length(recipients) == 3 + assert user in recipients + assert addressed in recipients + end + + test "has following" do + actor = insert(:user) + user = insert(:user) + user_two = insert(:user) + addressed = insert(:user, local: true) + + {:ok, activity} = + CommonAPI.post(actor, %{ + status: "hey @#{addressed.nickname}" + }) + + assert Enum.map([actor, addressed], & &1.ap_id) -- + Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == [] + + {:ok, _actor} = User.follow(actor, user) + {:ok, _actor} = User.follow(actor, user_two) + recipients = User.get_recipients_from_activity(activity) + assert length(recipients) == 2 + assert addressed in recipients + end + end + + describe ".deactivate" do + test "can de-activate then re-activate a user" do + user = insert(:user) + assert false == user.deactivated + {:ok, user} = User.deactivate(user) + assert true == user.deactivated + {:ok, user} = User.deactivate(user, false) + assert false == user.deactivated + end + + test "hide a user from followers" do + user = insert(:user) + user2 = insert(:user) + + {:ok, user} = User.follow(user, user2) + {:ok, _user} = User.deactivate(user) + + user2 = User.get_cached_by_id(user2.id) + + assert user2.follower_count == 0 + assert [] = User.get_followers(user2) + end + + test "hide a user from friends" do + user = insert(:user) + user2 = insert(:user) + + {:ok, user2} = User.follow(user2, user) + assert user2.following_count == 1 + assert User.following_count(user2) == 1 + + {:ok, _user} = User.deactivate(user) + + user2 = User.get_cached_by_id(user2.id) + + assert refresh_record(user2).following_count == 0 + assert user2.following_count == 0 + assert User.following_count(user2) == 0 + assert [] = User.get_friends(user2) + end + + test "hide a user's statuses from timelines and notifications" do + user = insert(:user) + user2 = insert(:user) + + {:ok, user2} = User.follow(user2, user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{user2.nickname}"}) + + activity = Repo.preload(activity, :bookmark) + + [notification] = Pleroma.Notification.for_user(user2) + assert notification.activity.id == activity.id + + assert [activity] == ActivityPub.fetch_public_activities(%{}) |> Repo.preload(:bookmark) + + assert [%{activity | thread_muted?: CommonAPI.thread_muted?(user2, activity)}] == + ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{ + user: user2 + }) + + {:ok, _user} = User.deactivate(user) + + assert [] == ActivityPub.fetch_public_activities(%{}) + assert [] == Pleroma.Notification.for_user(user2) + + assert [] == + ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{ + user: user2 + }) + end + end + + describe "approve" do + test "approves a user" do + user = insert(:user, approval_pending: true) + assert true == user.approval_pending + {:ok, user} = User.approve(user) + assert false == user.approval_pending + end + + test "approves a list of users" do + unapproved_users = [ + insert(:user, approval_pending: true), + insert(:user, approval_pending: true), + insert(:user, approval_pending: true) + ] + + {:ok, users} = User.approve(unapproved_users) + + assert Enum.count(users) == 3 + + Enum.each(users, fn user -> + assert false == user.approval_pending + end) + end + end + + describe "delete" do + setup do + {:ok, user} = insert(:user) |> User.set_cache() + + [user: user] + end + + setup do: clear_config([:instance, :federating]) + + test ".delete_user_activities deletes all create activities", %{user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "2hu"}) + + User.delete_user_activities(user) + + # TODO: Test removal favorites, repeats, delete activities. + refute Activity.get_by_id(activity.id) + end + + test "it deactivates a user, all follow relationships and all activities", %{user: user} do + follower = insert(:user) + {:ok, follower} = User.follow(follower, user) + + locked_user = insert(:user, name: "locked", locked: true) + {:ok, _} = User.follow(user, locked_user, :follow_pending) + + object = insert(:note, user: user) + activity = insert(:note_activity, user: user, note: object) + + object_two = insert(:note, user: follower) + activity_two = insert(:note_activity, user: follower, note: object_two) + + {:ok, like} = CommonAPI.favorite(user, activity_two.id) + {:ok, like_two} = CommonAPI.favorite(follower, activity.id) + {:ok, repeat} = CommonAPI.repeat(activity_two.id, user) + + {:ok, job} = User.delete(user) + {:ok, _user} = ObanHelpers.perform(job) + + follower = User.get_cached_by_id(follower.id) + + refute User.following?(follower, user) + assert %{deactivated: true} = User.get_by_id(user.id) + + assert [] == User.get_follow_requests(locked_user) + + user_activities = + user.ap_id + |> Activity.Queries.by_actor() + |> Repo.all() + |> Enum.map(fn act -> act.data["type"] end) + + assert Enum.all?(user_activities, fn act -> act in ~w(Delete Undo) end) + + refute Activity.get_by_id(activity.id) + refute Activity.get_by_id(like.id) + refute Activity.get_by_id(like_two.id) + refute Activity.get_by_id(repeat.id) + end + end + + describe "delete/1 when confirmation is pending" do + setup do + user = insert(:user, confirmation_pending: true) + {:ok, user: user} + end + + test "deletes user from database when activation required", %{user: user} do + clear_config([:instance, :account_activation_required], true) + + {:ok, job} = User.delete(user) + {:ok, _} = ObanHelpers.perform(job) + + refute User.get_cached_by_id(user.id) + refute User.get_by_id(user.id) + end + + test "deactivates user when activation is not required", %{user: user} do + clear_config([:instance, :account_activation_required], false) + + {:ok, job} = User.delete(user) + {:ok, _} = ObanHelpers.perform(job) + + assert %{deactivated: true} = User.get_cached_by_id(user.id) + assert %{deactivated: true} = User.get_by_id(user.id) + end + end + + test "delete/1 when approval is pending deletes the user" do + user = insert(:user, approval_pending: true) + + {:ok, job} = User.delete(user) + {:ok, _} = ObanHelpers.perform(job) + + refute User.get_cached_by_id(user.id) + refute User.get_by_id(user.id) + end + + test "delete/1 purges a user when they wouldn't be fully deleted" do + user = + insert(:user, %{ + bio: "eyy lmao", + name: "qqqqqqq", + password_hash: "pdfk2$1b3n159001", + keys: "RSA begin buplic key", + public_key: "--PRIVATE KEYE--", + avatar: %{"a" => "b"}, + tags: ["qqqqq"], + banner: %{"a" => "b"}, + background: %{"a" => "b"}, + note_count: 9, + follower_count: 9, + following_count: 9001, + locked: true, + confirmation_pending: true, + password_reset_pending: true, + approval_pending: true, + registration_reason: "ahhhhh", + confirmation_token: "qqqq", + domain_blocks: ["lain.com"], + deactivated: true, + ap_enabled: true, + is_moderator: true, + is_admin: true, + mastofe_settings: %{"a" => "b"}, + mascot: %{"a" => "b"}, + emoji: %{"a" => "b"}, + pleroma_settings_store: %{"q" => "x"}, + fields: [%{"gg" => "qq"}], + raw_fields: [%{"gg" => "qq"}], + discoverable: true, + also_known_as: ["https://lol.olo/users/loll"] + }) + + {:ok, job} = User.delete(user) + {:ok, _} = ObanHelpers.perform(job) + user = User.get_by_id(user.id) + + assert %User{ + bio: "", + raw_bio: nil, + email: nil, + name: nil, + password_hash: nil, + keys: nil, + public_key: nil, + avatar: %{}, + tags: [], + last_refreshed_at: nil, + last_digest_emailed_at: nil, + banner: %{}, + background: %{}, + note_count: 0, + follower_count: 0, + following_count: 0, + locked: false, + confirmation_pending: false, + password_reset_pending: false, + approval_pending: false, + registration_reason: nil, + confirmation_token: nil, + domain_blocks: [], + deactivated: true, + ap_enabled: false, + is_moderator: false, + is_admin: false, + mastofe_settings: nil, + mascot: nil, + emoji: %{}, + pleroma_settings_store: %{}, + fields: [], + raw_fields: [], + discoverable: false, + also_known_as: [] + } = user + 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") + end + + describe "per-user rich-text filtering" do + test "html_filter_policy returns default policies, when rich-text is enabled" do + user = insert(:user) + + assert Pleroma.Config.get([:markup, :scrub_policy]) == User.html_filter_policy(user) + end + + test "html_filter_policy returns TwitterText scrubber when rich-text is disabled" do + user = insert(:user, no_rich_text: true) + + assert Pleroma.HTML.Scrubber.TwitterText == User.html_filter_policy(user) + end + end + + describe "caching" do + test "invalidate_cache works" do + user = insert(:user) + + User.set_cache(user) + User.invalidate_cache(user) + + {:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") + {:ok, nil} = Cachex.get(:user_cache, "nickname:#{user.nickname}") + end + + test "User.delete() plugs any possible zombie objects" do + user = insert(:user) + + {:ok, job} = User.delete(user) + {:ok, _} = ObanHelpers.perform(job) + + {:ok, cached_user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") + + assert cached_user != user + + {:ok, cached_user} = Cachex.get(:user_cache, "nickname:#{user.ap_id}") + + assert cached_user != user + end + end + + describe "account_status/1" do + setup do: clear_config([:instance, :account_activation_required]) + + test "return confirmation_pending for unconfirm user" do + Pleroma.Config.put([:instance, :account_activation_required], true) + user = insert(:user, confirmation_pending: true) + assert User.account_status(user) == :confirmation_pending + end + + test "return active for confirmed user" do + Pleroma.Config.put([:instance, :account_activation_required], true) + user = insert(:user, confirmation_pending: false) + assert User.account_status(user) == :active + end + + test "return active for remote user" do + user = insert(:user, local: false) + assert User.account_status(user) == :active + end + + test "returns :password_reset_pending for user with reset password" do + user = insert(:user, password_reset_pending: true) + assert User.account_status(user) == :password_reset_pending + end + + test "returns :deactivated for deactivated user" do + user = insert(:user, local: true, confirmation_pending: false, deactivated: true) + assert User.account_status(user) == :deactivated + end + + test "returns :approval_pending for unapproved user" do + user = insert(:user, local: true, approval_pending: true) + assert User.account_status(user) == :approval_pending + + user = insert(:user, local: true, confirmation_pending: true, approval_pending: true) + assert User.account_status(user) == :approval_pending + end + end + + describe "superuser?/1" do + test "returns false for unprivileged users" do + user = insert(:user, local: true) + + refute User.superuser?(user) + 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) + end + + test "returns true for local moderators" do + user = insert(:user, local: true, is_moderator: true) + + assert User.superuser?(user) + end + + test "returns true for local admins" do + user = insert(:user, local: true, is_admin: true) + + assert User.superuser?(user) + end + end + + describe "invisible?/1" do + test "returns true for an invisible user" do + user = insert(:user, local: true, invisible: true) + + assert User.invisible?(user) + end + + test "returns false for a non-invisible user" do + user = insert(:user, local: true) + + refute User.invisible?(user) + end + end + + describe "visible_for/2" do + test "returns true when the account is itself" do + user = insert(:user, local: true) + + assert User.visible_for(user, user) == :visible + end + + test "returns false when the account is unconfirmed and confirmation is required" do + Pleroma.Config.put([:instance, :account_activation_required], true) + + user = insert(:user, local: true, confirmation_pending: true) + other_user = insert(:user, local: true) + + refute User.visible_for(user, other_user) == :visible + end + + test "returns true when the account is unconfirmed and confirmation is required but the account is remote" do + Pleroma.Config.put([:instance, :account_activation_required], true) + + user = insert(:user, local: false, confirmation_pending: true) + other_user = insert(:user, local: true) + + assert User.visible_for(user, other_user) == :visible + end + + test "returns true when the account is unconfirmed and confirmation is not required" do + user = insert(:user, local: true, confirmation_pending: true) + other_user = insert(:user, local: true) + + 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 + Pleroma.Config.put([:instance, :account_activation_required], true) + + user = insert(:user, local: true, confirmation_pending: true) + other_user = insert(:user, local: true, is_admin: true) + + assert User.visible_for(user, other_user) == :visible + end + end + + describe "parse_bio/2" do + test "preserves hosts in user links text" do + remote_user = insert(:user, local: false, nickname: "nick@domain.com") + user = insert(:user) + bio = "A.k.a. @nick@domain.com" + + expected_text = + ~s(A.k.a. <span class="h-card"><a class="u-url mention" data-user="#{remote_user.id}" href="#{ + remote_user.ap_id + }" rel="ugc">@<span>nick@domain.com</span></a></span>) + + assert expected_text == User.parse_bio(bio, user) + end + + test "Adds rel=me on linkbacked urls" do + user = insert(:user, ap_id: "https://social.example.org/users/lain") + + bio = "http://example.com/rel_me/null" + expected_text = "<a href=\"#{bio}\">#{bio}</a>" + assert expected_text == User.parse_bio(bio, user) + + bio = "http://example.com/rel_me/link" + expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>" + assert expected_text == User.parse_bio(bio, user) + + bio = "http://example.com/rel_me/anchor" + expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>" + assert expected_text == User.parse_bio(bio, user) + end + end + + test "follower count is updated when a follower is blocked" do + user = insert(:user) + follower = insert(:user) + follower2 = insert(:user) + follower3 = insert(:user) + + {:ok, follower} = User.follow(follower, user) + {:ok, _follower2} = User.follow(follower2, user) + {:ok, _follower3} = User.follow(follower3, user) + + {:ok, _user_relationship} = User.block(user, follower) + user = refresh_record(user) + + assert user.follower_count == 2 + end + + describe "list_inactive_users_query/1" do + defp days_ago(days) do + NaiveDateTime.add( + NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), + -days * 60 * 60 * 24, + :second + ) + end + + test "Users are inactive by default" do + total = 10 + + users = + Enum.map(1..total, fn _ -> + insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false) + end) + + inactive_users_ids = + Pleroma.User.list_inactive_users_query() + |> Pleroma.Repo.all() + |> Enum.map(& &1.id) + + Enum.each(users, fn user -> + assert user.id in inactive_users_ids + end) + end + + test "Only includes users who has no recent activity" do + total = 10 + + users = + Enum.map(1..total, fn _ -> + insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false) + end) + + {inactive, active} = Enum.split(users, trunc(total / 2)) + + Enum.map(active, fn user -> + to = Enum.random(users -- [user]) + + {:ok, _} = + CommonAPI.post(user, %{ + status: "hey @#{to.nickname}" + }) + end) + + inactive_users_ids = + Pleroma.User.list_inactive_users_query() + |> Pleroma.Repo.all() + |> Enum.map(& &1.id) + + Enum.each(active, fn user -> + refute user.id in inactive_users_ids + end) + + Enum.each(inactive, fn user -> + assert user.id in inactive_users_ids + end) + end + + test "Only includes users with no read notifications" do + total = 10 + + users = + Enum.map(1..total, fn _ -> + insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false) + end) + + [sender | recipients] = users + {inactive, active} = Enum.split(recipients, trunc(total / 2)) + + Enum.each(recipients, fn to -> + {:ok, _} = + CommonAPI.post(sender, %{ + status: "hey @#{to.nickname}" + }) + + {:ok, _} = + CommonAPI.post(sender, %{ + status: "hey again @#{to.nickname}" + }) + end) + + Enum.each(active, fn user -> + [n1, _n2] = Pleroma.Notification.for_user(user) + {:ok, _} = Pleroma.Notification.read_one(user, n1.id) + end) + + inactive_users_ids = + Pleroma.User.list_inactive_users_query() + |> Pleroma.Repo.all() + |> Enum.map(& &1.id) + + Enum.each(active, fn user -> + refute user.id in inactive_users_ids + end) + + Enum.each(inactive, fn user -> + assert user.id in inactive_users_ids + end) + end + end + + describe "toggle_confirmation/1" do + test "if user is confirmed" do + user = insert(:user, confirmation_pending: false) + {:ok, user} = User.toggle_confirmation(user) + + assert user.confirmation_pending + assert user.confirmation_token + end + + test "if user is unconfirmed" do + user = insert(:user, confirmation_pending: true, confirmation_token: "some token") + {:ok, user} = User.toggle_confirmation(user) + + refute user.confirmation_pending + refute user.confirmation_token + 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) + user_two = insert(:user) + + ap_ids = User.get_ap_ids_by_nicknames([user.nickname, user_two.nickname, "nonexistent"]) + assert length(ap_ids) == 2 + assert user.ap_id in ap_ids + assert user_two.ap_id in ap_ids + end + end + + describe "sync followers count" do + setup do + user1 = insert(:user, local: false, ap_id: "http://localhost:4001/users/masto_closed") + user2 = insert(:user, local: false, ap_id: "http://localhost:4001/users/fuser2") + insert(:user, local: true) + insert(:user, local: false, deactivated: true) + {:ok, user1: user1, user2: user2} + end + + test "external_users/1 external active users with limit", %{user1: user1, user2: user2} do + [fdb_user1] = User.external_users(limit: 1) + + assert fdb_user1.ap_id + assert fdb_user1.ap_id == user1.ap_id + assert fdb_user1.id == user1.id + + [fdb_user2] = User.external_users(max_id: fdb_user1.id, limit: 1) + + assert fdb_user2.ap_id + assert fdb_user2.ap_id == user2.ap_id + assert fdb_user2.id == user2.id + + assert User.external_users(max_id: fdb_user2.id, limit: 1) == [] + end + end + + describe "is_internal_user?/1" do + test "non-internal user returns false" do + user = insert(:user) + refute User.is_internal_user?(user) + end + + test "user with no nickname returns true" do + user = insert(:user, %{nickname: nil}) + assert User.is_internal_user?(user) + end + + test "user with internal-prefixed nickname returns true" do + user = insert(:user, %{nickname: "internal.test"}) + assert User.is_internal_user?(user) + end + end + + describe "update_and_set_cache/1" do + test "returns error when user is stale instead Ecto.StaleEntryError" do + user = insert(:user) + + changeset = Ecto.Changeset.change(user, bio: "test") + + Repo.delete(user) + + assert {:error, %Ecto.Changeset{errors: [id: {"is stale", [stale: true]}], valid?: false}} = + User.update_and_set_cache(changeset) + end + + test "performs update cache if user updated" do + user = insert(:user) + assert {:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") + + changeset = Ecto.Changeset.change(user, bio: "test-bio") + + assert {:ok, %User{bio: "test-bio"} = user} = User.update_and_set_cache(changeset) + 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 + end + + describe "following/followers synchronization" do + setup do: clear_config([:instance, :external_user_synchronization]) + + test "updates the counters normally on following/getting a follow when disabled" do + Pleroma.Config.put([:instance, :external_user_synchronization], false) + user = insert(:user) + + other_user = + 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 + ) + + assert other_user.following_count == 0 + assert other_user.follower_count == 0 + + {:ok, user} = Pleroma.User.follow(user, other_user) + other_user = Pleroma.User.get_by_id(other_user.id) + + assert user.following_count == 1 + assert other_user.follower_count == 1 + end + + test "syncronizes the counters with the remote instance for the followed when enabled" do + Pleroma.Config.put([:instance, :external_user_synchronization], false) + + user = insert(:user) + + other_user = + 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 + ) + + assert other_user.following_count == 0 + assert other_user.follower_count == 0 + + Pleroma.Config.put([:instance, :external_user_synchronization], true) + {:ok, _user} = User.follow(user, other_user) + other_user = User.get_by_id(other_user.id) + + assert other_user.follower_count == 437 + end + + test "syncronizes the counters with the remote instance for the follower when enabled" do + Pleroma.Config.put([:instance, :external_user_synchronization], false) + + user = insert(:user) + + other_user = + 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 + ) + + assert other_user.following_count == 0 + assert other_user.follower_count == 0 + + Pleroma.Config.put([:instance, :external_user_synchronization], true) + {:ok, other_user} = User.follow(other_user, user) + + assert other_user.following_count == 152 + end + end + + describe "change_email/2" do + setup do + [user: insert(:user)] + end + + test "blank email returns error", %{user: user} do + assert {:error, %{errors: [email: {"can't be blank", _}]}} = User.change_email(user, "") + assert {:error, %{errors: [email: {"can't be blank", _}]}} = User.change_email(user, nil) + end + + test "non unique email returns error", %{user: user} do + %{email: email} = insert(:user) + + assert {:error, %{errors: [email: {"has already been taken", _}]}} = + User.change_email(user, email) + end + + test "invalid email returns error", %{user: user} do + assert {:error, %{errors: [email: {"has invalid format", _}]}} = + User.change_email(user, "cofe") + end + + test "changes email", %{user: user} do + assert {:ok, %User{email: "cofe@cofe.party"}} = User.change_email(user, "cofe@cofe.party") + end + end + + describe "get_cached_by_nickname_or_id" do + setup do + local_user = insert(:user) + remote_user = insert(:user, nickname: "nickname@example.com", local: false) + + [local_user: local_user, remote_user: remote_user] + end + + setup do: clear_config([:instance, :limit_to_local_content]) + + test "allows getting remote users by id no matter what :limit_to_local_content is set to", %{ + remote_user: remote_user + } do + Pleroma.Config.put([:instance, :limit_to_local_content], false) + assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id) + + Pleroma.Config.put([:instance, :limit_to_local_content], true) + assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id) + + Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) + assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id) + end + + test "disallows getting remote users by nickname without authentication when :limit_to_local_content is set to :unauthenticated", + %{remote_user: remote_user} do + Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) + assert nil == User.get_cached_by_nickname_or_id(remote_user.nickname) + end + + test "allows getting remote users by nickname with authentication when :limit_to_local_content is set to :unauthenticated", + %{remote_user: remote_user, local_user: local_user} do + Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) + assert %User{} = User.get_cached_by_nickname_or_id(remote_user.nickname, for: local_user) + end + + test "disallows getting remote users by nickname when :limit_to_local_content is set to true", + %{remote_user: remote_user} do + Pleroma.Config.put([:instance, :limit_to_local_content], true) + assert nil == User.get_cached_by_nickname_or_id(remote_user.nickname) + end + + test "allows getting local users by nickname no matter what :limit_to_local_content is set to", + %{local_user: local_user} do + Pleroma.Config.put([:instance, :limit_to_local_content], false) + assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname) + + Pleroma.Config.put([:instance, :limit_to_local_content], true) + assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname) + + Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) + assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname) + end + end + + describe "update_email_notifications/2" do + setup do + user = insert(:user, email_notifications: %{"digest" => true}) + + {:ok, user: user} + end + + test "Notifications are updated", %{user: user} do + true = user.email_notifications["digest"] + assert {:ok, result} = User.update_email_notifications(user, %{"digest" => false}) + assert result.email_notifications["digest"] == false + end + end + + test "avatar fallback" do + user = insert(:user) + assert User.avatar_url(user) =~ "/images/avi.png" + + clear_config([:assets, :default_user_avatar], "avatar.png") + + user = User.get_cached_by_nickname_or_id(user.nickname) + assert User.avatar_url(user) =~ "avatar.png" + + assert User.avatar_url(user, no_default: true) == nil + end +end diff --git a/test/pleroma/utils_test.exs b/test/pleroma/utils_test.exs @@ -0,0 +1,15 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UtilsTest do + use ExUnit.Case, async: true + + describe "tmp_dir/1" do + test "returns unique temporary directory" do + {:ok, path} = Pleroma.Utils.tmp_dir("emoji") + assert path =~ ~r/\/emoji-(.*)-#{:os.getpid()}-(.*)/ + File.rm_rf(path) + end + 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 @@ -0,0 +1,1550 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do + use Pleroma.Web.ConnCase + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.Delivery + alias Pleroma.Instances + alias Pleroma.Object + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.ObjectView + alias Pleroma.Web.ActivityPub.Relay + alias Pleroma.Web.ActivityPub.UserView + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Endpoint + alias Pleroma.Workers.ReceiverWorker + + import Pleroma.Factory + + require Pleroma.Constants + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup do: clear_config([:instance, :federating], true) + + describe "/relay" do + setup do: clear_config([:instance, :allow_relay]) + + test "with the relay active, it returns the relay user", %{conn: conn} do + res = + conn + |> get(activity_pub_path(conn, :relay)) + |> json_response(200) + + assert res["id"] =~ "/relay" + end + + test "with the relay disabled, it returns 404", %{conn: conn} do + Config.put([:instance, :allow_relay], false) + + conn + |> get(activity_pub_path(conn, :relay)) + |> json_response(404) + end + + test "on non-federating instance, it returns 404", %{conn: conn} do + Config.put([:instance, :federating], false) + user = insert(:user) + + conn + |> assign(:user, user) + |> get(activity_pub_path(conn, :relay)) + |> json_response(404) + end + end + + describe "/internal/fetch" do + test "it returns the internal fetch user", %{conn: conn} do + res = + conn + |> get(activity_pub_path(conn, :internal_fetch)) + |> json_response(200) + + assert res["id"] =~ "/fetch" + end + + test "on non-federating instance, it returns 404", %{conn: conn} do + Config.put([:instance, :federating], false) + user = insert(:user) + + conn + |> assign(:user, user) + |> get(activity_pub_path(conn, :internal_fetch)) + |> json_response(404) + end + end + + describe "/users/:nickname" do + test "it returns a json representation of the user with accept application/json", %{ + conn: conn + } do + user = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/json") + |> get("/users/#{user.nickname}") + + user = User.get_cached_by_id(user.id) + + assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) + end + + test "it returns a json representation of the user with accept application/activity+json", %{ + conn: conn + } do + user = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}") + + user = User.get_cached_by_id(user.id) + + assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) + end + + test "it returns a json representation of the user with accept application/ld+json", %{ + conn: conn + } do + user = insert(:user) + + conn = + conn + |> put_req_header( + "accept", + "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + ) + |> get("/users/#{user.nickname}") + + user = User.get_cached_by_id(user.id) + + assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) + end + + test "it returns 404 for remote users", %{ + conn: conn + } do + user = insert(:user, local: false, nickname: "remoteuser@example.com") + + conn = + conn + |> put_req_header("accept", "application/json") + |> get("/users/#{user.nickname}.json") + + assert json_response(conn, 404) + end + + test "it returns error when user is not found", %{conn: conn} do + response = + conn + |> put_req_header("accept", "application/json") + |> get("/users/jimm") + |> json_response(404) + + assert response == "Not found" + end + + test "it requires authentication if instance is NOT federating", %{ + conn: conn + } do + user = insert(:user) + + conn = + put_req_header( + conn, + "accept", + "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + ) + + ensure_federating_or_authenticated(conn, "/users/#{user.nickname}.json", user) + end + end + + describe "mastodon compatibility routes" do + test "it returns a json representation of the object with accept application/json", %{ + conn: conn + } do + {:ok, object} = + %{ + "type" => "Note", + "content" => "hey", + "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999", + "actor" => Endpoint.url() <> "/users/raymoo", + "to" => [Pleroma.Constants.as_public()] + } + |> Object.create() + + conn = + conn + |> put_req_header("accept", "application/json") + |> get("/users/raymoo/statuses/999999999") + + assert json_response(conn, 200) == ObjectView.render("object.json", %{object: object}) + end + + test "it returns a json representation of the activity with accept application/json", %{ + conn: conn + } do + {:ok, object} = + %{ + "type" => "Note", + "content" => "hey", + "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999", + "actor" => Endpoint.url() <> "/users/raymoo", + "to" => [Pleroma.Constants.as_public()] + } + |> Object.create() + + {:ok, activity, _} = + %{ + "id" => object.data["id"] <> "/activity", + "type" => "Create", + "object" => object.data["id"], + "actor" => object.data["actor"], + "to" => object.data["to"] + } + |> ActivityPub.persist(local: true) + + conn = + conn + |> put_req_header("accept", "application/json") + |> get("/users/raymoo/statuses/999999999/activity") + + assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity}) + end + end + + describe "/objects/:uuid" do + test "it returns a json representation of the object with accept application/json", %{ + conn: conn + } do + note = insert(:note) + uuid = String.split(note.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header("accept", "application/json") + |> get("/objects/#{uuid}") + + assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) + end + + test "it returns a json representation of the object with accept application/activity+json", + %{conn: conn} do + note = insert(:note) + uuid = String.split(note.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) + end + + test "it returns a json representation of the object with accept application/ld+json", %{ + conn: conn + } do + note = insert(:note) + uuid = String.split(note.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header( + "accept", + "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + ) + |> get("/objects/#{uuid}") + + assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) + end + + test "it returns 404 for non-public messages", %{conn: conn} do + note = insert(:direct_note) + uuid = String.split(note.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + assert json_response(conn, 404) + end + + test "it returns 404 for tombstone objects", %{conn: conn} do + tombstone = insert(:tombstone) + uuid = String.split(tombstone.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + assert json_response(conn, 404) + end + + test "it caches a response", %{conn: conn} do + note = insert(:note) + uuid = String.split(note.data["id"], "/") |> List.last() + + conn1 = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + assert json_response(conn1, :ok) + assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) + + conn2 = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + assert json_response(conn1, :ok) == json_response(conn2, :ok) + assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"})) + end + + test "cached purged after object deletion", %{conn: conn} do + note = insert(:note) + uuid = String.split(note.data["id"], "/") |> List.last() + + conn1 = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + assert json_response(conn1, :ok) + assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) + + Object.delete(note) + + conn2 = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + assert "Not found" == json_response(conn2, :not_found) + end + + test "it requires authentication if instance is NOT federating", %{ + conn: conn + } do + user = insert(:user) + note = insert(:note) + uuid = String.split(note.data["id"], "/") |> List.last() + + conn = put_req_header(conn, "accept", "application/activity+json") + + ensure_federating_or_authenticated(conn, "/objects/#{uuid}", user) + end + end + + describe "/activities/:uuid" do + test "it returns a json representation of the activity", %{conn: conn} do + activity = insert(:note_activity) + uuid = String.split(activity.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/activities/#{uuid}") + + assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity}) + end + + test "it returns 404 for non-public activities", %{conn: conn} do + activity = insert(:direct_note_activity) + uuid = String.split(activity.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/activities/#{uuid}") + + assert json_response(conn, 404) + end + + test "it caches a response", %{conn: conn} do + activity = insert(:note_activity) + uuid = String.split(activity.data["id"], "/") |> List.last() + + conn1 = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/activities/#{uuid}") + + assert json_response(conn1, :ok) + assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) + + conn2 = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/activities/#{uuid}") + + assert json_response(conn1, :ok) == json_response(conn2, :ok) + assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"})) + end + + test "cached purged after activity deletion", %{conn: conn} do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "cofe"}) + + uuid = String.split(activity.data["id"], "/") |> List.last() + + conn1 = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/activities/#{uuid}") + + assert json_response(conn1, :ok) + assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) + + Activity.delete_all_by_object_ap_id(activity.object.data["id"]) + + conn2 = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/activities/#{uuid}") + + assert "Not found" == json_response(conn2, :not_found) + end + + test "it requires authentication if instance is NOT federating", %{ + conn: conn + } do + user = insert(:user) + activity = insert(:note_activity) + uuid = String.split(activity.data["id"], "/") |> List.last() + + conn = put_req_header(conn, "accept", "application/activity+json") + + ensure_federating_or_authenticated(conn, "/activities/#{uuid}", user) + end + end + + describe "/inbox" do + test "it inserts an incoming activity into the database", %{conn: conn} do + data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + assert Activity.get_by_ap_id(data["id"]) + end + + @tag capture_log: true + test "it inserts an incoming activity into the database" <> + "even if we can't fetch the user but have it in our db", + %{conn: conn} do + user = + insert(:user, + ap_id: "https://mastodon.example.org/users/raymoo", + ap_enabled: true, + local: false, + last_refreshed_at: nil + ) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + |> Map.put("actor", user.ap_id) + |> put_in(["object", "attridbutedTo"], user.ap_id) + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + assert Activity.get_by_ap_id(data["id"]) + end + + test "it clears `unreachable` federation status of the sender", %{conn: conn} do + data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() + + sender_url = data["actor"] + Instances.set_consistently_unreachable(sender_url) + refute Instances.reachable?(sender_url) + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + assert Instances.reachable?(sender_url) + end + + test "accept follow activity", %{conn: conn} do + Pleroma.Config.put([:instance, :federating], true) + relay = Relay.get_actor() + + assert {:ok, %Activity{} = activity} = Relay.follow("https://relay.mastodon.host/actor") + + followed_relay = Pleroma.User.get_by_ap_id("https://relay.mastodon.host/actor") + relay = refresh_record(relay) + + accept = + File.read!("test/fixtures/relay/accept-follow.json") + |> String.replace("{{ap_id}}", relay.ap_id) + |> String.replace("{{activity_id}}", activity.data["id"]) + + assert "ok" == + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/inbox", accept) + |> json_response(200) + + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + assert Pleroma.FollowingRelationship.following?( + relay, + followed_relay + ) + + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) + assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]} + end + + @tag capture_log: true + test "without valid signature, " <> + "it only accepts Create activities and requires enabled federation", + %{conn: conn} do + data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() + non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!() + + conn = put_req_header(conn, "content-type", "application/activity+json") + + Config.put([:instance, :federating], false) + + conn + |> post("/inbox", data) + |> json_response(403) + + conn + |> post("/inbox", non_create_data) + |> json_response(403) + + Config.put([:instance, :federating], true) + + ret_conn = post(conn, "/inbox", data) + assert "ok" == json_response(ret_conn, 200) + + conn + |> post("/inbox", non_create_data) + |> json_response(400) + end + end + + describe "/users/:nickname/inbox" do + setup do + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + [data: data] + end + + test "it inserts an incoming activity into the database", %{conn: conn, data: data} do + user = insert(:user) + data = Map.put(data, "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 "ok" == json_response(conn, 200) + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + assert Activity.get_by_ap_id(data["id"]) + end + + test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do + user = insert(:user) + + data = + Map.put(data, "to", user.ap_id) + |> Map.delete("cc") + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + assert Activity.get_by_ap_id(data["id"]) + end + + test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do + user = insert(:user) + + data = + Map.put(data, "cc", user.ap_id) + |> Map.delete("to") + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + %Activity{} = activity = Activity.get_by_ap_id(data["id"]) + assert user.ap_id in activity.recipients + end + + test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do + user = insert(:user) + + data = + Map.put(data, "bcc", user.ap_id) + |> Map.delete("to") + |> Map.delete("cc") + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + assert Activity.get_by_ap_id(data["id"]) + end + + test "it accepts announces with to as string instead of array", %{conn: conn} do + user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "hey"}) + announcer = insert(:user, local: false) + + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => announcer.ap_id, + "id" => "#{announcer.ap_id}/statuses/19512778738411822/activity", + "object" => post.data["object"], + "to" => "https://www.w3.org/ns/activitystreams#Public", + "cc" => [user.ap_id], + "type" => "Announce" + } + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + %Activity{} = activity = Activity.get_by_ap_id(data["id"]) + assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients + end + + test "it accepts messages from actors that are followed by the user", %{ + conn: conn, + data: data + } do + recipient = insert(:user) + actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"}) + + {:ok, recipient} = User.follow(recipient, actor) + + object = + data["object"] + |> Map.put("attributedTo", actor.ap_id) + + data = + data + |> Map.put("actor", actor.ap_id) + |> Map.put("object", object) + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{recipient.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + assert Activity.get_by_ap_id(data["id"]) + end + + test "it rejects reads from other users", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + conn = + conn + |> assign(:user, other_user) + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}/inbox") + + assert json_response(conn, 403) + end + + test "it returns a note activity in a collection", %{conn: conn} do + note_activity = insert(:direct_note_activity) + note_object = Object.normalize(note_activity) + user = User.get_cached_by_ap_id(hd(note_activity.data["to"])) + + conn = + conn + |> assign(:user, user) + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}/inbox?page=true") + + assert response(conn, 200) =~ note_object.data["content"] + end + + test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do + user = insert(:user) + data = Map.put(data, "bcc", [user.ap_id]) + + sender_host = URI.parse(data["actor"]).host + Instances.set_consistently_unreachable(sender_host) + refute Instances.reachable?(sender_host) + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + assert Instances.reachable?(sender_host) + end + + test "it removes all follower collections but actor's", %{conn: conn} do + [actor, recipient] = insert_pair(:user) + + data = + File.read!("test/fixtures/activitypub-client-post-activity.json") + |> Poison.decode!() + + object = Map.put(data["object"], "attributedTo", actor.ap_id) + + data = + data + |> Map.put("id", Utils.generate_object_id()) + |> Map.put("actor", actor.ap_id) + |> Map.put("object", object) + |> Map.put("cc", [ + recipient.follower_address, + actor.follower_address + ]) + |> Map.put("to", [ + recipient.ap_id, + recipient.follower_address, + "https://www.w3.org/ns/activitystreams#Public" + ]) + + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{recipient.nickname}/inbox", data) + |> json_response(200) + + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + activity = Activity.get_by_ap_id(data["id"]) + + assert activity.id + assert actor.follower_address in activity.recipients + assert actor.follower_address in activity.data["cc"] + + refute recipient.follower_address in activity.recipients + refute recipient.follower_address in activity.data["cc"] + refute recipient.follower_address in activity.data["to"] + end + + test "it requires authentication", %{conn: conn} do + user = insert(:user) + conn = put_req_header(conn, "accept", "application/activity+json") + + ret_conn = get(conn, "/users/#{user.nickname}/inbox") + assert json_response(ret_conn, 403) + + ret_conn = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/inbox") + + assert json_response(ret_conn, 200) + end + end + + describe "GET /users/:nickname/outbox" do + test "it paginates correctly", %{conn: conn} do + user = insert(:user) + conn = assign(conn, :user, user) + outbox_endpoint = user.ap_id <> "/outbox" + + _posts = + for i <- 0..25 do + {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"}) + activity + end + + result = + conn + |> put_req_header("accept", "application/activity+json") + |> get(outbox_endpoint <> "?page=true") + |> json_response(200) + + result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end) + assert length(result["orderedItems"]) == 20 + assert length(result_ids) == 20 + assert result["next"] + assert String.starts_with?(result["next"], outbox_endpoint) + + result_next = + conn + |> put_req_header("accept", "application/activity+json") + |> get(result["next"]) + |> json_response(200) + + result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end) + assert length(result_next["orderedItems"]) == 6 + assert length(result_next_ids) == 6 + refute Enum.find(result_next_ids, fn x -> x in result_ids end) + refute Enum.find(result_ids, fn x -> x in result_next_ids end) + assert String.starts_with?(result["id"], outbox_endpoint) + + result_next_again = + conn + |> put_req_header("accept", "application/activity+json") + |> get(result_next["id"]) + |> json_response(200) + + assert result_next == result_next_again + end + + test "it returns 200 even if there're no activities", %{conn: conn} do + user = insert(:user) + outbox_endpoint = user.ap_id <> "/outbox" + + conn = + conn + |> assign(:user, user) + |> put_req_header("accept", "application/activity+json") + |> get(outbox_endpoint) + + result = json_response(conn, 200) + assert outbox_endpoint == result["id"] + end + + test "it returns a note activity in a collection", %{conn: conn} do + note_activity = insert(:note_activity) + note_object = Object.normalize(note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + conn = + conn + |> assign(:user, user) + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}/outbox?page=true") + + assert response(conn, 200) =~ note_object.data["content"] + end + + test "it returns an announce activity in a collection", %{conn: conn} do + announce_activity = insert(:announce_activity) + user = User.get_cached_by_ap_id(announce_activity.data["actor"]) + + conn = + conn + |> assign(:user, user) + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}/outbox?page=true") + + assert response(conn, 200) =~ announce_activity.data["object"] + end + + test "it requires authentication if instance is NOT federating", %{ + conn: conn + } do + user = insert(:user) + conn = put_req_header(conn, "accept", "application/activity+json") + + ensure_federating_or_authenticated(conn, "/users/#{user.nickname}/outbox", user) + end + end + + describe "POST /users/:nickname/outbox (C2S)" do + setup do: clear_config([:instance, :limit]) + + setup do + [ + activity: %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "object" => %{"type" => "Note", "content" => "AP C2S test"}, + "to" => "https://www.w3.org/ns/activitystreams#Public", + "cc" => [] + } + ] + end + + test "it rejects posts from other users / unauthenticated users", %{ + conn: conn, + activity: activity + } do + user = insert(:user) + other_user = insert(:user) + conn = put_req_header(conn, "content-type", "application/activity+json") + + conn + |> post("/users/#{user.nickname}/outbox", activity) + |> json_response(403) + + conn + |> assign(:user, other_user) + |> post("/users/#{user.nickname}/outbox", activity) + |> json_response(403) + end + + test "it inserts an incoming create activity into the database", %{ + conn: conn, + activity: activity + } do + user = insert(:user) + + result = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", activity) + |> json_response(201) + + assert Activity.get_by_ap_id(result["id"]) + assert result["object"] + assert %Object{data: object} = Object.normalize(result["object"]) + assert object["content"] == activity["object"]["content"] + end + + test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do + user = insert(:user) + + activity = + activity + |> put_in(["object", "type"], "Benis") + + _result = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", activity) + |> json_response(400) + end + + test "it inserts an incoming sensitive activity into the database", %{ + conn: conn, + activity: activity + } do + user = insert(:user) + conn = assign(conn, :user, user) + object = Map.put(activity["object"], "sensitive", true) + activity = Map.put(activity, "object", object) + + response = + conn + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", activity) + |> json_response(201) + + assert Activity.get_by_ap_id(response["id"]) + assert response["object"] + assert %Object{data: response_object} = Object.normalize(response["object"]) + assert response_object["sensitive"] == true + assert response_object["content"] == activity["object"]["content"] + + representation = + conn + |> put_req_header("accept", "application/activity+json") + |> get(response["id"]) + |> json_response(200) + + assert representation["object"]["sensitive"] == true + end + + test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do + user = insert(:user) + activity = Map.put(activity, "type", "BadType") + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", activity) + + assert json_response(conn, 400) + end + + test "it erects a tombstone when receiving a delete activity", %{conn: conn} do + note_activity = insert(:note_activity) + note_object = Object.normalize(note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + data = %{ + type: "Delete", + object: %{ + id: note_object.data["id"] + } + } + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", data) + + result = json_response(conn, 201) + assert Activity.get_by_ap_id(result["id"]) + + assert object = Object.get_by_ap_id(note_object.data["id"]) + assert object.data["type"] == "Tombstone" + end + + test "it rejects delete activity of object from other actor", %{conn: conn} do + note_activity = insert(:note_activity) + note_object = Object.normalize(note_activity) + user = insert(:user) + + data = %{ + type: "Delete", + object: %{ + id: note_object.data["id"] + } + } + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", data) + + assert json_response(conn, 400) + end + + test "it increases like count when receiving a like action", %{conn: conn} do + note_activity = insert(:note_activity) + note_object = Object.normalize(note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + data = %{ + type: "Like", + object: %{ + id: note_object.data["id"] + } + } + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", data) + + result = json_response(conn, 201) + assert Activity.get_by_ap_id(result["id"]) + + assert object = Object.get_by_ap_id(note_object.data["id"]) + assert object.data["like_count"] == 1 + end + + test "it doesn't spreads faulty attributedTo or actor fields", %{ + conn: conn, + activity: activity + } do + reimu = insert(:user, nickname: "reimu") + cirno = insert(:user, nickname: "cirno") + + assert reimu.ap_id + assert cirno.ap_id + + activity = + activity + |> put_in(["object", "actor"], reimu.ap_id) + |> put_in(["object", "attributedTo"], reimu.ap_id) + |> put_in(["actor"], reimu.ap_id) + |> put_in(["attributedTo"], reimu.ap_id) + + _reimu_outbox = + conn + |> assign(:user, cirno) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{reimu.nickname}/outbox", activity) + |> json_response(403) + + cirno_outbox = + conn + |> assign(:user, cirno) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{cirno.nickname}/outbox", activity) + |> json_response(201) + + assert cirno_outbox["attributedTo"] == nil + assert cirno_outbox["actor"] == cirno.ap_id + + assert cirno_object = Object.normalize(cirno_outbox["object"]) + assert cirno_object.data["actor"] == cirno.ap_id + assert cirno_object.data["attributedTo"] == cirno.ap_id + end + + test "Character limitation", %{conn: conn, activity: activity} do + Pleroma.Config.put([:instance, :limit], 5) + user = insert(:user) + + result = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", activity) + |> json_response(400) + + assert result == "Note is over the character limit" + end + end + + describe "/relay/followers" do + test "it returns relay followers", %{conn: conn} do + relay_actor = Relay.get_actor() + user = insert(:user) + User.follow(user, relay_actor) + + result = + conn + |> get("/relay/followers") + |> json_response(200) + + assert result["first"]["orderedItems"] == [user.ap_id] + end + + test "on non-federating instance, it returns 404", %{conn: conn} do + Config.put([:instance, :federating], false) + user = insert(:user) + + conn + |> assign(:user, user) + |> get("/relay/followers") + |> json_response(404) + end + end + + describe "/relay/following" do + test "it returns relay following", %{conn: conn} do + result = + conn + |> get("/relay/following") + |> json_response(200) + + assert result["first"]["orderedItems"] == [] + end + + test "on non-federating instance, it returns 404", %{conn: conn} do + Config.put([:instance, :federating], false) + user = insert(:user) + + conn + |> assign(:user, user) + |> get("/relay/following") + |> json_response(404) + end + end + + describe "/users/:nickname/followers" do + test "it returns the followers in a collection", %{conn: conn} do + user = insert(:user) + user_two = insert(:user) + User.follow(user, user_two) + + result = + conn + |> assign(:user, user_two) + |> get("/users/#{user_two.nickname}/followers") + |> json_response(200) + + assert result["first"]["orderedItems"] == [user.ap_id] + end + + test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do + user = insert(:user) + user_two = insert(:user, hide_followers: true) + User.follow(user, user_two) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user_two.nickname}/followers") + |> json_response(200) + + assert is_binary(result["first"]) + end + + test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user", + %{conn: conn} do + user = insert(:user) + other_user = insert(:user, hide_followers: true) + + result = + conn + |> assign(:user, user) + |> get("/users/#{other_user.nickname}/followers?page=1") + + assert result.status == 403 + assert result.resp_body == "" + end + + test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user", + %{conn: conn} do + user = insert(:user, hide_followers: true) + other_user = insert(:user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/followers?page=1") + |> json_response(200) + + assert result["totalItems"] == 1 + assert result["orderedItems"] == [other_user.ap_id] + end + + test "it works for more than 10 users", %{conn: conn} do + user = insert(:user) + + Enum.each(1..15, fn _ -> + other_user = insert(:user) + User.follow(other_user, user) + end) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/followers") + |> json_response(200) + + assert length(result["first"]["orderedItems"]) == 10 + assert result["first"]["totalItems"] == 15 + assert result["totalItems"] == 15 + + result = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/followers?page=2") + |> json_response(200) + + assert length(result["orderedItems"]) == 5 + assert result["totalItems"] == 15 + end + + test "does not require authentication", %{conn: conn} do + user = insert(:user) + + conn + |> get("/users/#{user.nickname}/followers") + |> json_response(200) + end + end + + describe "/users/:nickname/following" do + test "it returns the following in a collection", %{conn: conn} do + user = insert(:user) + user_two = insert(:user) + User.follow(user, user_two) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/following") + |> json_response(200) + + assert result["first"]["orderedItems"] == [user_two.ap_id] + end + + test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do + user = insert(:user) + user_two = insert(:user, hide_follows: true) + User.follow(user, user_two) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user_two.nickname}/following") + |> json_response(200) + + assert is_binary(result["first"]) + end + + test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user", + %{conn: conn} do + user = insert(:user) + user_two = insert(:user, hide_follows: true) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user_two.nickname}/following?page=1") + + assert result.status == 403 + assert result.resp_body == "" + end + + test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user", + %{conn: conn} do + user = insert(:user, hide_follows: true) + other_user = insert(:user) + {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/following?page=1") + |> json_response(200) + + assert result["totalItems"] == 1 + assert result["orderedItems"] == [other_user.ap_id] + end + + test "it works for more than 10 users", %{conn: conn} do + user = insert(:user) + + Enum.each(1..15, fn _ -> + user = User.get_cached_by_id(user.id) + other_user = insert(:user) + User.follow(user, other_user) + end) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/following") + |> json_response(200) + + assert length(result["first"]["orderedItems"]) == 10 + assert result["first"]["totalItems"] == 15 + assert result["totalItems"] == 15 + + result = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/following?page=2") + |> json_response(200) + + assert length(result["orderedItems"]) == 5 + assert result["totalItems"] == 15 + end + + test "does not require authentication", %{conn: conn} do + user = insert(:user) + + conn + |> get("/users/#{user.nickname}/following") + |> json_response(200) + end + end + + describe "delivery tracking" do + test "it tracks a signed object fetch", %{conn: conn} do + user = insert(:user, local: false) + activity = insert(:note_activity) + object = Object.normalize(activity) + + object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url()) + + conn + |> put_req_header("accept", "application/activity+json") + |> assign(:user, user) + |> get(object_path) + |> json_response(200) + + assert Delivery.get(object.id, user.id) + end + + test "it tracks a signed activity fetch", %{conn: conn} do + user = insert(:user, local: false) + activity = insert(:note_activity) + object = Object.normalize(activity) + + activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url()) + + conn + |> put_req_header("accept", "application/activity+json") + |> assign(:user, user) + |> get(activity_path) + |> json_response(200) + + assert Delivery.get(object.id, user.id) + end + + test "it tracks a signed object fetch when the json is cached", %{conn: conn} do + user = insert(:user, local: false) + other_user = insert(:user, local: false) + activity = insert(:note_activity) + object = Object.normalize(activity) + + object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url()) + + conn + |> put_req_header("accept", "application/activity+json") + |> assign(:user, user) + |> get(object_path) + |> json_response(200) + + build_conn() + |> put_req_header("accept", "application/activity+json") + |> assign(:user, other_user) + |> get(object_path) + |> json_response(200) + + assert Delivery.get(object.id, user.id) + assert Delivery.get(object.id, other_user.id) + end + + test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do + user = insert(:user, local: false) + other_user = insert(:user, local: false) + activity = insert(:note_activity) + object = Object.normalize(activity) + + activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url()) + + conn + |> put_req_header("accept", "application/activity+json") + |> assign(:user, user) + |> get(activity_path) + |> json_response(200) + + build_conn() + |> put_req_header("accept", "application/activity+json") + |> assign(:user, other_user) + |> get(activity_path) + |> json_response(200) + + assert Delivery.get(object.id, user.id) + assert Delivery.get(object.id, other_user.id) + end + end + + describe "Additional ActivityPub C2S endpoints" do + test "GET /api/ap/whoami", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> get("/api/ap/whoami") + + user = User.get_cached_by_id(user.id) + + assert UserView.render("user.json", %{user: user}) == json_response(conn, 200) + + conn + |> get("/api/ap/whoami") + |> json_response(403) + end + + setup do: clear_config([:media_proxy]) + setup do: clear_config([Pleroma.Upload]) + + test "POST /api/ap/upload_media", %{conn: conn} do + user = insert(:user) + + desc = "Description of the image" + + image = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + object = + conn + |> assign(:user, user) + |> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) + |> json_response(:created) + + assert object["name"] == desc + assert object["type"] == "Document" + assert object["actor"] == user.ap_id + assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"] + assert is_binary(object_href) + assert object_mediatype == "image/jpeg" + + activity_request = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "object" => %{ + "type" => "Note", + "content" => "AP C2S test, attachment", + "attachment" => [object] + }, + "to" => "https://www.w3.org/ns/activitystreams#Public", + "cc" => [] + } + + activity_response = + conn + |> assign(:user, user) + |> post("/users/#{user.nickname}/outbox", activity_request) + |> json_response(:created) + + assert activity_response["id"] + assert activity_response["object"] + assert activity_response["actor"] == user.ap_id + + assert %Object{data: %{"attachment" => [attachment]}} = + Object.normalize(activity_response["object"]) + + assert attachment["type"] == "Document" + assert attachment["name"] == desc + + assert [ + %{ + "href" => ^object_href, + "type" => "Link", + "mediaType" => ^object_mediatype + } + ] = attachment["url"] + + # Fails if unauthenticated + conn + |> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) + |> json_response(403) + end + end +end diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -0,0 +1,2260 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ActivityPubTest do + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Activity + alias Pleroma.Builders.ActivityBuilder + alias Pleroma.Config + alias Pleroma.Notification + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.CommonAPI + + import ExUnit.CaptureLog + import Mock + import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup do: clear_config([:instance, :federating]) + + describe "streaming out participations" do + test "it streams them out" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + + {:ok, conversation} = Pleroma.Conversation.create_or_bump_for(activity) + + participations = + conversation.participations + |> Repo.preload(:user) + + with_mock Pleroma.Web.Streamer, + stream: fn _, _ -> nil end do + ActivityPub.stream_out_participations(conversation.participations) + + assert called(Pleroma.Web.Streamer.stream("participation", participations)) + end + end + + test "streams them out on activity creation" do + user_one = insert(:user) + user_two = insert(:user) + + with_mock Pleroma.Web.Streamer, + stream: fn _, _ -> nil end do + {:ok, activity} = + CommonAPI.post(user_one, %{ + status: "@#{user_two.nickname}", + visibility: "direct" + }) + + conversation = + activity.data["context"] + |> Pleroma.Conversation.get_for_ap_id() + |> Repo.preload(participations: :user) + + assert called(Pleroma.Web.Streamer.stream("participation", conversation.participations)) + end + end + end + + describe "fetching restricted by visibility" do + test "it restricts by the appropriate visibility" do + user = insert(:user) + + {:ok, public_activity} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + + {:ok, direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + + {:ok, unlisted_activity} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + + {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"}) + + activities = ActivityPub.fetch_activities([], %{visibility: "direct", actor_id: user.ap_id}) + + assert activities == [direct_activity] + + activities = + ActivityPub.fetch_activities([], %{visibility: "unlisted", actor_id: user.ap_id}) + + assert activities == [unlisted_activity] + + activities = + ActivityPub.fetch_activities([], %{visibility: "private", actor_id: user.ap_id}) + + assert activities == [private_activity] + + activities = ActivityPub.fetch_activities([], %{visibility: "public", actor_id: user.ap_id}) + + assert activities == [public_activity] + + activities = + ActivityPub.fetch_activities([], %{ + visibility: ~w[private public], + actor_id: user.ap_id + }) + + assert activities == [public_activity, private_activity] + end + end + + describe "fetching excluded by visibility" do + test "it excludes by the appropriate visibility" do + user = insert(:user) + + {:ok, public_activity} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + + {:ok, direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + + {:ok, unlisted_activity} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + + {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"}) + + activities = + ActivityPub.fetch_activities([], %{ + exclude_visibilities: "direct", + actor_id: user.ap_id + }) + + assert public_activity in activities + assert unlisted_activity in activities + assert private_activity in activities + refute direct_activity in activities + + activities = + ActivityPub.fetch_activities([], %{ + exclude_visibilities: "unlisted", + actor_id: user.ap_id + }) + + assert public_activity in activities + refute unlisted_activity in activities + assert private_activity in activities + assert direct_activity in activities + + activities = + ActivityPub.fetch_activities([], %{ + exclude_visibilities: "private", + actor_id: user.ap_id + }) + + assert public_activity in activities + assert unlisted_activity in activities + refute private_activity in activities + assert direct_activity in activities + + activities = + ActivityPub.fetch_activities([], %{ + exclude_visibilities: "public", + actor_id: user.ap_id + }) + + refute public_activity in activities + assert unlisted_activity in activities + assert private_activity in activities + assert direct_activity in activities + end + end + + describe "building a user from his ap id" do + test "it returns a user" do + user_id = "http://mastodon.example.org/users/admin" + {: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 + + test "it returns a user that is invisible" do + user_id = "http://mastodon.example.org/users/relay" + {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) + assert User.invisible?(user) + end + + test "it returns a user that accepts chat messages" do + user_id = "http://mastodon.example.org/users/admin" + {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) + + assert user.accepts_chat_messages + end + end + + test "it fetches the appropriate tag-restricted posts" do + user = insert(:user) + + {:ok, status_one} = CommonAPI.post(user, %{status: ". #test"}) + {:ok, status_two} = CommonAPI.post(user, %{status: ". #essais"}) + {:ok, status_three} = CommonAPI.post(user, %{status: ". #test #reject"}) + + fetch_one = ActivityPub.fetch_activities([], %{type: "Create", tag: "test"}) + + fetch_two = ActivityPub.fetch_activities([], %{type: "Create", tag: ["test", "essais"]}) + + fetch_three = + ActivityPub.fetch_activities([], %{ + type: "Create", + tag: ["test", "essais"], + tag_reject: ["reject"] + }) + + fetch_four = + ActivityPub.fetch_activities([], %{ + type: "Create", + tag: ["test"], + tag_all: ["test", "reject"] + }) + + assert fetch_one == [status_one, status_three] + assert fetch_two == [status_one, status_two, status_three] + assert fetch_three == [status_one, status_two] + assert fetch_four == [status_three] + end + + describe "insertion" do + test "drops activities beyond a certain limit" do + limit = Config.get([:instance, :remote_limit]) + + random_text = + :crypto.strong_rand_bytes(limit + 1) + |> Base.encode64() + |> binary_part(0, limit + 1) + + data = %{ + "ok" => true, + "object" => %{ + "content" => random_text + } + } + + assert {:error, :remote_limit} = ActivityPub.insert(data) + end + + test "doesn't drop activities with content being null" do + user = insert(:user) + + data = %{ + "actor" => user.ap_id, + "to" => [], + "object" => %{ + "actor" => user.ap_id, + "to" => [], + "type" => "Note", + "content" => nil + } + } + + assert {:ok, _} = ActivityPub.insert(data) + end + + test "returns the activity if one with the same id is already in" do + activity = insert(:note_activity) + {:ok, new_activity} = ActivityPub.insert(activity.data) + + assert activity.id == new_activity.id + end + + test "inserts a given map into the activity database, giving it an id if it has none." do + user = insert(:user) + + data = %{ + "actor" => user.ap_id, + "to" => [], + "object" => %{ + "actor" => user.ap_id, + "to" => [], + "type" => "Note", + "content" => "hey" + } + } + + {:ok, %Activity{} = activity} = ActivityPub.insert(data) + assert activity.data["ok"] == data["ok"] + assert is_binary(activity.data["id"]) + + given_id = "bla" + + data = %{ + "id" => given_id, + "actor" => user.ap_id, + "to" => [], + "context" => "blabla", + "object" => %{ + "actor" => user.ap_id, + "to" => [], + "type" => "Note", + "content" => "hey" + } + } + + {:ok, %Activity{} = activity} = ActivityPub.insert(data) + 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 + user = insert(:user) + + data = %{ + "actor" => user.ap_id, + "to" => [], + "object" => %{ + "actor" => user.ap_id, + "to" => [], + "type" => "Note", + "content" => "hey" + } + } + + {:ok, %Activity{} = activity} = ActivityPub.insert(data) + object = Pleroma.Object.normalize(activity) + + 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 + user = insert(:user) + + data = %{ + "actor" => user.ap_id, + "to" => [], + "object" => %{ + "actor" => user.ap_id, + "to" => [], + "type" => "Note", + "content" => "hey" + } + } + + {:ok, %Activity{} = activity} = ActivityPub.insert(data) + assert object = Object.normalize(activity) + assert is_binary(object.data["id"]) + end + end + + describe "listen activities" do + test "does not increase user note count" do + user = insert(:user) + + {:ok, activity} = + ActivityPub.listen(%{ + to: ["https://www.w3.org/ns/activitystreams#Public"], + actor: user, + context: "", + object: %{ + "actor" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "artist" => "lain", + "title" => "lain radio episode 1", + "length" => 180_000, + "type" => "Audio" + } + }) + + assert activity.actor == user.ap_id + + user = User.get_cached_by_id(user.id) + assert user.note_count == 0 + end + + test "can be fetched into a timeline" do + _listen_activity_1 = insert(:listen) + _listen_activity_2 = insert(:listen) + _listen_activity_3 = insert(:listen) + + timeline = ActivityPub.fetch_activities([], %{type: ["Listen"]}) + + assert length(timeline) == 3 + end + end + + describe "create activities" do + setup do + [user: insert(:user)] + end + + test "it reverts create", %{user: user} do + with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do + assert {:error, :reverted} = + ActivityPub.create(%{ + to: ["user1", "user2"], + actor: user, + context: "", + object: %{ + "to" => ["user1", "user2"], + "type" => "Note", + "content" => "testing" + } + }) + end + + assert Repo.aggregate(Activity, :count, :id) == 0 + assert Repo.aggregate(Object, :count, :id) == 0 + end + + test "creates activity if expiration is not configured and expires_at is not passed", %{ + user: user + } do + clear_config([Pleroma.Workers.PurgeExpiredActivity, :enabled], false) + + assert {:ok, _} = + ActivityPub.create(%{ + to: ["user1", "user2"], + actor: user, + context: "", + object: %{ + "to" => ["user1", "user2"], + "type" => "Note", + "content" => "testing" + } + }) + end + + test "rejects activity if expires_at present but expiration is not configured", %{user: user} do + clear_config([Pleroma.Workers.PurgeExpiredActivity, :enabled], false) + + assert {:error, :expired_activities_disabled} = + ActivityPub.create(%{ + to: ["user1", "user2"], + actor: user, + context: "", + object: %{ + "to" => ["user1", "user2"], + "type" => "Note", + "content" => "testing" + }, + additional: %{ + "expires_at" => DateTime.utc_now() + } + }) + + assert Repo.aggregate(Activity, :count, :id) == 0 + assert Repo.aggregate(Object, :count, :id) == 0 + end + + test "removes doubled 'to' recipients", %{user: user} do + {:ok, activity} = + ActivityPub.create(%{ + to: ["user1", "user1", "user2"], + actor: user, + context: "", + object: %{ + "to" => ["user1", "user1", "user2"], + "type" => "Note", + "content" => "testing" + } + }) + + assert activity.data["to"] == ["user1", "user2"] + assert activity.actor == user.ap_id + assert activity.recipients == ["user1", "user2", user.ap_id] + end + + test "increases user note count only for public activities", %{user: user} do + {:ok, _} = + CommonAPI.post(User.get_cached_by_id(user.id), %{ + status: "1", + visibility: "public" + }) + + {:ok, _} = + CommonAPI.post(User.get_cached_by_id(user.id), %{ + status: "2", + visibility: "unlisted" + }) + + {:ok, _} = + CommonAPI.post(User.get_cached_by_id(user.id), %{ + status: "2", + visibility: "private" + }) + + {:ok, _} = + CommonAPI.post(User.get_cached_by_id(user.id), %{ + status: "3", + visibility: "direct" + }) + + user = User.get_cached_by_id(user.id) + assert user.note_count == 2 + end + + test "increases replies count", %{user: user} do + user2 = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "1", visibility: "public"}) + ap_id = activity.data["id"] + reply_data = %{status: "1", in_reply_to_status_id: activity.id} + + # public + {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "public")) + assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert object.data["repliesCount"] == 1 + + # unlisted + {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "unlisted")) + assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert object.data["repliesCount"] == 2 + + # private + {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "private")) + assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert object.data["repliesCount"] == 2 + + # direct + {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "direct")) + assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert object.data["repliesCount"] == 2 + end + end + + describe "fetch activities for recipients" do + test "retrieve the activities for certain recipients" do + {:ok, activity_one} = ActivityBuilder.insert(%{"to" => ["someone"]}) + {:ok, activity_two} = ActivityBuilder.insert(%{"to" => ["someone_else"]}) + {:ok, _activity_three} = ActivityBuilder.insert(%{"to" => ["noone"]}) + + activities = ActivityPub.fetch_activities(["someone", "someone_else"]) + assert length(activities) == 2 + assert activities == [activity_one, activity_two] + end + end + + describe "fetch activities in context" do + test "retrieves activities that have a given context" do + {:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"}) + {:ok, activity_two} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"}) + {:ok, _activity_three} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"}) + {:ok, _activity_four} = ActivityBuilder.insert(%{"type" => "Announce", "context" => "2hu"}) + activity_five = insert(:note_activity) + user = insert(:user) + + {:ok, _user_relationship} = User.block(user, %{ap_id: activity_five.data["actor"]}) + + activities = ActivityPub.fetch_activities_for_context("2hu", %{blocking_user: user}) + assert activities == [activity_two, activity] + end + + test "doesn't return activities with filtered words" do + user = insert(:user) + user_two = insert(:user) + insert(:filter, user: user, phrase: "test", hide: true) + + {:ok, %{id: id1, data: %{"context" => context}}} = CommonAPI.post(user, %{status: "1"}) + + {:ok, %{id: id2}} = CommonAPI.post(user_two, %{status: "2", in_reply_to_status_id: id1}) + + {:ok, %{id: id3} = user_activity} = + CommonAPI.post(user, %{status: "3 test?", in_reply_to_status_id: id2}) + + {:ok, %{id: id4} = filtered_activity} = + CommonAPI.post(user_two, %{status: "4 test!", in_reply_to_status_id: id3}) + + {:ok, _} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4}) + + activities = + context + |> ActivityPub.fetch_activities_for_context(%{user: user}) + |> Enum.map(& &1.id) + + assert length(activities) == 4 + assert user_activity.id in activities + refute filtered_activity.id in activities + end + end + + test "doesn't return blocked activities" do + activity_one = insert(:note_activity) + activity_two = insert(:note_activity) + activity_three = insert(:note_activity) + user = insert(:user) + booster = insert(:user) + {:ok, _user_relationship} = User.block(user, %{ap_id: activity_one.data["actor"]}) + + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + refute Enum.member?(activities, activity_one) + + {:ok, _user_block} = User.unblock(user, %{ap_id: activity_one.data["actor"]}) + + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + assert Enum.member?(activities, activity_one) + + {:ok, _user_relationship} = User.block(user, %{ap_id: activity_three.data["actor"]}) + {:ok, %{data: %{"object" => id}}} = CommonAPI.repeat(activity_three.id, booster) + %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) + activity_three = Activity.get_by_id(activity_three.id) + + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) + + assert Enum.member?(activities, activity_two) + refute Enum.member?(activities, activity_three) + refute Enum.member?(activities, boost_activity) + assert Enum.member?(activities, activity_one) + + activities = ActivityPub.fetch_activities([], %{blocking_user: nil, skip_preload: true}) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + assert Enum.member?(activities, boost_activity) + assert Enum.member?(activities, activity_one) + end + + test "doesn't return transitive interactions concerning blocked users" do + blocker = insert(:user) + blockee = insert(:user) + friend = insert(:user) + + {:ok, _user_relationship} = User.block(blocker, blockee) + + {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"}) + + {:ok, activity_two} = CommonAPI.post(friend, %{status: "hey! @#{blockee.nickname}"}) + + {:ok, activity_three} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) + + {:ok, activity_four} = CommonAPI.post(blockee, %{status: "hey! @#{blocker.nickname}"}) + + activities = ActivityPub.fetch_activities([], %{blocking_user: blocker}) + + assert Enum.member?(activities, activity_one) + refute Enum.member?(activities, activity_two) + refute Enum.member?(activities, activity_three) + refute Enum.member?(activities, activity_four) + end + + test "doesn't return announce activities with blocked users in 'to'" do + blocker = insert(:user) + blockee = insert(:user) + friend = insert(:user) + + {:ok, _user_relationship} = User.block(blocker, blockee) + + {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"}) + + {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) + + {:ok, activity_three} = CommonAPI.repeat(activity_two.id, friend) + + activities = + ActivityPub.fetch_activities([], %{blocking_user: blocker}) + |> Enum.map(fn act -> act.id end) + + assert Enum.member?(activities, activity_one.id) + refute Enum.member?(activities, activity_two.id) + refute Enum.member?(activities, activity_three.id) + end + + test "doesn't return announce activities with blocked users in 'cc'" do + blocker = insert(:user) + blockee = insert(:user) + friend = insert(:user) + + {:ok, _user_relationship} = User.block(blocker, blockee) + + {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"}) + + {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) + + assert object = Pleroma.Object.normalize(activity_two) + + data = %{ + "actor" => friend.ap_id, + "object" => object.data["id"], + "context" => object.data["context"], + "type" => "Announce", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [blockee.ap_id] + } + + assert {:ok, activity_three} = ActivityPub.insert(data) + + activities = + ActivityPub.fetch_activities([], %{blocking_user: blocker}) + |> Enum.map(fn act -> act.id end) + + assert Enum.member?(activities, activity_one.id) + refute Enum.member?(activities, activity_two.id) + refute Enum.member?(activities, activity_three.id) + end + + test "doesn't return activities from blocked domains" do + domain = "dogwhistle.zone" + domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"}) + note = insert(:note, %{data: %{"actor" => domain_user.ap_id}}) + activity = insert(:note_activity, %{note: note}) + user = insert(:user) + {:ok, user} = User.block_domain(user, domain) + + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) + + refute activity in activities + + followed_user = insert(:user) + CommonAPI.follow(user, followed_user) + {:ok, repeat_activity} = CommonAPI.repeat(activity.id, followed_user) + + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) + + refute repeat_activity in activities + end + + test "does return activities from followed users on blocked domains" do + domain = "meanies.social" + domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"}) + blocker = insert(:user) + + {:ok, blocker} = User.follow(blocker, domain_user) + {:ok, blocker} = User.block_domain(blocker, domain) + + assert User.following?(blocker, domain_user) + assert User.blocks_domain?(blocker, domain_user) + refute User.blocks?(blocker, domain_user) + + note = insert(:note, %{data: %{"actor" => domain_user.ap_id}}) + activity = insert(:note_activity, %{note: note}) + + activities = ActivityPub.fetch_activities([], %{blocking_user: blocker, skip_preload: true}) + + assert activity in activities + + # And check that if the guy we DO follow boosts someone else from their domain, + # that should be hidden + another_user = insert(:user, %{ap_id: "https://#{domain}/@meanie2"}) + bad_note = insert(:note, %{data: %{"actor" => another_user.ap_id}}) + bad_activity = insert(:note_activity, %{note: bad_note}) + {:ok, repeat_activity} = CommonAPI.repeat(bad_activity.id, domain_user) + + activities = ActivityPub.fetch_activities([], %{blocking_user: blocker, skip_preload: true}) + + refute repeat_activity in activities + end + + test "doesn't return muted activities" do + activity_one = insert(:note_activity) + activity_two = insert(:note_activity) + activity_three = insert(:note_activity) + user = insert(:user) + booster = insert(:user) + + activity_one_actor = User.get_by_ap_id(activity_one.data["actor"]) + {:ok, _user_relationships} = User.mute(user, activity_one_actor) + + activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + refute Enum.member?(activities, activity_one) + + # Calling with 'with_muted' will deliver muted activities, too. + activities = + ActivityPub.fetch_activities([], %{ + muting_user: user, + with_muted: true, + skip_preload: true + }) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + assert Enum.member?(activities, activity_one) + + {:ok, _user_mute} = User.unmute(user, activity_one_actor) + + activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + assert Enum.member?(activities, activity_one) + + activity_three_actor = User.get_by_ap_id(activity_three.data["actor"]) + {:ok, _user_relationships} = User.mute(user, activity_three_actor) + {:ok, %{data: %{"object" => id}}} = CommonAPI.repeat(activity_three.id, booster) + %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) + activity_three = Activity.get_by_id(activity_three.id) + + activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) + + assert Enum.member?(activities, activity_two) + refute Enum.member?(activities, activity_three) + refute Enum.member?(activities, boost_activity) + assert Enum.member?(activities, activity_one) + + activities = ActivityPub.fetch_activities([], %{muting_user: nil, skip_preload: true}) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + assert Enum.member?(activities, boost_activity) + assert Enum.member?(activities, activity_one) + end + + test "doesn't return thread muted activities" do + user = insert(:user) + _activity_one = insert(:note_activity) + note_two = insert(:note, data: %{"context" => "suya.."}) + activity_two = insert(:note_activity, note: note_two) + + {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two) + + assert [_activity_one] = ActivityPub.fetch_activities([], %{muting_user: user}) + end + + test "returns thread muted activities when with_muted is set" do + user = insert(:user) + _activity_one = insert(:note_activity) + note_two = insert(:note, data: %{"context" => "suya.."}) + activity_two = insert(:note_activity, note: note_two) + + {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two) + + assert [_activity_two, _activity_one] = + ActivityPub.fetch_activities([], %{muting_user: user, with_muted: true}) + end + + test "does include announces on request" do + activity_three = insert(:note_activity) + user = insert(:user) + booster = insert(:user) + + {:ok, user} = User.follow(user, booster) + + {:ok, announce} = CommonAPI.repeat(activity_three.id, booster) + + [announce_activity] = ActivityPub.fetch_activities([user.ap_id | User.following(user)]) + + assert announce_activity.id == announce.id + end + + test "excludes reblogs on request" do + user = insert(:user) + {:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user}) + {:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user}) + + [activity] = ActivityPub.fetch_user_activities(user, nil, %{exclude_reblogs: true}) + + assert activity == expected_activity + end + + describe "irreversible filters" do + setup do + user = insert(:user) + user_two = insert(:user) + + insert(:filter, user: user_two, phrase: "cofe", hide: true) + insert(:filter, user: user_two, phrase: "ok boomer", hide: true) + insert(:filter, user: user_two, phrase: "test", hide: false) + + params = %{ + type: ["Create", "Announce"], + user: user_two + } + + {:ok, %{user: user, user_two: user_two, params: params}} + end + + test "it returns statuses if they don't contain exact filter words", %{ + user: user, + params: params + } do + {:ok, _} = CommonAPI.post(user, %{status: "hey"}) + {:ok, _} = CommonAPI.post(user, %{status: "got cofefe?"}) + {:ok, _} = CommonAPI.post(user, %{status: "I am not a boomer"}) + {:ok, _} = CommonAPI.post(user, %{status: "ok boomers"}) + {:ok, _} = CommonAPI.post(user, %{status: "ccofee is not a word"}) + {:ok, _} = CommonAPI.post(user, %{status: "this is a test"}) + + activities = ActivityPub.fetch_activities([], params) + + assert Enum.count(activities) == 6 + end + + test "it does not filter user's own statuses", %{user_two: user_two, params: params} do + {:ok, _} = CommonAPI.post(user_two, %{status: "Give me some cofe!"}) + {:ok, _} = CommonAPI.post(user_two, %{status: "ok boomer"}) + + activities = ActivityPub.fetch_activities([], params) + + assert Enum.count(activities) == 2 + end + + test "it excludes statuses with filter words", %{user: user, params: params} do + {:ok, _} = CommonAPI.post(user, %{status: "Give me some cofe!"}) + {:ok, _} = CommonAPI.post(user, %{status: "ok boomer"}) + {:ok, _} = CommonAPI.post(user, %{status: "is it a cOfE?"}) + {:ok, _} = CommonAPI.post(user, %{status: "cofe is all I need"}) + {:ok, _} = CommonAPI.post(user, %{status: "— ok BOOMER\n"}) + + activities = ActivityPub.fetch_activities([], params) + + assert Enum.empty?(activities) + end + + test "it returns all statuses if user does not have any filters" do + another_user = insert(:user) + {:ok, _} = CommonAPI.post(another_user, %{status: "got cofe?"}) + {:ok, _} = CommonAPI.post(another_user, %{status: "test!"}) + + activities = + ActivityPub.fetch_activities([], %{ + type: ["Create", "Announce"], + user: another_user + }) + + assert Enum.count(activities) == 2 + end + end + + describe "public fetch activities" do + test "doesn't retrieve unlisted activities" do + user = insert(:user) + + {:ok, _unlisted_activity} = CommonAPI.post(user, %{status: "yeah", visibility: "unlisted"}) + + {:ok, listed_activity} = CommonAPI.post(user, %{status: "yeah"}) + + [activity] = ActivityPub.fetch_public_activities() + + assert activity == listed_activity + end + + test "retrieves public activities" do + _activities = ActivityPub.fetch_public_activities() + + %{public: public} = ActivityBuilder.public_and_non_public() + + activities = ActivityPub.fetch_public_activities() + assert length(activities) == 1 + assert Enum.at(activities, 0) == public + end + + test "retrieves a maximum of 20 activities" do + ActivityBuilder.insert_list(10) + expected_activities = ActivityBuilder.insert_list(20) + + activities = ActivityPub.fetch_public_activities() + + assert collect_ids(activities) == collect_ids(expected_activities) + assert length(activities) == 20 + end + + test "retrieves ids starting from a since_id" do + activities = ActivityBuilder.insert_list(30) + expected_activities = ActivityBuilder.insert_list(10) + since_id = List.last(activities).id + + activities = ActivityPub.fetch_public_activities(%{since_id: since_id}) + + assert collect_ids(activities) == collect_ids(expected_activities) + assert length(activities) == 10 + end + + test "retrieves ids up to max_id" do + ActivityBuilder.insert_list(10) + expected_activities = ActivityBuilder.insert_list(20) + + %{id: max_id} = + 10 + |> ActivityBuilder.insert_list() + |> List.first() + + activities = ActivityPub.fetch_public_activities(%{max_id: max_id}) + + assert length(activities) == 20 + assert collect_ids(activities) == collect_ids(expected_activities) + end + + test "paginates via offset/limit" do + _first_part_activities = ActivityBuilder.insert_list(10) + second_part_activities = ActivityBuilder.insert_list(10) + + later_activities = ActivityBuilder.insert_list(10) + + activities = ActivityPub.fetch_public_activities(%{page: "2", page_size: "20"}, :offset) + + assert length(activities) == 20 + + assert collect_ids(activities) == + collect_ids(second_part_activities) ++ collect_ids(later_activities) + end + + test "doesn't return reblogs for users for whom reblogs have been muted" do + activity = insert(:note_activity) + user = insert(:user) + booster = insert(:user) + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster) + + {:ok, activity} = CommonAPI.repeat(activity.id, booster) + + activities = ActivityPub.fetch_activities([], %{muting_user: user}) + + refute Enum.any?(activities, fn %{id: id} -> id == activity.id end) + end + + test "returns reblogs for users for whom reblogs have not been muted" do + activity = insert(:note_activity) + user = insert(:user) + booster = insert(:user) + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster) + {:ok, _reblog_mute} = CommonAPI.show_reblogs(user, booster) + + {:ok, activity} = CommonAPI.repeat(activity.id, booster) + + activities = ActivityPub.fetch_activities([], %{muting_user: user}) + + assert Enum.any?(activities, fn %{id: id} -> id == activity.id end) + end + end + + describe "uploading files" do + setup do + test_file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + %{test_file: test_file} + 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" + end + + test "it sets the default description depending on the configuration", %{test_file: file} do + clear_config([Pleroma.Upload, :default_description]) + + Pleroma.Config.put([Pleroma.Upload, :default_description], nil) + {:ok, %Object{} = object} = ActivityPub.upload(file) + assert object.data["name"] == "" + + Pleroma.Config.put([Pleroma.Upload, :default_description], :filename) + {:ok, %Object{} = object} = ActivityPub.upload(file) + assert object.data["name"] == "an_image.jpg" + + Pleroma.Config.put([Pleroma.Upload, :default_description], "unnamed attachment") + {:ok, %Object{} = object} = ActivityPub.upload(file) + assert object.data["name"] == "unnamed attachment" + end + + test "copies the file to the configured folder", %{test_file: file} do + clear_config([Pleroma.Upload, :default_description], :filename) + {:ok, %Object{} = object} = ActivityPub.upload(file) + assert object.data["name"] == "an_image.jpg" + end + + test "works with base64 encoded images" do + file = %{ + img: data_uri() + } + + {:ok, %Object{}} = ActivityPub.upload(file) + end + end + + describe "fetch the latest Follow" do + test "fetches the latest Follow activity" do + %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity) + follower = Repo.get_by(User, ap_id: activity.data["actor"]) + followed = Repo.get_by(User, ap_id: activity.data["object"]) + + assert activity == Utils.fetch_latest_follow(follower, followed) + end + end + + describe "unfollowing" do + test "it reverts unfollow activity" do + follower = insert(:user) + followed = insert(:user) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) + + with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do + assert {:error, :reverted} = ActivityPub.unfollow(follower, followed) + end + + activity = Activity.get_by_id(follow_activity.id) + assert activity.data["type"] == "Follow" + assert activity.data["actor"] == follower.ap_id + + assert activity.data["object"] == followed.ap_id + end + + test "creates an undo activity for the last follow" do + follower = insert(:user) + followed = insert(:user) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) + {:ok, activity} = ActivityPub.unfollow(follower, followed) + + assert activity.data["type"] == "Undo" + assert activity.data["actor"] == follower.ap_id + + embedded_object = activity.data["object"] + assert is_map(embedded_object) + assert embedded_object["type"] == "Follow" + assert embedded_object["object"] == followed.ap_id + assert embedded_object["id"] == follow_activity.data["id"] + end + + test "creates an undo activity for a pending follow request" do + follower = insert(:user) + followed = insert(:user, %{locked: true}) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) + {:ok, activity} = ActivityPub.unfollow(follower, followed) + + assert activity.data["type"] == "Undo" + assert activity.data["actor"] == follower.ap_id + + embedded_object = activity.data["object"] + assert is_map(embedded_object) + assert embedded_object["type"] == "Follow" + assert embedded_object["object"] == followed.ap_id + assert embedded_object["id"] == follow_activity.data["id"] + end + end + + describe "timeline post-processing" do + test "it filters broken threads" do + user1 = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + + {:ok, user1} = User.follow(user1, user3) + assert User.following?(user1, user3) + + {:ok, user2} = User.follow(user2, user3) + assert User.following?(user2, user3) + + {:ok, user3} = User.follow(user3, user2) + assert User.following?(user3, user2) + + {:ok, public_activity} = CommonAPI.post(user3, %{status: "hi 1"}) + + {:ok, private_activity_1} = CommonAPI.post(user3, %{status: "hi 2", visibility: "private"}) + + {:ok, private_activity_2} = + CommonAPI.post(user2, %{ + status: "hi 3", + visibility: "private", + in_reply_to_status_id: private_activity_1.id + }) + + {:ok, private_activity_3} = + CommonAPI.post(user3, %{ + status: "hi 4", + visibility: "private", + in_reply_to_status_id: private_activity_2.id + }) + + activities = + ActivityPub.fetch_activities([user1.ap_id | User.following(user1)]) + |> Enum.map(fn a -> a.id end) + + private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"]) + + assert [public_activity.id, private_activity_1.id, private_activity_3.id] == activities + + assert length(activities) == 3 + + activities = + ActivityPub.fetch_activities([user1.ap_id | User.following(user1)], %{user: user1}) + |> Enum.map(fn a -> a.id end) + + assert [public_activity.id, private_activity_1.id] == activities + assert length(activities) == 2 + end + end + + describe "flag/1" do + setup do + reporter = insert(:user) + target_account = insert(:user) + content = "foobar" + {:ok, activity} = CommonAPI.post(target_account, %{status: content}) + context = Utils.generate_context_id() + + reporter_ap_id = reporter.ap_id + target_ap_id = target_account.ap_id + activity_ap_id = activity.data["id"] + + activity_with_object = Activity.get_by_ap_id_with_object(activity_ap_id) + + {:ok, + %{ + reporter: reporter, + context: context, + target_account: target_account, + reported_activity: activity, + content: content, + activity_ap_id: activity_ap_id, + activity_with_object: activity_with_object, + reporter_ap_id: reporter_ap_id, + target_ap_id: target_ap_id + }} + end + + test "it can create a Flag activity", + %{ + reporter: reporter, + context: context, + target_account: target_account, + reported_activity: reported_activity, + content: content, + activity_ap_id: activity_ap_id, + activity_with_object: activity_with_object, + reporter_ap_id: reporter_ap_id, + target_ap_id: target_ap_id + } do + assert {:ok, activity} = + ActivityPub.flag(%{ + actor: reporter, + context: context, + account: target_account, + statuses: [reported_activity], + content: content + }) + + note_obj = %{ + "type" => "Note", + "id" => activity_ap_id, + "content" => content, + "published" => activity_with_object.object.data["published"], + "actor" => + AccountView.render("show.json", %{user: target_account, skip_visibility_check: true}) + } + + assert %Activity{ + actor: ^reporter_ap_id, + data: %{ + "type" => "Flag", + "content" => ^content, + "context" => ^context, + "object" => [^target_ap_id, ^note_obj] + } + } = activity + end + + test_with_mock "strips status data from Flag, before federating it", + %{ + reporter: reporter, + context: context, + target_account: target_account, + reported_activity: reported_activity, + content: content + }, + Utils, + [:passthrough], + [] do + {:ok, activity} = + ActivityPub.flag(%{ + actor: reporter, + context: context, + account: target_account, + statuses: [reported_activity], + content: content + }) + + new_data = + put_in(activity.data, ["object"], [target_account.ap_id, reported_activity.data["id"]]) + + assert_called(Utils.maybe_federate(%{activity | data: new_data})) + end + end + + test "fetch_activities/2 returns activities addressed to a list " do + user = insert(:user) + member = insert(:user) + {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.follow(list, member) + + {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) + + activity = Repo.preload(activity, :bookmark) + activity = %Activity{activity | thread_muted?: !!activity.thread_muted?} + + assert ActivityPub.fetch_activities([], %{user: user}) == [activity] + end + + def data_uri do + File.read!("test/fixtures/avatar_data_uri") + end + + describe "fetch_activities_bounded" do + test "fetches private posts for followed users" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "thought I looked cute might delete later :3", + visibility: "private" + }) + + [result] = ActivityPub.fetch_activities_bounded([user.follower_address], []) + assert result.id == activity.id + end + + test "fetches only public posts for other users" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe", visibility: "public"}) + + {:ok, _private_activity} = + CommonAPI.post(user, %{ + status: "why is tenshi eating a corndog so cute?", + visibility: "private" + }) + + [result] = ActivityPub.fetch_activities_bounded([], [user.follower_address]) + assert result.id == activity.id + end + end + + describe "fetch_follow_information_for_user" do + test "syncronizes following/followers counters" do + user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/fuser2/followers", + following_address: "http://localhost:4001/users/fuser2/following" + ) + + {:ok, info} = ActivityPub.fetch_follow_information_for_user(user) + assert info.follower_count == 527 + assert info.following_count == 267 + end + + test "detects hidden followers" do + mock(fn env -> + case env.url do + "http://localhost:4001/users/masto_closed/followers?page=1" -> + %Tesla.Env{status: 403, body: ""} + + _ -> + apply(HttpRequestMock, :request, [env]) + end + end) + + user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_closed/followers", + following_address: "http://localhost:4001/users/masto_closed/following" + ) + + {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) + assert follow_info.hide_followers == true + assert follow_info.hide_follows == false + end + + test "detects hidden follows" do + mock(fn env -> + case env.url do + "http://localhost:4001/users/masto_closed/following?page=1" -> + %Tesla.Env{status: 403, body: ""} + + _ -> + apply(HttpRequestMock, :request, [env]) + end + end) + + user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_closed/followers", + following_address: "http://localhost:4001/users/masto_closed/following" + ) + + {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) + assert follow_info.hide_followers == false + assert follow_info.hide_follows == true + end + + test "detects hidden follows/followers for friendica" do + user = + insert(:user, + local: false, + follower_address: "http://localhost:8080/followers/fuser3", + following_address: "http://localhost:8080/following/fuser3" + ) + + {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) + assert follow_info.hide_followers == true + assert follow_info.follower_count == 296 + assert follow_info.following_count == 32 + assert follow_info.hide_follows == true + end + + test "doesn't crash when follower and following counters are hidden" do + mock(fn env -> + case env.url do + "http://localhost:4001/users/masto_hidden_counters/following" -> + json(%{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "http://localhost:4001/users/masto_hidden_counters/followers" + }) + + "http://localhost:4001/users/masto_hidden_counters/following?page=1" -> + %Tesla.Env{status: 403, body: ""} + + "http://localhost:4001/users/masto_hidden_counters/followers" -> + json(%{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "http://localhost:4001/users/masto_hidden_counters/following" + }) + + "http://localhost:4001/users/masto_hidden_counters/followers?page=1" -> + %Tesla.Env{status: 403, body: ""} + end + end) + + user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_hidden_counters/followers", + following_address: "http://localhost:4001/users/masto_hidden_counters/following" + ) + + {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) + + assert follow_info.hide_followers == true + assert follow_info.follower_count == 0 + assert follow_info.hide_follows == true + assert follow_info.following_count == 0 + end + end + + describe "fetch_favourites/3" do + test "returns a favourite activities sorted by adds to favorite" do + user = insert(:user) + other_user = insert(:user) + user1 = insert(:user) + user2 = insert(:user) + {:ok, a1} = CommonAPI.post(user1, %{status: "bla"}) + {:ok, _a2} = CommonAPI.post(user2, %{status: "traps are happy"}) + {:ok, a3} = CommonAPI.post(user2, %{status: "Trees Are "}) + {:ok, a4} = CommonAPI.post(user2, %{status: "Agent Smith "}) + {:ok, a5} = CommonAPI.post(user1, %{status: "Red or Blue "}) + + {:ok, _} = CommonAPI.favorite(user, a4.id) + {:ok, _} = CommonAPI.favorite(other_user, a3.id) + {:ok, _} = CommonAPI.favorite(user, a3.id) + {:ok, _} = CommonAPI.favorite(other_user, a5.id) + {:ok, _} = CommonAPI.favorite(user, a5.id) + {:ok, _} = CommonAPI.favorite(other_user, a4.id) + {:ok, _} = CommonAPI.favorite(user, a1.id) + {:ok, _} = CommonAPI.favorite(other_user, a1.id) + result = ActivityPub.fetch_favourites(user) + + assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id] + + result = ActivityPub.fetch_favourites(user, %{limit: 2}) + assert Enum.map(result, & &1.id) == [a1.id, a5.id] + end + end + + describe "Move activity" do + test "create" 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 = insert(:user) + follower_move_opted_out = insert(:user, allow_following_move: false) + + User.follow(follower, old_user) + User.follow(follower_move_opted_out, old_user) + + assert User.following?(follower, old_user) + assert User.following?(follower_move_opted_out, 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}) + + refute User.following?(follower, old_user) + assert User.following?(follower, new_user) + + assert User.following?(follower_move_opted_out, old_user) + refute User.following?(follower_move_opted_out, new_user) + + activity = %Activity{activity | object: nil} + + assert [%Notification{activity: ^activity}] = Notification.for_user(follower) + + assert [%Notification{activity: ^activity}] = Notification.for_user(follower_move_opted_out) + end + + test "old user must be in the new user's `also_known_as` list" do + old_user = insert(:user) + new_user = insert(:user) + + assert {:error, "Target account must have the origin in `alsoKnownAs`"} = + ActivityPub.move(old_user, new_user) + end + end + + test "doesn't retrieve replies activities with exclude_replies" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "yeah"}) + + {:ok, _reply} = CommonAPI.post(user, %{status: "yeah", in_reply_to_status_id: activity.id}) + + [result] = ActivityPub.fetch_public_activities(%{exclude_replies: true}) + + assert result.id == activity.id + + assert length(ActivityPub.fetch_public_activities()) == 2 + end + + describe "replies filtering with public messages" do + setup :public_messages + + test "public timeline", %{users: %{u1: user}} do + activities_ids = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) + |> ActivityPub.fetch_public_activities() + |> Enum.map(& &1.id) + + assert length(activities_ids) == 16 + end + + test "public timeline with reply_visibility `following`", %{ + users: %{u1: user}, + u1: u1, + u2: u2, + u3: u3, + u4: u4, + activities: activities + } do + activities_ids = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) + |> ActivityPub.fetch_public_activities() + |> Enum.map(& &1.id) + + assert length(activities_ids) == 14 + + visible_ids = + Map.values(u1) ++ Map.values(u2) ++ Map.values(u4) ++ Map.values(activities) ++ [u3[:r1]] + + assert Enum.all?(visible_ids, &(&1 in activities_ids)) + end + + test "public timeline with reply_visibility `self`", %{ + users: %{u1: user}, + u1: u1, + u2: u2, + u3: u3, + u4: u4, + activities: activities + } do + activities_ids = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) + |> ActivityPub.fetch_public_activities() + |> Enum.map(& &1.id) + + assert length(activities_ids) == 10 + visible_ids = Map.values(u1) ++ [u2[:r1], u3[:r1], u4[:r1]] ++ Map.values(activities) + assert Enum.all?(visible_ids, &(&1 in activities_ids)) + end + + test "home timeline", %{ + users: %{u1: user}, + activities: activities, + u1: u1, + u2: u2, + u3: u3, + u4: u4 + } do + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_filtering_user, user) + + activities_ids = + ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) + |> Enum.map(& &1.id) + + assert length(activities_ids) == 13 + + visible_ids = + Map.values(u1) ++ + Map.values(u3) ++ + [ + activities[:a1], + activities[:a2], + activities[:a4], + u2[:r1], + u2[:r3], + u4[:r1], + u4[:r2] + ] + + assert Enum.all?(visible_ids, &(&1 in activities_ids)) + end + + test "home timeline with reply_visibility `following`", %{ + users: %{u1: user}, + activities: activities, + u1: u1, + u2: u2, + u3: u3, + u4: u4 + } do + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) + + activities_ids = + ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) + |> Enum.map(& &1.id) + + assert length(activities_ids) == 11 + + visible_ids = + Map.values(u1) ++ + [ + activities[:a1], + activities[:a2], + activities[:a4], + u2[:r1], + u2[:r3], + u3[:r1], + u4[:r1], + u4[:r2] + ] + + assert Enum.all?(visible_ids, &(&1 in activities_ids)) + end + + test "home timeline with reply_visibility `self`", %{ + users: %{u1: user}, + activities: activities, + u1: u1, + u2: u2, + u3: u3, + u4: u4 + } do + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) + + activities_ids = + ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) + |> Enum.map(& &1.id) + + assert length(activities_ids) == 9 + + visible_ids = + Map.values(u1) ++ + [ + activities[:a1], + activities[:a2], + activities[:a4], + u2[:r1], + u3[:r1], + u4[:r1] + ] + + assert Enum.all?(visible_ids, &(&1 in activities_ids)) + end + + test "filtering out announces where the user is the actor of the announced message" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + User.follow(user, other_user) + + {:ok, post} = CommonAPI.post(user, %{status: "yo"}) + {:ok, other_post} = CommonAPI.post(third_user, %{status: "yo"}) + {:ok, _announce} = CommonAPI.repeat(post.id, other_user) + {:ok, _announce} = CommonAPI.repeat(post.id, third_user) + {:ok, announce} = CommonAPI.repeat(other_post.id, other_user) + + params = %{ + type: ["Announce"] + } + + results = + [user.ap_id | User.following(user)] + |> ActivityPub.fetch_activities(params) + + assert length(results) == 3 + + params = %{ + type: ["Announce"], + announce_filtering_user: user + } + + [result] = + [user.ap_id | User.following(user)] + |> ActivityPub.fetch_activities(params) + + assert result.id == announce.id + end + end + + describe "replies filtering with private messages" do + setup :private_messages + + test "public timeline", %{users: %{u1: user}} do + activities_ids = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> ActivityPub.fetch_public_activities() + |> Enum.map(& &1.id) + + assert activities_ids == [] + end + + test "public timeline with default reply_visibility `following`", %{users: %{u1: user}} do + activities_ids = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) + |> Map.put(:user, user) + |> ActivityPub.fetch_public_activities() + |> Enum.map(& &1.id) + + assert activities_ids == [] + end + + test "public timeline with default reply_visibility `self`", %{users: %{u1: user}} do + activities_ids = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) + |> Map.put(:user, user) + |> ActivityPub.fetch_public_activities() + |> Enum.map(& &1.id) + + assert activities_ids == [] + + activities_ids = + %{} + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, nil) + |> ActivityPub.fetch_public_activities() + + assert activities_ids == [] + end + + test "home timeline", %{users: %{u1: user}} do + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + + activities_ids = + ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) + |> Enum.map(& &1.id) + + assert length(activities_ids) == 12 + end + + test "home timeline with default reply_visibility `following`", %{users: %{u1: user}} do + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) + + activities_ids = + ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) + |> Enum.map(& &1.id) + + assert length(activities_ids) == 12 + end + + test "home timeline with default reply_visibility `self`", %{ + users: %{u1: user}, + activities: activities, + u1: u1, + u2: u2, + u3: u3, + u4: u4 + } do + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) + + activities_ids = + ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) + |> Enum.map(& &1.id) + + assert length(activities_ids) == 10 + + visible_ids = + Map.values(u1) ++ Map.values(u4) ++ [u2[:r1], u3[:r1]] ++ Map.values(activities) + + assert Enum.all?(visible_ids, &(&1 in activities_ids)) + end + end + + defp public_messages(_) do + [u1, u2, u3, u4] = insert_list(4, :user) + {:ok, u1} = User.follow(u1, u2) + {:ok, u2} = User.follow(u2, u1) + {:ok, u1} = User.follow(u1, u4) + {:ok, u4} = User.follow(u4, u1) + + {:ok, u2} = User.follow(u2, u3) + {:ok, u3} = User.follow(u3, u2) + + {:ok, a1} = CommonAPI.post(u1, %{status: "Status"}) + + {:ok, r1_1} = + CommonAPI.post(u2, %{ + status: "@#{u1.nickname} reply from u2 to u1", + in_reply_to_status_id: a1.id + }) + + {:ok, r1_2} = + CommonAPI.post(u3, %{ + status: "@#{u1.nickname} reply from u3 to u1", + in_reply_to_status_id: a1.id + }) + + {:ok, r1_3} = + CommonAPI.post(u4, %{ + status: "@#{u1.nickname} reply from u4 to u1", + in_reply_to_status_id: a1.id + }) + + {:ok, a2} = CommonAPI.post(u2, %{status: "Status"}) + + {:ok, r2_1} = + CommonAPI.post(u1, %{ + status: "@#{u2.nickname} reply from u1 to u2", + in_reply_to_status_id: a2.id + }) + + {:ok, r2_2} = + CommonAPI.post(u3, %{ + status: "@#{u2.nickname} reply from u3 to u2", + in_reply_to_status_id: a2.id + }) + + {:ok, r2_3} = + CommonAPI.post(u4, %{ + status: "@#{u2.nickname} reply from u4 to u2", + in_reply_to_status_id: a2.id + }) + + {:ok, a3} = CommonAPI.post(u3, %{status: "Status"}) + + {:ok, r3_1} = + CommonAPI.post(u1, %{ + status: "@#{u3.nickname} reply from u1 to u3", + in_reply_to_status_id: a3.id + }) + + {:ok, r3_2} = + CommonAPI.post(u2, %{ + status: "@#{u3.nickname} reply from u2 to u3", + in_reply_to_status_id: a3.id + }) + + {:ok, r3_3} = + CommonAPI.post(u4, %{ + status: "@#{u3.nickname} reply from u4 to u3", + in_reply_to_status_id: a3.id + }) + + {:ok, a4} = CommonAPI.post(u4, %{status: "Status"}) + + {:ok, r4_1} = + CommonAPI.post(u1, %{ + status: "@#{u4.nickname} reply from u1 to u4", + in_reply_to_status_id: a4.id + }) + + {:ok, r4_2} = + CommonAPI.post(u2, %{ + status: "@#{u4.nickname} reply from u2 to u4", + in_reply_to_status_id: a4.id + }) + + {:ok, r4_3} = + CommonAPI.post(u3, %{ + status: "@#{u4.nickname} reply from u3 to u4", + in_reply_to_status_id: a4.id + }) + + {:ok, + users: %{u1: u1, u2: u2, u3: u3, u4: u4}, + activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id}, + u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id}, + u2: %{r1: r2_1.id, r2: r2_2.id, r3: r2_3.id}, + u3: %{r1: r3_1.id, r2: r3_2.id, r3: r3_3.id}, + u4: %{r1: r4_1.id, r2: r4_2.id, r3: r4_3.id}} + end + + defp private_messages(_) do + [u1, u2, u3, u4] = insert_list(4, :user) + {:ok, u1} = User.follow(u1, u2) + {:ok, u2} = User.follow(u2, u1) + {:ok, u1} = User.follow(u1, u3) + {:ok, u3} = User.follow(u3, u1) + {:ok, u1} = User.follow(u1, u4) + {:ok, u4} = User.follow(u4, u1) + + {:ok, u2} = User.follow(u2, u3) + {:ok, u3} = User.follow(u3, u2) + + {:ok, a1} = CommonAPI.post(u1, %{status: "Status", visibility: "private"}) + + {:ok, r1_1} = + CommonAPI.post(u2, %{ + status: "@#{u1.nickname} reply from u2 to u1", + in_reply_to_status_id: a1.id, + visibility: "private" + }) + + {:ok, r1_2} = + CommonAPI.post(u3, %{ + status: "@#{u1.nickname} reply from u3 to u1", + in_reply_to_status_id: a1.id, + visibility: "private" + }) + + {:ok, r1_3} = + CommonAPI.post(u4, %{ + status: "@#{u1.nickname} reply from u4 to u1", + in_reply_to_status_id: a1.id, + visibility: "private" + }) + + {:ok, a2} = CommonAPI.post(u2, %{status: "Status", visibility: "private"}) + + {:ok, r2_1} = + CommonAPI.post(u1, %{ + status: "@#{u2.nickname} reply from u1 to u2", + in_reply_to_status_id: a2.id, + visibility: "private" + }) + + {:ok, r2_2} = + CommonAPI.post(u3, %{ + status: "@#{u2.nickname} reply from u3 to u2", + in_reply_to_status_id: a2.id, + visibility: "private" + }) + + {:ok, a3} = CommonAPI.post(u3, %{status: "Status", visibility: "private"}) + + {:ok, r3_1} = + CommonAPI.post(u1, %{ + status: "@#{u3.nickname} reply from u1 to u3", + in_reply_to_status_id: a3.id, + visibility: "private" + }) + + {:ok, r3_2} = + CommonAPI.post(u2, %{ + status: "@#{u3.nickname} reply from u2 to u3", + in_reply_to_status_id: a3.id, + visibility: "private" + }) + + {:ok, a4} = CommonAPI.post(u4, %{status: "Status", visibility: "private"}) + + {:ok, r4_1} = + CommonAPI.post(u1, %{ + status: "@#{u4.nickname} reply from u1 to u4", + in_reply_to_status_id: a4.id, + visibility: "private" + }) + + {:ok, + users: %{u1: u1, u2: u2, u3: u3, u4: u4}, + activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id}, + u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id}, + u2: %{r1: r2_1.id, r2: r2_2.id}, + u3: %{r1: r3_1.id, r2: r3_2.id}, + u4: %{r1: r4_1.id}} + end + + describe "maybe_update_follow_information/1" do + setup do + clear_config([:instance, :external_user_synchronization], true) + + user = %{ + local: false, + ap_id: "https://gensokyo.2hu/users/raymoo", + following_address: "https://gensokyo.2hu/users/following", + follower_address: "https://gensokyo.2hu/users/followers", + type: "Person" + } + + %{user: user} + end + + test "logs an error when it can't fetch the info", %{user: user} do + assert capture_log(fn -> + ActivityPub.maybe_update_follow_information(user) + end) =~ "Follower/Following counter update for #{user.ap_id} failed" + end + + test "just returns the input if the user type is Application", %{ + user: user + } do + user = + user + |> Map.put(:type, "Application") + + refute capture_log(fn -> + assert ^user = ActivityPub.maybe_update_follow_information(user) + end) =~ "Follower/Following counter update for #{user.ap_id} failed" + end + + test "it just returns the input if the user has no following/follower addresses", %{ + user: user + } do + user = + user + |> Map.put(:following_address, nil) + |> Map.put(:follower_address, nil) + + refute capture_log(fn -> + assert ^user = ActivityPub.maybe_update_follow_information(user) + end) =~ "Follower/Following counter update for #{user.ap_id} failed" + end + end + + describe "global activity expiration" do + test "creates an activity expiration for local Create activities" do + clear_config([:mrf, :policies], Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy) + + {:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"}) + {:ok, follow} = ActivityBuilder.insert(%{"type" => "Follow", "context" => "3hu"}) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity.id}, + scheduled_at: + activity.inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> Timex.shift(days: 365) + ) + + refute_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: follow.id} + ) + end + end + + describe "handling of clashing nicknames" do + test "renames an existing user with a clashing nickname and a different ap id" do + orig_user = + insert( + :user, + local: false, + nickname: "admin@mastodon.example.org", + ap_id: "http://mastodon.example.org/users/harinezumigari" + ) + + %{ + nickname: orig_user.nickname, + ap_id: orig_user.ap_id <> "part_2" + } + |> ActivityPub.maybe_handle_clashing_nickname() + + user = User.get_by_id(orig_user.id) + + assert user.nickname == "#{orig_user.id}.admin@mastodon.example.org" + end + + test "does nothing with a clashing nickname and the same ap id" do + orig_user = + insert( + :user, + local: false, + nickname: "admin@mastodon.example.org", + ap_id: "http://mastodon.example.org/users/harinezumigari" + ) + + %{ + nickname: orig_user.nickname, + ap_id: orig_user.ap_id + } + |> ActivityPub.maybe_handle_clashing_nickname() + + user = User.get_by_id(orig_user.id) + + assert user.nickname == orig_user.nickname + end + end + + describe "reply filtering" do + test "`following` still contains announcements by friends" do + user = insert(:user) + followed = insert(:user) + not_followed = insert(:user) + + User.follow(user, followed) + + {:ok, followed_post} = CommonAPI.post(followed, %{status: "Hello"}) + + {:ok, not_followed_to_followed} = + CommonAPI.post(not_followed, %{ + status: "Also hello", + in_reply_to_status_id: followed_post.id + }) + + {:ok, retoot} = CommonAPI.repeat(not_followed_to_followed.id, followed) + + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:announce_filtering_user, user) + |> Map.put(:user, user) + + activities = + [user.ap_id | User.following(user)] + |> ActivityPub.fetch_activities(params) + + followed_post_id = followed_post.id + retoot_id = retoot.id + + assert [%{id: ^followed_post_id}, %{id: ^retoot_id}] = activities + + assert length(activities) == 2 + end + + # This test is skipped because, while this is the desired behavior, + # there seems to be no good way to achieve it with the method that + # we currently use for detecting to who a reply is directed. + # This is a TODO and should be fixed by a later rewrite of the code + # in question. + @tag skip: true + test "`following` still contains self-replies by friends" do + user = insert(:user) + followed = insert(:user) + not_followed = insert(:user) + + User.follow(user, followed) + + {:ok, followed_post} = CommonAPI.post(followed, %{status: "Hello"}) + {:ok, not_followed_post} = CommonAPI.post(not_followed, %{status: "Also hello"}) + + {:ok, _followed_to_not_followed} = + CommonAPI.post(followed, %{status: "sup", in_reply_to_status_id: not_followed_post.id}) + + {:ok, _followed_self_reply} = + CommonAPI.post(followed, %{status: "Also cofe", in_reply_to_status_id: followed_post.id}) + + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:announce_filtering_user, user) + |> Map.put(:user, user) + + activities = + [user.ap_id | User.following(user)] + |> ActivityPub.fetch_activities(params) + + assert length(activities) == 2 + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf/activity_expiration_policy_test.exs b/test/pleroma/web/activity_pub/mrf/activity_expiration_policy_test.exs @@ -0,0 +1,84 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do + use ExUnit.Case, async: true + alias Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy + + @id Pleroma.Web.Endpoint.url() <> "/activities/cofe" + @local_actor Pleroma.Web.Endpoint.url() <> "/users/cofe" + + test "adds `expires_at` property" do + assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} = + ActivityExpirationPolicy.filter(%{ + "id" => @id, + "actor" => @local_actor, + "type" => "Create", + "object" => %{"type" => "Note"} + }) + + assert Timex.diff(expires_at, DateTime.utc_now(), :days) == 364 + end + + test "keeps existing `expires_at` if it less than the config setting" do + expires_at = DateTime.utc_now() |> Timex.shift(days: 1) + + assert {:ok, %{"type" => "Create", "expires_at" => ^expires_at}} = + ActivityExpirationPolicy.filter(%{ + "id" => @id, + "actor" => @local_actor, + "type" => "Create", + "expires_at" => expires_at, + "object" => %{"type" => "Note"} + }) + end + + test "overwrites existing `expires_at` if it greater than the config setting" do + too_distant_future = DateTime.utc_now() |> Timex.shift(years: 2) + + assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} = + ActivityExpirationPolicy.filter(%{ + "id" => @id, + "actor" => @local_actor, + "type" => "Create", + "expires_at" => too_distant_future, + "object" => %{"type" => "Note"} + }) + + assert Timex.diff(expires_at, DateTime.utc_now(), :days) == 364 + end + + test "ignores remote activities" do + assert {:ok, activity} = + ActivityExpirationPolicy.filter(%{ + "id" => "https://example.com/123", + "actor" => "https://example.com/users/cofe", + "type" => "Create", + "object" => %{"type" => "Note"} + }) + + refute Map.has_key?(activity, "expires_at") + end + + test "ignores non-Create/Note activities" do + assert {:ok, activity} = + ActivityExpirationPolicy.filter(%{ + "id" => "https://example.com/123", + "actor" => "https://example.com/users/cofe", + "type" => "Follow" + }) + + refute Map.has_key?(activity, "expires_at") + + assert {:ok, activity} = + ActivityExpirationPolicy.filter(%{ + "id" => "https://example.com/123", + "actor" => "https://example.com/users/cofe", + "type" => "Create", + "object" => %{"type" => "Cofe"} + }) + + refute Map.has_key?(activity, "expires_at") + end +end diff --git a/test/pleroma/web/activity_pub/mrf/anti_followbot_policy_test.exs b/test/pleroma/web/activity_pub/mrf/anti_followbot_policy_test.exs @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy + + describe "blocking based on attributes" do + test "matches followbots by nickname" do + actor = insert(:user, %{nickname: "followbot@example.com"}) + target = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Follow", + "actor" => actor.ap_id, + "object" => target.ap_id, + "id" => "https://example.com/activities/1234" + } + + assert {:reject, "[AntiFollowbotPolicy]" <> _} = AntiFollowbotPolicy.filter(message) + end + + test "matches followbots by display name" do + actor = insert(:user, %{name: "Federation Bot"}) + target = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Follow", + "actor" => actor.ap_id, + "object" => target.ap_id, + "id" => "https://example.com/activities/1234" + } + + assert {:reject, "[AntiFollowbotPolicy]" <> _} = AntiFollowbotPolicy.filter(message) + end + end + + test "it allows non-followbots" do + actor = insert(:user) + target = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Follow", + "actor" => actor.ap_id, + "object" => target.ap_id, + "id" => "https://example.com/activities/1234" + } + + {:ok, _} = AntiFollowbotPolicy.filter(message) + end + + test "it gracefully handles nil display names" do + actor = insert(:user, %{name: nil}) + target = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Follow", + "actor" => actor.ap_id, + "object" => target.ap_id, + "id" => "https://example.com/activities/1234" + } + + {:ok, _} = AntiFollowbotPolicy.filter(message) + 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 @@ -0,0 +1,166 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + import ExUnit.CaptureLog + + alias Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy + + @linkless_message %{ + "type" => "Create", + "object" => %{ + "content" => "hi world!" + } + } + + @linkful_message %{ + "type" => "Create", + "object" => %{ + "content" => "<a href='https://example.com'>hi world!</a>" + } + } + + @response_message %{ + "type" => "Create", + "object" => %{ + "name" => "yes", + "type" => "Answer" + } + } + + describe "with new user" do + test "it allows posts without links" do + user = insert(:user, local: false) + + assert user.note_count == 0 + + message = + @linkless_message + |> Map.put("actor", user.ap_id) + + {:ok, _message} = AntiLinkSpamPolicy.filter(message) + end + + test "it disallows posts with links" do + user = insert(:user, local: false) + + assert user.note_count == 0 + + message = + @linkful_message + |> Map.put("actor", user.ap_id) + + {:reject, _} = AntiLinkSpamPolicy.filter(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) + + {:ok, _message} = AntiLinkSpamPolicy.filter(message) + end + end + + describe "with old user" do + test "it allows posts without links" do + user = insert(:user, note_count: 1) + + assert user.note_count == 1 + + message = + @linkless_message + |> Map.put("actor", user.ap_id) + + {:ok, _message} = AntiLinkSpamPolicy.filter(message) + end + + test "it allows posts with links" do + user = insert(:user, note_count: 1) + + assert user.note_count == 1 + + message = + @linkful_message + |> Map.put("actor", user.ap_id) + + {:ok, _message} = AntiLinkSpamPolicy.filter(message) + end + end + + describe "with followed new user" do + test "it allows posts without links" do + user = insert(:user, follower_count: 1) + + assert user.follower_count == 1 + + message = + @linkless_message + |> Map.put("actor", user.ap_id) + + {:ok, _message} = AntiLinkSpamPolicy.filter(message) + end + + test "it allows posts with links" do + user = insert(:user, follower_count: 1) + + assert user.follower_count == 1 + + message = + @linkful_message + |> Map.put("actor", user.ap_id) + + {:ok, _message} = AntiLinkSpamPolicy.filter(message) + end + end + + describe "with unknown actors" do + setup do + Tesla.Mock.mock(fn + %{method: :get, url: "http://invalid.actor"} -> + %Tesla.Env{status: 500, body: ""} + end) + + :ok + end + + test "it rejects posts without links" do + message = + @linkless_message + |> Map.put("actor", "http://invalid.actor") + + assert capture_log(fn -> + {:reject, _} = AntiLinkSpamPolicy.filter(message) + end) =~ "[error] Could not decode user at fetch http://invalid.actor" + end + + test "it rejects posts with links" do + message = + @linkful_message + |> Map.put("actor", "http://invalid.actor") + + assert capture_log(fn -> + {:reject, _} = AntiLinkSpamPolicy.filter(message) + end) =~ "[error] Could not decode user at fetch http://invalid.actor" + end + end + + describe "with contentless-objects" do + test "it does not reject them or error out" do + user = insert(:user, note_count: 1) + + message = + @response_message + |> Map.put("actor", user.ap_id) + + {:ok, _message} = AntiLinkSpamPolicy.filter(message) + 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 @@ -0,0 +1,92 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrependedTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.MRF.EnsureRePrepended + + describe "rewrites summary" do + test "it adds `re:` to summary object when child summary and parent summary equal" do + message = %{ + "type" => "Create", + "object" => %{ + "summary" => "object-summary", + "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "object-summary"}}} + } + } + + assert {:ok, res} = EnsureRePrepended.filter(message) + 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 + message = %{ + "type" => "Create", + "object" => %{ + "summary" => "object-summary", + "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "re: object-summary"}}} + } + } + + assert {:ok, res} = EnsureRePrepended.filter(message) + assert res["object"]["summary"] == "re: object-summary" + end + end + + describe "skip filter" do + test "it skip if type isn't 'Create'" do + message = %{ + "type" => "Annotation", + "object" => %{"summary" => "object-summary"} + } + + assert {:ok, res} = EnsureRePrepended.filter(message) + assert res == message + end + + test "it skip if summary is empty" do + message = %{ + "type" => "Create", + "object" => %{ + "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "summary"}}} + } + } + + assert {:ok, res} = EnsureRePrepended.filter(message) + assert res == message + end + + test "it skip if inReplyTo is empty" do + message = %{"type" => "Create", "object" => %{"summary" => "summary"}} + assert {:ok, res} = EnsureRePrepended.filter(message) + assert res == message + end + + test "it skip if parent and child summary isn't equal" do + message = %{ + "type" => "Create", + "object" => %{ + "summary" => "object-summary", + "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "summary"}}} + } + } + + assert {:ok, res} = EnsureRePrepended.filter(message) + assert res == message + end + + test "it skips if the object is only a reference" do + message = %{ + "type" => "Create", + "object" => "somereference" + } + + assert {:ok, res} = EnsureRePrepended.filter(message) + assert res == message + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs b/test/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs @@ -0,0 +1,60 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy + @public "https://www.w3.org/ns/activitystreams#Public" + + defp generate_messages(actor) do + {%{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{}, + "to" => [@public, "f"], + "cc" => [actor.follower_address, "d"] + }, + %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d", @public]}, + "to" => ["f", actor.follower_address], + "cc" => ["d", @public] + }} + end + + test "removes from the federated timeline by nickname heuristics 1" do + actor = insert(:user, %{nickname: "annoying_ebooks@example.com"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end + + test "removes from the federated timeline by nickname heuristics 2" do + actor = insert(:user, %{nickname: "cirnonewsnetworkbot@meow.cat"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end + + test "removes from the federated timeline by actor type Application" do + actor = insert(:user, %{actor_type: "Application"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end + + test "removes from the federated timeline by actor type Service" do + actor = insert(:user, %{actor_type: "Service"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end +end diff --git a/test/pleroma/web/activity_pub/mrf/hellthread_policy_test.exs b/test/pleroma/web/activity_pub/mrf/hellthread_policy_test.exs @@ -0,0 +1,92 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + + import Pleroma.Web.ActivityPub.MRF.HellthreadPolicy + + alias Pleroma.Web.CommonAPI + + setup do + user = insert(:user) + + message = %{ + "actor" => user.ap_id, + "cc" => [user.follower_address], + "type" => "Create", + "to" => [ + "https://www.w3.org/ns/activitystreams#Public", + "https://instance.tld/users/user1", + "https://instance.tld/users/user2", + "https://instance.tld/users/user3" + ], + "object" => %{ + "type" => "Note" + } + } + + [user: user, message: message] + end + + setup do: clear_config(:mrf_hellthread) + + test "doesn't die on chat messages" do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 2, reject_threshold: 0}) + + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post_chat_message(user, other_user, "moin") + + assert {:ok, _} = filter(activity.data) + end + + describe "reject" do + test "rejects the message if the recipient count is above reject_threshold", %{ + message: message + } do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 2}) + + assert {:reject, "[HellthreadPolicy] 3 recipients is over the limit of 2"} == + filter(message) + end + + test "does not reject the message if the recipient count is below reject_threshold", %{ + message: message + } do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 3}) + + assert {:ok, ^message} = filter(message) + end + end + + describe "delist" do + test "delists the message if the recipient count is above delist_threshold", %{ + user: user, + message: message + } do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 2, reject_threshold: 0}) + + {:ok, message} = filter(message) + assert user.follower_address in message["to"] + assert "https://www.w3.org/ns/activitystreams#Public" in message["cc"] + end + + test "does not delist the message if the recipient count is below delist_threshold", %{ + message: message + } do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 4, reject_threshold: 0}) + + assert {:ok, ^message} = filter(message) + end + end + + test "excludes follower collection and public URI from threshold count", %{message: message} do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 3}) + + assert {:ok, ^message} = filter(message) + 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 @@ -0,0 +1,225 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicyTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.MRF.KeywordPolicy + + setup do: clear_config(:mrf_keyword) + + setup do + Pleroma.Config.put([:mrf_keyword], %{reject: [], federated_timeline_removal: [], replace: []}) + end + + describe "rejecting based on keywords" do + test "rejects if string matches in content" do + Pleroma.Config.put([:mrf_keyword, :reject], ["pun"]) + + message = %{ + "type" => "Create", + "object" => %{ + "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 if string matches in summary" do + Pleroma.Config.put([:mrf_keyword, :reject], ["pun"]) + + message = %{ + "type" => "Create", + "object" => %{ + "summary" => "just a daily reminder that compLAINer is a good pun", + "content" => "" + } + } + + assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} = + KeywordPolicy.filter(message) + end + + test "rejects if regex matches in content" do + Pleroma.Config.put([:mrf_keyword, :reject], [~r/comp[lL][aA][iI][nN]er/]) + + assert true == + Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> + message = %{ + "type" => "Create", + "object" => %{ + "content" => "just a daily reminder that #{content} is a good pun", + "summary" => "" + } + } + + {:reject, "[KeywordPolicy] Matches with rejected keyword"} == + KeywordPolicy.filter(message) + end) + end + + test "rejects if regex matches in summary" do + Pleroma.Config.put([:mrf_keyword, :reject], [~r/comp[lL][aA][iI][nN]er/]) + + assert true == + Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> + message = %{ + "type" => "Create", + "object" => %{ + "summary" => "just a daily reminder that #{content} is a good pun", + "content" => "" + } + } + + {:reject, "[KeywordPolicy] Matches with rejected keyword"} == + KeywordPolicy.filter(message) + end) + end + end + + describe "delisting from ftl based on keywords" do + test "delists if string matches in content" do + Pleroma.Config.put([: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 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 + + test "delists if string matches in summary" do + Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], ["pun"]) + + message = %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "type" => "Create", + "object" => %{ + "summary" => "just a daily reminder that compLAINer is a good pun", + "content" => "" + } + } + + {: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 + + test "delists if regex matches in content" do + Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], [~r/comp[lL][aA][iI][nN]er/]) + + assert true == + Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{ + "content" => "just a daily reminder that #{content} is a good pun", + "summary" => "" + } + } + + {:ok, result} = KeywordPolicy.filter(message) + + ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] and + not (["https://www.w3.org/ns/activitystreams#Public"] == result["to"]) + end) + end + + test "delists if regex matches in summary" do + Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], [~r/comp[lL][aA][iI][nN]er/]) + + assert true == + Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{ + "summary" => "just a daily reminder that #{content} is a good pun", + "content" => "" + } + } + + {:ok, result} = KeywordPolicy.filter(message) + + ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] and + not (["https://www.w3.org/ns/activitystreams#Public"] == result["to"]) + end) + end + end + + describe "replacing keywords" do + test "replaces keyword if string matches in content" do + Pleroma.Config.put([:mrf_keyword, :replace], [{"opensource", "free software"}]) + + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{"content" => "ZFS is opensource", "summary" => ""} + } + + {:ok, %{"object" => %{"content" => result}}} = KeywordPolicy.filter(message) + assert result == "ZFS is free software" + end + + test "replaces keyword if string matches in summary" do + Pleroma.Config.put([:mrf_keyword, :replace], [{"opensource", "free software"}]) + + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{"summary" => "ZFS is opensource", "content" => ""} + } + + {:ok, %{"object" => %{"summary" => result}}} = KeywordPolicy.filter(message) + assert result == "ZFS is free software" + end + + test "replaces keyword if regex matches in content" do + Pleroma.Config.put([:mrf_keyword, :replace], [ + {~r/open(-|\s)?source\s?(software)?/, "free software"} + ]) + + assert true == + Enum.all?(["opensource", "open-source", "open source"], fn content -> + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{"content" => "ZFS is #{content}", "summary" => ""} + } + + {:ok, %{"object" => %{"content" => result}}} = KeywordPolicy.filter(message) + result == "ZFS is free software" + end) + end + + test "replaces keyword if regex matches in summary" do + Pleroma.Config.put([:mrf_keyword, :replace], [ + {~r/open(-|\s)?source\s?(software)?/, "free software"} + ]) + + assert true == + Enum.all?(["opensource", "open-source", "open source"], fn content -> + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{"summary" => "ZFS is #{content}", "content" => ""} + } + + {:ok, %{"object" => %{"summary" => result}}} = KeywordPolicy.filter(message) + result == "ZFS is free software" + end) + 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 @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do + use Pleroma.DataCase + + alias Pleroma.HTTP + alias Pleroma.Tests.ObanHelpers + alias Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy + + import Mock + + @message %{ + "type" => "Create", + "object" => %{ + "type" => "Note", + "content" => "content", + "attachment" => [ + %{"url" => [%{"href" => "http://example.com/image.jpg"}]} + ] + } + } + + setup do: clear_config([:media_proxy, :enabled], true) + + test "it prefetches media proxy URIs" do + with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do + MediaProxyWarmingPolicy.filter(@message) + + ObanHelpers.perform_all() + # Performing jobs which has been just enqueued + ObanHelpers.perform_all() + + assert called(HTTP.get(:_, :_, :_)) + end + end + + test "it does nothing when no attachments are present" do + object = + @message["object"] + |> Map.delete("attachment") + + message = + @message + |> Map.put("object", object) + + with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do + MediaProxyWarmingPolicy.filter(message) + refute called(HTTP.get(:_, :_, :_)) + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf/mention_policy_test.exs b/test/pleroma/web/activity_pub/mrf/mention_policy_test.exs @@ -0,0 +1,96 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicyTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.MRF.MentionPolicy + + setup do: clear_config(:mrf_mention) + + test "pass filter if allow list is empty" do + Pleroma.Config.delete([:mrf_mention]) + + message = %{ + "type" => "Create", + "to" => ["https://example.com/ok"], + "cc" => ["https://example.com/blocked"] + } + + assert MentionPolicy.filter(message) == {:ok, message} + end + + describe "allow" do + test "empty" do + Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) + + message = %{ + "type" => "Create" + } + + assert MentionPolicy.filter(message) == {:ok, message} + end + + test "to" do + Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) + + message = %{ + "type" => "Create", + "to" => ["https://example.com/ok"] + } + + assert MentionPolicy.filter(message) == {:ok, message} + end + + test "cc" do + Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) + + message = %{ + "type" => "Create", + "cc" => ["https://example.com/ok"] + } + + assert MentionPolicy.filter(message) == {:ok, message} + end + + test "both" do + Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) + + message = %{ + "type" => "Create", + "to" => ["https://example.com/ok"], + "cc" => ["https://example.com/ok2"] + } + + assert MentionPolicy.filter(message) == {:ok, message} + end + end + + describe "deny" do + test "to" do + Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) + + message = %{ + "type" => "Create", + "to" => ["https://example.com/blocked"] + } + + assert MentionPolicy.filter(message) == + {:reject, "[MentionPolicy] Rejected for mention of https://example.com/blocked"} + end + + test "cc" do + Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) + + message = %{ + "type" => "Create", + "to" => ["https://example.com/ok"], + "cc" => ["https://example.com/blocked"] + } + + assert MentionPolicy.filter(message) == + {:reject, "[MentionPolicy] Rejected for mention of https://example.com/blocked"} + end + 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 @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicyTest do + use Pleroma.DataCase + alias Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy + + test "it clears content object" do + message = %{ + "type" => "Create", + "object" => %{"content" => ".", "attachment" => "image"} + } + + assert {:ok, res} = NoPlaceholderTextPolicy.filter(message) + assert res["object"]["content"] == "" + + message = put_in(message, ["object", "content"], "<p>.</p>") + assert {:ok, res} = NoPlaceholderTextPolicy.filter(message) + assert res["object"]["content"] == "" + end + + @messages [ + %{ + "type" => "Create", + "object" => %{"content" => "test", "attachment" => "image"} + }, + %{"type" => "Create", "object" => %{"content" => "."}}, + %{"type" => "Create", "object" => %{"content" => "<p>.</p>"}} + ] + test "it skips filter" do + Enum.each(@messages, fn message -> + assert {:ok, res} = NoPlaceholderTextPolicy.filter(message) + assert res == message + end) + end +end diff --git a/test/pleroma/web/activity_pub/mrf/normalize_markup_test.exs b/test/pleroma/web/activity_pub/mrf/normalize_markup_test.exs @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkupTest do + use Pleroma.DataCase + alias Pleroma.Web.ActivityPub.MRF.NormalizeMarkup + + @html_sample """ + <b>this is in bold</b> + <p>this is a paragraph</p> + this is a linebreak<br /> + this is a link with allowed "rel" attribute: <a href="http://example.com/" rel="tag">example.com</a> + this is a link with not allowed "rel" attribute: <a href="http://example.com/" rel="tag noallowed">example.com</a> + this is an image: <img src="http://example.com/image.jpg"><br /> + <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;) + """ + + message = %{"type" => "Create", "object" => %{"content" => @html_sample}} + + assert {:ok, res} = NormalizeMarkup.filter(message) + assert res["object"]["content"] == expected + end + + test "it skips filter if type isn't `Create`" do + message = %{"type" => "Note", "object" => %{}} + + assert {:ok, res} = NormalizeMarkup.filter(message) + assert res == message + end +end diff --git a/test/pleroma/web/activity_pub/mrf/object_age_policy_test.exs b/test/pleroma/web/activity_pub/mrf/object_age_policy_test.exs @@ -0,0 +1,148 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do + use Pleroma.DataCase + alias Pleroma.Config + alias Pleroma.User + alias Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy + alias Pleroma.Web.ActivityPub.Visibility + + setup do: + clear_config(:mrf_object_age, + threshold: 172_800, + actions: [:delist, :strip_followers] + ) + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + defp get_old_message do + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + end + + defp get_new_message do + old_message = get_old_message() + + new_object = + old_message + |> Map.get("object") + |> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601()) + + old_message + |> Map.put("object", new_object) + end + + describe "with reject action" do + test "works with objects with empty to or cc fields" do + Config.put([:mrf_object_age, :actions], [:reject]) + + data = + get_old_message() + |> Map.put("cc", nil) + |> Map.put("to", nil) + + assert match?({:reject, _}, ObjectAgePolicy.filter(data)) + end + + test "it rejects an old post" do + Config.put([:mrf_object_age, :actions], [:reject]) + + data = get_old_message() + + assert match?({:reject, _}, ObjectAgePolicy.filter(data)) + end + + test "it allows a new post" do + Config.put([:mrf_object_age, :actions], [:reject]) + + data = get_new_message() + + assert match?({:ok, _}, ObjectAgePolicy.filter(data)) + end + end + + describe "with delist action" do + test "works with objects with empty to or cc fields" do + Config.put([:mrf_object_age, :actions], [:delist]) + + data = + get_old_message() + |> Map.put("cc", nil) + |> Map.put("to", nil) + + {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"]) + + {:ok, data} = ObjectAgePolicy.filter(data) + + assert Visibility.get_visibility(%{data: data}) == "unlisted" + end + + test "it delists an old post" do + Config.put([:mrf_object_age, :actions], [:delist]) + + data = get_old_message() + + {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"]) + + {:ok, data} = ObjectAgePolicy.filter(data) + + assert Visibility.get_visibility(%{data: data}) == "unlisted" + end + + test "it allows a new post" do + Config.put([:mrf_object_age, :actions], [:delist]) + + data = get_new_message() + + {:ok, _user} = User.get_or_fetch_by_ap_id(data["actor"]) + + assert match?({:ok, ^data}, ObjectAgePolicy.filter(data)) + end + end + + describe "with strip_followers action" do + test "works with objects with empty to or cc fields" do + Config.put([:mrf_object_age, :actions], [:strip_followers]) + + data = + get_old_message() + |> Map.put("cc", nil) + |> Map.put("to", nil) + + {:ok, user} = User.get_or_fetch_by_ap_id(data["actor"]) + + {:ok, data} = ObjectAgePolicy.filter(data) + + refute user.follower_address in data["to"] + refute user.follower_address in data["cc"] + end + + test "it strips followers collections from an old post" do + Config.put([:mrf_object_age, :actions], [:strip_followers]) + + data = get_old_message() + + {:ok, user} = User.get_or_fetch_by_ap_id(data["actor"]) + + {:ok, data} = ObjectAgePolicy.filter(data) + + refute user.follower_address in data["to"] + refute user.follower_address in data["cc"] + end + + test "it allows a new post" do + Config.put([:mrf_object_age, :actions], [:strip_followers]) + + data = get_new_message() + + {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"]) + + assert match?({:ok, ^data}, ObjectAgePolicy.filter(data)) + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf/reject_non_public_test.exs b/test/pleroma/web/activity_pub/mrf/reject_non_public_test.exs @@ -0,0 +1,100 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublicTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.ActivityPub.MRF.RejectNonPublic + + setup do: clear_config([:mrf_rejectnonpublic]) + + describe "public message" do + test "it's allowed when address is public" do + actor = insert(:user, follower_address: "test-address") + + message = %{ + "actor" => actor.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], + "type" => "Create" + } + + assert {:ok, message} = RejectNonPublic.filter(message) + end + + test "it's allowed when cc address contain public address" do + actor = insert(:user, follower_address: "test-address") + + message = %{ + "actor" => actor.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], + "type" => "Create" + } + + assert {:ok, message} = RejectNonPublic.filter(message) + end + end + + describe "followers message" do + test "it's allowed when addrer of message in the follower addresses of user and it enabled in config" do + actor = insert(:user, follower_address: "test-address") + + message = %{ + "actor" => actor.ap_id, + "to" => ["test-address"], + "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], + "type" => "Create" + } + + Pleroma.Config.put([:mrf_rejectnonpublic, :allow_followersonly], true) + assert {:ok, message} = RejectNonPublic.filter(message) + end + + test "it's rejected when addrer of message in the follower addresses of user and it disabled in config" do + actor = insert(:user, follower_address: "test-address") + + message = %{ + "actor" => actor.ap_id, + "to" => ["test-address"], + "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], + "type" => "Create" + } + + Pleroma.Config.put([:mrf_rejectnonpublic, :allow_followersonly], false) + assert {:reject, _} = RejectNonPublic.filter(message) + end + end + + describe "direct message" do + test "it's allows when direct messages are allow" do + actor = insert(:user) + + message = %{ + "actor" => actor.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Publid"], + "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], + "type" => "Create" + } + + Pleroma.Config.put([:mrf_rejectnonpublic, :allow_direct], true) + assert {:ok, message} = RejectNonPublic.filter(message) + end + + test "it's reject when direct messages aren't allow" do + actor = insert(:user) + + message = %{ + "actor" => actor.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Publid~~~"], + "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], + "type" => "Create" + } + + Pleroma.Config.put([:mrf_rejectnonpublic, :allow_direct], false) + assert {:reject, _} = RejectNonPublic.filter(message) + end + 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 @@ -0,0 +1,539 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Config + alias Pleroma.Web.ActivityPub.MRF.SimplePolicy + alias Pleroma.Web.CommonAPI + + setup do: + clear_config(:mrf_simple, + media_removal: [], + media_nsfw: [], + federated_timeline_removal: [], + report_removal: [], + reject: [], + followers_only: [], + accept: [], + avatar_removal: [], + banner_removal: [], + reject_deletes: [] + ) + + describe "when :media_removal" do + test "is empty" do + Config.put([:mrf_simple, :media_removal], []) + media_message = build_media_message() + local_message = build_local_message() + + assert SimplePolicy.filter(media_message) == {:ok, media_message} + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "has a matching host" do + Config.put([:mrf_simple, :media_removal], ["remote.instance"]) + media_message = build_media_message() + local_message = build_local_message() + + assert SimplePolicy.filter(media_message) == + {:ok, + media_message + |> Map.put("object", Map.delete(media_message["object"], "attachment"))} + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "match with wildcard domain" do + Config.put([:mrf_simple, :media_removal], ["*.remote.instance"]) + media_message = build_media_message() + local_message = build_local_message() + + assert SimplePolicy.filter(media_message) == + {:ok, + media_message + |> Map.put("object", Map.delete(media_message["object"], "attachment"))} + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + end + + describe "when :media_nsfw" do + test "is empty" do + Config.put([:mrf_simple, :media_nsfw], []) + media_message = build_media_message() + local_message = build_local_message() + + assert SimplePolicy.filter(media_message) == {:ok, media_message} + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "has a matching host" do + Config.put([:mrf_simple, :media_nsfw], ["remote.instance"]) + media_message = build_media_message() + local_message = build_local_message() + + assert SimplePolicy.filter(media_message) == + {:ok, + media_message + |> put_in(["object", "tag"], ["foo", "nsfw"]) + |> put_in(["object", "sensitive"], true)} + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "match with wildcard domain" do + Config.put([:mrf_simple, :media_nsfw], ["*.remote.instance"]) + media_message = build_media_message() + local_message = build_local_message() + + assert SimplePolicy.filter(media_message) == + {:ok, + media_message + |> put_in(["object", "tag"], ["foo", "nsfw"]) + |> put_in(["object", "sensitive"], true)} + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + end + + defp build_media_message do + %{ + "actor" => "https://remote.instance/users/bob", + "type" => "Create", + "object" => %{ + "attachment" => [%{}], + "tag" => ["foo"], + "sensitive" => false + } + } + end + + describe "when :report_removal" do + test "is empty" do + Config.put([:mrf_simple, :report_removal], []) + report_message = build_report_message() + local_message = build_local_message() + + assert SimplePolicy.filter(report_message) == {:ok, report_message} + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "has a matching host" do + Config.put([:mrf_simple, :report_removal], ["remote.instance"]) + report_message = build_report_message() + local_message = build_local_message() + + assert {:reject, _} = SimplePolicy.filter(report_message) + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "match with wildcard domain" do + Config.put([:mrf_simple, :report_removal], ["*.remote.instance"]) + report_message = build_report_message() + local_message = build_local_message() + + assert {:reject, _} = SimplePolicy.filter(report_message) + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + end + + defp build_report_message do + %{ + "actor" => "https://remote.instance/users/bob", + "type" => "Flag" + } + end + + describe "when :federated_timeline_removal" do + test "is empty" do + Config.put([:mrf_simple, :federated_timeline_removal], []) + {_, ftl_message} = build_ftl_actor_and_message() + local_message = build_local_message() + + assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message} + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "has a matching host" do + {actor, ftl_message} = build_ftl_actor_and_message() + + ftl_message_actor_host = + ftl_message + |> Map.fetch!("actor") + |> URI.parse() + |> Map.fetch!(:host) + + Config.put([:mrf_simple, :federated_timeline_removal], [ftl_message_actor_host]) + local_message = build_local_message() + + assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message) + assert actor.follower_address in ftl_message["to"] + refute actor.follower_address in ftl_message["cc"] + refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"] + assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"] + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "match with wildcard domain" do + {actor, ftl_message} = build_ftl_actor_and_message() + + ftl_message_actor_host = + ftl_message + |> Map.fetch!("actor") + |> URI.parse() + |> Map.fetch!(:host) + + Config.put([:mrf_simple, :federated_timeline_removal], ["*." <> ftl_message_actor_host]) + local_message = build_local_message() + + assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message) + assert actor.follower_address in ftl_message["to"] + refute actor.follower_address in ftl_message["cc"] + refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"] + assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"] + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "has a matching host but only as:Public in to" do + {_actor, ftl_message} = build_ftl_actor_and_message() + + ftl_message_actor_host = + ftl_message + |> Map.fetch!("actor") + |> URI.parse() + |> Map.fetch!(:host) + + ftl_message = Map.put(ftl_message, "cc", []) + + Config.put([:mrf_simple, :federated_timeline_removal], [ftl_message_actor_host]) + + assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message) + refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"] + assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"] + end + end + + defp build_ftl_actor_and_message do + actor = insert(:user) + + {actor, + %{ + "actor" => actor.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public", "http://foo.bar/baz"], + "cc" => [actor.follower_address, "http://foo.bar/qux"] + }} + end + + describe "when :reject" do + test "is empty" do + Config.put([:mrf_simple, :reject], []) + + remote_message = build_remote_message() + + assert SimplePolicy.filter(remote_message) == {:ok, remote_message} + end + + test "activity has a matching host" do + Config.put([:mrf_simple, :reject], ["remote.instance"]) + + remote_message = build_remote_message() + + assert {:reject, _} = SimplePolicy.filter(remote_message) + end + + test "activity matches with wildcard domain" do + Config.put([:mrf_simple, :reject], ["*.remote.instance"]) + + remote_message = build_remote_message() + + assert {:reject, _} = SimplePolicy.filter(remote_message) + end + + test "actor has a matching host" do + Config.put([:mrf_simple, :reject], ["remote.instance"]) + + remote_user = build_remote_user() + + assert {:reject, _} = SimplePolicy.filter(remote_user) + end + end + + describe "when :followers_only" do + test "is empty" do + Config.put([:mrf_simple, :followers_only], []) + {_, ftl_message} = build_ftl_actor_and_message() + local_message = build_local_message() + + assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message} + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "has a matching host" do + actor = insert(:user) + following_user = insert(:user) + non_following_user = insert(:user) + + {:ok, _, _, _} = CommonAPI.follow(following_user, actor) + + activity = %{ + "actor" => actor.ap_id, + "to" => [ + "https://www.w3.org/ns/activitystreams#Public", + following_user.ap_id, + non_following_user.ap_id + ], + "cc" => [actor.follower_address, "http://foo.bar/qux"] + } + + dm_activity = %{ + "actor" => actor.ap_id, + "to" => [ + following_user.ap_id, + non_following_user.ap_id + ], + "cc" => [] + } + + actor_domain = + activity + |> Map.fetch!("actor") + |> URI.parse() + |> Map.fetch!(:host) + + Config.put([:mrf_simple, :followers_only], [actor_domain]) + + assert {:ok, new_activity} = SimplePolicy.filter(activity) + assert actor.follower_address in new_activity["cc"] + assert following_user.ap_id in new_activity["to"] + refute "https://www.w3.org/ns/activitystreams#Public" in new_activity["to"] + refute "https://www.w3.org/ns/activitystreams#Public" in new_activity["cc"] + refute non_following_user.ap_id in new_activity["to"] + refute non_following_user.ap_id in new_activity["cc"] + + assert {:ok, new_dm_activity} = SimplePolicy.filter(dm_activity) + assert new_dm_activity["to"] == [following_user.ap_id] + assert new_dm_activity["cc"] == [] + end + end + + describe "when :accept" do + test "is empty" do + Config.put([:mrf_simple, :accept], []) + + local_message = build_local_message() + remote_message = build_remote_message() + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + assert SimplePolicy.filter(remote_message) == {:ok, remote_message} + end + + test "is not empty but activity doesn't have a matching host" do + Config.put([:mrf_simple, :accept], ["non.matching.remote"]) + + local_message = build_local_message() + remote_message = build_remote_message() + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + assert {:reject, _} = SimplePolicy.filter(remote_message) + end + + test "activity has a matching host" do + Config.put([:mrf_simple, :accept], ["remote.instance"]) + + local_message = build_local_message() + remote_message = build_remote_message() + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + assert SimplePolicy.filter(remote_message) == {:ok, remote_message} + end + + test "activity matches with wildcard domain" do + Config.put([:mrf_simple, :accept], ["*.remote.instance"]) + + local_message = build_local_message() + remote_message = build_remote_message() + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + assert SimplePolicy.filter(remote_message) == {:ok, remote_message} + end + + test "actor has a matching host" do + Config.put([:mrf_simple, :accept], ["remote.instance"]) + + remote_user = build_remote_user() + + assert SimplePolicy.filter(remote_user) == {:ok, remote_user} + end + end + + describe "when :avatar_removal" do + test "is empty" do + Config.put([:mrf_simple, :avatar_removal], []) + + remote_user = build_remote_user() + + assert SimplePolicy.filter(remote_user) == {:ok, remote_user} + end + + test "is not empty but it doesn't have a matching host" do + Config.put([:mrf_simple, :avatar_removal], ["non.matching.remote"]) + + remote_user = build_remote_user() + + assert SimplePolicy.filter(remote_user) == {:ok, remote_user} + end + + test "has a matching host" do + Config.put([:mrf_simple, :avatar_removal], ["remote.instance"]) + + remote_user = build_remote_user() + {:ok, filtered} = SimplePolicy.filter(remote_user) + + refute filtered["icon"] + end + + test "match with wildcard domain" do + Config.put([:mrf_simple, :avatar_removal], ["*.remote.instance"]) + + remote_user = build_remote_user() + {:ok, filtered} = SimplePolicy.filter(remote_user) + + refute filtered["icon"] + end + end + + describe "when :banner_removal" do + test "is empty" do + Config.put([:mrf_simple, :banner_removal], []) + + remote_user = build_remote_user() + + assert SimplePolicy.filter(remote_user) == {:ok, remote_user} + end + + test "is not empty but it doesn't have a matching host" do + Config.put([:mrf_simple, :banner_removal], ["non.matching.remote"]) + + remote_user = build_remote_user() + + assert SimplePolicy.filter(remote_user) == {:ok, remote_user} + end + + test "has a matching host" do + Config.put([:mrf_simple, :banner_removal], ["remote.instance"]) + + remote_user = build_remote_user() + {:ok, filtered} = SimplePolicy.filter(remote_user) + + refute filtered["image"] + end + + test "match with wildcard domain" do + Config.put([:mrf_simple, :banner_removal], ["*.remote.instance"]) + + remote_user = build_remote_user() + {:ok, filtered} = SimplePolicy.filter(remote_user) + + refute filtered["image"] + end + end + + describe "when :reject_deletes is empty" do + setup do: Config.put([:mrf_simple, :reject_deletes], []) + + test "it accepts deletions even from rejected servers" do + Config.put([:mrf_simple, :reject], ["remote.instance"]) + + deletion_message = build_remote_deletion_message() + + assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message} + end + + test "it accepts deletions even from non-whitelisted servers" do + Config.put([:mrf_simple, :accept], ["non.matching.remote"]) + + deletion_message = build_remote_deletion_message() + + assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message} + end + end + + describe "when :reject_deletes is not empty but it doesn't have a matching host" do + setup do: Config.put([:mrf_simple, :reject_deletes], ["non.matching.remote"]) + + test "it accepts deletions even from rejected servers" do + Config.put([:mrf_simple, :reject], ["remote.instance"]) + + deletion_message = build_remote_deletion_message() + + assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message} + end + + test "it accepts deletions even from non-whitelisted servers" do + Config.put([:mrf_simple, :accept], ["non.matching.remote"]) + + deletion_message = build_remote_deletion_message() + + assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message} + end + end + + describe "when :reject_deletes has a matching host" do + setup do: Config.put([:mrf_simple, :reject_deletes], ["remote.instance"]) + + test "it rejects the deletion" do + deletion_message = build_remote_deletion_message() + + assert {:reject, _} = SimplePolicy.filter(deletion_message) + end + end + + describe "when :reject_deletes match with wildcard domain" do + setup do: Config.put([:mrf_simple, :reject_deletes], ["*.remote.instance"]) + + test "it rejects the deletion" do + deletion_message = build_remote_deletion_message() + + assert {:reject, _} = SimplePolicy.filter(deletion_message) + end + end + + defp build_local_message do + %{ + "actor" => "#{Pleroma.Web.base_url()}/users/alice", + "to" => [], + "cc" => [] + } + end + + defp build_remote_message do + %{"actor" => "https://remote.instance/users/bob"} + end + + defp build_remote_user do + %{ + "id" => "https://remote.instance/users/bob", + "icon" => %{ + "url" => "http://example.com/image.jpg", + "type" => "Image" + }, + "image" => %{ + "url" => "http://example.com/image.jpg", + "type" => "Image" + }, + "type" => "Person" + } + end + + defp build_remote_deletion_message do + %{ + "type" => "Delete", + "actor" => "https://remote.instance/users/bob" + } + end +end 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 @@ -0,0 +1,68 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup do + emoji_path = Path.join(Config.get([:instance, :static_dir]), "emoji/stolen") + File.rm_rf!(emoji_path) + File.mkdir!(emoji_path) + + Pleroma.Emoji.reload() + + on_exit(fn -> + File.rm_rf!(emoji_path) + end) + + :ok + end + + test "does nothing by default" do + installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) + refute "firedfox" in installed_emoji + + message = %{ + "type" => "Create", + "object" => %{ + "emoji" => [{"firedfox", "https://example.org/emoji/firedfox.png"}], + "actor" => "https://example.org/users/admin" + } + } + + assert {:ok, message} == StealEmojiPolicy.filter(message) + + installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) + refute "firedfox" in installed_emoji + end + + test "Steals emoji on unknown shortcode from allowed remote host" do + installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) + refute "firedfox" in installed_emoji + + message = %{ + "type" => "Create", + "object" => %{ + "emoji" => [{"firedfox", "https://example.org/emoji/firedfox.png"}], + "actor" => "https://example.org/users/admin" + } + } + + clear_config([:mrf_steal_emoji, :hosts], ["example.org"]) + clear_config([:mrf_steal_emoji, :size_limit], 284_468) + + assert {:ok, message} == StealEmojiPolicy.filter(message) + + installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) + assert "firedfox" in installed_emoji + end +end diff --git a/test/pleroma/web/activity_pub/mrf/subchain_policy_test.exs b/test/pleroma/web/activity_pub/mrf/subchain_policy_test.exs @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicyTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.MRF.DropPolicy + alias Pleroma.Web.ActivityPub.MRF.SubchainPolicy + + @message %{ + "actor" => "https://banned.com", + "type" => "Create", + "object" => %{"content" => "hi"} + } + setup do: clear_config([:mrf_subchain, :match_actor]) + + test "it matches and processes subchains when the actor matches a configured target" do + Pleroma.Config.put([:mrf_subchain, :match_actor], %{ + ~r/^https:\/\/banned.com/s => [DropPolicy] + }) + + {:reject, _} = SubchainPolicy.filter(@message) + end + + test "it doesn't match and process subchains when the actor doesn't match a configured target" do + Pleroma.Config.put([:mrf_subchain, :match_actor], %{ + ~r/^https:\/\/borked.com/s => [DropPolicy] + }) + + {:ok, _message} = SubchainPolicy.filter(@message) + end +end diff --git a/test/pleroma/web/activity_pub/mrf/tag_policy_test.exs b/test/pleroma/web/activity_pub/mrf/tag_policy_test.exs @@ -0,0 +1,123 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.TagPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.ActivityPub.MRF.TagPolicy + @public "https://www.w3.org/ns/activitystreams#Public" + + describe "mrf_tag:disable-any-subscription" do + test "rejects message" do + actor = insert(:user, tags: ["mrf_tag:disable-any-subscription"]) + message = %{"object" => actor.ap_id, "type" => "Follow", "actor" => actor.ap_id} + assert {:reject, _} = TagPolicy.filter(message) + end + end + + describe "mrf_tag:disable-remote-subscription" do + test "rejects non-local follow requests" do + actor = insert(:user, tags: ["mrf_tag:disable-remote-subscription"]) + follower = insert(:user, tags: ["mrf_tag:disable-remote-subscription"], local: false) + message = %{"object" => actor.ap_id, "type" => "Follow", "actor" => follower.ap_id} + assert {:reject, _} = TagPolicy.filter(message) + end + + test "allows non-local follow requests" do + actor = insert(:user, tags: ["mrf_tag:disable-remote-subscription"]) + follower = insert(:user, tags: ["mrf_tag:disable-remote-subscription"], local: true) + message = %{"object" => actor.ap_id, "type" => "Follow", "actor" => follower.ap_id} + assert {:ok, message} = TagPolicy.filter(message) + end + end + + describe "mrf_tag:sandbox" do + test "removes from public timelines" do + actor = insert(:user, tags: ["mrf_tag:sandbox"]) + + message = %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{}, + "to" => [@public, "f"], + "cc" => [@public, "d"] + } + + except_message = %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d"]}, + "to" => ["f", actor.follower_address], + "cc" => ["d"] + } + + assert TagPolicy.filter(message) == {:ok, except_message} + end + end + + describe "mrf_tag:force-unlisted" do + test "removes from the federated timeline" do + actor = insert(:user, tags: ["mrf_tag:force-unlisted"]) + + message = %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{}, + "to" => [@public, "f"], + "cc" => [actor.follower_address, "d"] + } + + except_message = %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d", @public]}, + "to" => ["f", actor.follower_address], + "cc" => ["d", @public] + } + + assert TagPolicy.filter(message) == {:ok, except_message} + end + end + + describe "mrf_tag:media-strip" do + test "removes attachments" do + actor = insert(:user, tags: ["mrf_tag:media-strip"]) + + message = %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{"attachment" => ["file1"]} + } + + except_message = %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{} + } + + assert TagPolicy.filter(message) == {:ok, except_message} + end + end + + describe "mrf_tag:media-force-nsfw" do + test "Mark as sensitive on presence of attachments" do + actor = insert(:user, tags: ["mrf_tag:media-force-nsfw"]) + + message = %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{"tag" => ["test"], "attachment" => ["file1"]} + } + + except_message = %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{"tag" => ["test", "nsfw"], "attachment" => ["file1"], "sensitive" => true} + } + + assert TagPolicy.filter(message) == {:ok, except_message} + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf/user_allow_list_policy_test.exs b/test/pleroma/web/activity_pub/mrf/user_allow_list_policy_test.exs @@ -0,0 +1,31 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only +defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy + + setup do: clear_config(:mrf_user_allowlist) + + test "pass filter if allow list is empty" do + actor = insert(:user) + message = %{"actor" => actor.ap_id} + assert UserAllowListPolicy.filter(message) == {:ok, message} + end + + test "pass filter if allow list isn't empty and user in allow list" do + actor = insert(:user) + Pleroma.Config.put([:mrf_user_allowlist], %{"localhost" => [actor.ap_id, "test-ap-id"]}) + message = %{"actor" => actor.ap_id} + assert UserAllowListPolicy.filter(message) == {:ok, message} + end + + test "rejected if allow list isn't empty and user not in allow list" do + actor = insert(:user) + Pleroma.Config.put([:mrf_user_allowlist], %{"localhost" => ["test-ap-id"]}) + message = %{"actor" => actor.ap_id} + assert {:reject, _} = UserAllowListPolicy.filter(message) + end +end diff --git a/test/pleroma/web/activity_pub/mrf/vocabulary_policy_test.exs b/test/pleroma/web/activity_pub/mrf/vocabulary_policy_test.exs @@ -0,0 +1,106 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.MRF.VocabularyPolicy + + describe "accept" do + setup do: clear_config([:mrf_vocabulary, :accept]) + + test "it accepts based on parent activity type" do + Pleroma.Config.put([:mrf_vocabulary, :accept], ["Like"]) + + message = %{ + "type" => "Like", + "object" => "whatever" + } + + {:ok, ^message} = VocabularyPolicy.filter(message) + end + + test "it accepts based on child object type" do + Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"]) + + message = %{ + "type" => "Create", + "object" => %{ + "type" => "Note", + "content" => "whatever" + } + } + + {:ok, ^message} = VocabularyPolicy.filter(message) + end + + test "it does not accept disallowed child objects" do + Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"]) + + message = %{ + "type" => "Create", + "object" => %{ + "type" => "Article", + "content" => "whatever" + } + } + + {:reject, _} = VocabularyPolicy.filter(message) + end + + test "it does not accept disallowed parent types" do + Pleroma.Config.put([:mrf_vocabulary, :accept], ["Announce", "Note"]) + + message = %{ + "type" => "Create", + "object" => %{ + "type" => "Note", + "content" => "whatever" + } + } + + {:reject, _} = VocabularyPolicy.filter(message) + end + end + + describe "reject" do + setup do: clear_config([:mrf_vocabulary, :reject]) + + test "it rejects based on parent activity type" do + Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"]) + + message = %{ + "type" => "Like", + "object" => "whatever" + } + + {:reject, _} = VocabularyPolicy.filter(message) + end + + test "it rejects based on child object type" do + Pleroma.Config.put([:mrf_vocabulary, :reject], ["Note"]) + + message = %{ + "type" => "Create", + "object" => %{ + "type" => "Note", + "content" => "whatever" + } + } + + {:reject, _} = VocabularyPolicy.filter(message) + end + + test "it passes through objects that aren't disallowed" do + Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"]) + + message = %{ + "type" => "Announce", + "object" => "whatever" + } + + {:ok, ^message} = VocabularyPolicy.filter(message) + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf_test.exs b/test/pleroma/web/activity_pub/mrf_test.exs @@ -0,0 +1,90 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRFTest do + use ExUnit.Case, async: true + use Pleroma.Tests.Helpers + alias Pleroma.Web.ActivityPub.MRF + + test "subdomains_regex/1" do + assert MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) == [ + ~r/^unsafe.tld$/i, + ~r/^(.*\.)*unsafe.tld$/i + ] + end + + describe "subdomain_match/2" do + test "common domains" do + regexes = MRF.subdomains_regex(["unsafe.tld", "unsafe2.tld"]) + + assert regexes == [~r/^unsafe.tld$/i, ~r/^unsafe2.tld$/i] + + assert MRF.subdomain_match?(regexes, "unsafe.tld") + assert MRF.subdomain_match?(regexes, "unsafe2.tld") + + refute MRF.subdomain_match?(regexes, "example.com") + end + + test "wildcard domains with one subdomain" do + regexes = MRF.subdomains_regex(["*.unsafe.tld"]) + + assert regexes == [~r/^(.*\.)*unsafe.tld$/i] + + assert MRF.subdomain_match?(regexes, "unsafe.tld") + assert MRF.subdomain_match?(regexes, "sub.unsafe.tld") + refute MRF.subdomain_match?(regexes, "anotherunsafe.tld") + refute MRF.subdomain_match?(regexes, "unsafe.tldanother") + end + + test "wildcard domains with two subdomains" do + regexes = MRF.subdomains_regex(["*.unsafe.tld"]) + + assert regexes == [~r/^(.*\.)*unsafe.tld$/i] + + assert MRF.subdomain_match?(regexes, "unsafe.tld") + assert MRF.subdomain_match?(regexes, "sub.sub.unsafe.tld") + refute MRF.subdomain_match?(regexes, "sub.anotherunsafe.tld") + refute MRF.subdomain_match?(regexes, "sub.unsafe.tldanother") + end + + test "matches are case-insensitive" do + regexes = MRF.subdomains_regex(["UnSafe.TLD", "UnSAFE2.Tld"]) + + assert regexes == [~r/^UnSafe.TLD$/i, ~r/^UnSAFE2.Tld$/i] + + assert MRF.subdomain_match?(regexes, "UNSAFE.TLD") + assert MRF.subdomain_match?(regexes, "UNSAFE2.TLD") + assert MRF.subdomain_match?(regexes, "unsafe.tld") + assert MRF.subdomain_match?(regexes, "unsafe2.tld") + + refute MRF.subdomain_match?(regexes, "EXAMPLE.COM") + refute MRF.subdomain_match?(regexes, "example.com") + end + end + + describe "describe/0" do + test "it works as expected with noop policy" do + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.NoOpPolicy]) + + expected = %{ + mrf_policies: ["NoOpPolicy"], + exclusions: false + } + + {:ok, ^expected} = MRF.describe() + end + + test "it works as expected with mock policy" do + clear_config([:mrf, :policies], [MRFModuleMock]) + + expected = %{ + mrf_policies: ["MRFModuleMock"], + mrf_module_mock: "some config data", + exclusions: false + } + + {:ok, ^expected} = MRF.describe() + end + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/accept_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/accept_validation_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptValidationTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.ActivityPub.Pipeline + + import Pleroma.Factory + + setup do + follower = insert(:user) + followed = insert(:user, local: false) + + {:ok, follow_data, _} = Builder.follow(follower, followed) + {:ok, follow_activity, _} = Pipeline.common_pipeline(follow_data, local: true) + + {:ok, accept_data, _} = Builder.accept(followed, follow_activity) + + %{accept_data: accept_data, followed: followed} + end + + test "it validates a basic 'accept'", %{accept_data: accept_data} do + assert {:ok, _, _} = ObjectValidator.validate(accept_data, []) + end + + test "it fails when the actor doesn't exist", %{accept_data: accept_data} do + accept_data = + accept_data + |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") + + assert {:error, _} = ObjectValidator.validate(accept_data, []) + end + + test "it fails when the accepted activity doesn't exist", %{accept_data: accept_data} do + accept_data = + accept_data + |> Map.put("object", "https://gensokyo.2hu/users/raymoo/follows/1") + + assert {:error, _} = ObjectValidator.validate(accept_data, []) + end + + test "for an accepted follow, it only validates if the actor of the accept is the followed actor", + %{accept_data: accept_data} do + stranger = insert(:user) + + accept_data = + accept_data + |> Map.put("actor", stranger.ap_id) + + assert {:error, _} = ObjectValidator.validate(accept_data, []) + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/announce_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/announce_validation_test.exs @@ -0,0 +1,106 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidationTest do + use Pleroma.DataCase + + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "announces" do + setup do + user = insert(:user) + announcer = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) + + object = Object.normalize(post_activity, false) + {:ok, valid_announce, []} = Builder.announce(announcer, object) + + %{ + valid_announce: valid_announce, + user: user, + post_activity: post_activity, + announcer: announcer + } + end + + test "returns ok for a valid announce", %{valid_announce: valid_announce} do + assert {:ok, _object, _meta} = ObjectValidator.validate(valid_announce, []) + end + + test "returns an error if the object can't be found", %{valid_announce: valid_announce} do + without_object = + valid_announce + |> Map.delete("object") + + {:error, cng} = ObjectValidator.validate(without_object, []) + + assert {:object, {"can't be blank", [validation: :required]}} in cng.errors + + nonexisting_object = + valid_announce + |> Map.put("object", "https://gensokyo.2hu/objects/99999999") + + {:error, cng} = ObjectValidator.validate(nonexisting_object, []) + + assert {:object, {"can't find object", []}} in cng.errors + end + + test "returns an error if we don't have the actor", %{valid_announce: valid_announce} do + nonexisting_actor = + valid_announce + |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") + + {:error, cng} = ObjectValidator.validate(nonexisting_actor, []) + + assert {:actor, {"can't find user", []}} in cng.errors + end + + test "returns an error if the actor already announced the object", %{ + valid_announce: valid_announce, + announcer: announcer, + post_activity: post_activity + } do + _announce = CommonAPI.repeat(post_activity.id, announcer) + + {:error, cng} = ObjectValidator.validate(valid_announce, []) + + assert {:actor, {"already announced this object", []}} in cng.errors + assert {:object, {"already announced by this actor", []}} in cng.errors + end + + test "returns an error if the actor can't announce the object", %{ + announcer: announcer, + user: user + } do + {:ok, post_activity} = + CommonAPI.post(user, %{status: "a secret post", visibility: "private"}) + + object = Object.normalize(post_activity, false) + + # Another user can't announce it + {:ok, announce, []} = Builder.announce(announcer, object, public: false) + + {:error, cng} = ObjectValidator.validate(announce, []) + + assert {:actor, {"can not announce this object", []}} in cng.errors + + # The actor of the object can announce it + {:ok, announce, []} = Builder.announce(user, object, public: false) + + assert {:ok, _, _} = ObjectValidator.validate(announce, []) + + # The actor of the object can not announce it publicly + {:ok, announce, []} = Builder.announce(user, object, public: true) + + {:error, cng} = ObjectValidator.validate(announce, []) + + assert {:actor, {"can not announce this object publicly", []}} in cng.errors + end + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_validator_test.exs @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidatorTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator + alias Pleroma.Web.ActivityPub.Utils + + import Pleroma.Factory + + describe "Notes" do + setup do + user = insert(:user) + + note = %{ + "id" => Utils.generate_activity_id(), + "type" => "Note", + "actor" => user.ap_id, + "to" => [user.follower_address], + "cc" => [], + "content" => "Hellow this is content.", + "context" => "xxx", + "summary" => "a post" + } + + %{user: user, note: note} + end + + test "a basic note validates", %{note: note} do + %{valid?: true} = ArticleNoteValidator.cast_and_validate(note) + end + 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 @@ -0,0 +1,74 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidatorTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator + + import Pleroma.Factory + + describe "attachments" do + test "works with honkerific attachments" do + attachment = %{ + "mediaType" => "", + "name" => "", + "summary" => "298p3RG7j27tfsZ9RQ.jpg", + "type" => "Document", + "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg" + } + + assert {:ok, attachment} = + AttachmentValidator.cast_and_validate(attachment) + |> Ecto.Changeset.apply_action(:insert) + + assert attachment.mediaType == "application/octet-stream" + end + + test "it turns mastodon attachments into our attachments" do + attachment = %{ + "url" => + "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", + "type" => "Document", + "name" => nil, + "mediaType" => "image/jpeg" + } + + {:ok, attachment} = + AttachmentValidator.cast_and_validate(attachment) + |> Ecto.Changeset.apply_action(:insert) + + assert [ + %{ + href: + "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", + type: "Link", + mediaType: "image/jpeg" + } + ] = attachment.url + + assert attachment.mediaType == "image/jpeg" + end + + test "it handles our own uploads" do + user = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + {:ok, attachment} = + attachment.data + |> AttachmentValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) + + assert attachment.mediaType == "image/jpeg" + end + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/block_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/block_validation_test.exs @@ -0,0 +1,39 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.BlockValidationTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + + import Pleroma.Factory + + describe "blocks" do + setup do + user = insert(:user, local: false) + blocked = insert(:user) + + {:ok, valid_block, []} = Builder.block(user, blocked) + + %{user: user, valid_block: valid_block} + end + + test "validates a basic object", %{ + valid_block: valid_block + } do + assert {:ok, _block, []} = ObjectValidator.validate(valid_block, []) + end + + test "returns an error if we don't know the blocked user", %{ + valid_block: valid_block + } do + block = + valid_block + |> Map.put("object", "https://gensokyo.2hu/users/raymoo") + + assert {:error, _cng} = ObjectValidator.validate(block, []) + end + end +end 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 @@ -0,0 +1,212 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatValidationTest do + use Pleroma.DataCase + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "chat message create activities" do + test "it is invalid if the object already exists" do + user = insert(:user) + recipient = insert(:user) + {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "hey") + object = Object.normalize(activity, false) + + {:ok, create_data, _} = Builder.create(user, object.data, [recipient.ap_id]) + + {:error, cng} = ObjectValidator.validate(create_data, []) + + assert {:object, {"The object to create already exists", []}} in cng.errors + end + + test "it is invalid if the object data has a different `to` or `actor` field" do + user = insert(:user) + recipient = insert(:user) + {:ok, object_data, _} = Builder.chat_message(recipient, user.ap_id, "Hey") + + {:ok, create_data, _} = Builder.create(user, object_data, [recipient.ap_id]) + + {:error, cng} = ObjectValidator.validate(create_data, []) + + assert {:to, {"Recipients don't match with object recipients", []}} in cng.errors + assert {:actor, {"Actor doesn't match with object actor", []}} in cng.errors + end + end + + describe "chat messages" do + setup do + clear_config([:instance, :remote_limit]) + user = insert(:user) + recipient = insert(:user, local: false) + + {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey :firefox:") + + %{user: user, recipient: recipient, valid_chat_message: valid_chat_message} + end + + test "let's through some basic html", %{user: user, recipient: recipient} do + {:ok, valid_chat_message, _} = + Builder.chat_message( + user, + recipient.ap_id, + "hey <a href='https://example.org'>example</a> <script>alert('uguu')</script>" + ) + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["content"] == + "hey <a href=\"https://example.org\">example</a> alert(&#39;uguu&#39;)" + end + + test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert Map.put(valid_chat_message, "attachment", nil) == object + assert match?(%{"firefox" => _}, object["emoji"]) + end + + test "validates for a basic object with an attachment", %{ + valid_chat_message: valid_chat_message, + user: user + } do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + valid_chat_message = + valid_chat_message + |> Map.put("attachment", attachment.data) + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["attachment"] + end + + test "validates for a basic object with an attachment in an array", %{ + valid_chat_message: valid_chat_message, + user: user + } do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + valid_chat_message = + valid_chat_message + |> Map.put("attachment", [attachment.data]) + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["attachment"] + end + + test "validates for a basic object with an attachment but without content", %{ + valid_chat_message: valid_chat_message, + user: user + } do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + valid_chat_message = + valid_chat_message + |> Map.put("attachment", attachment.data) + |> Map.delete("content") + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["attachment"] + end + + test "does not validate if the message has no content", %{ + valid_chat_message: valid_chat_message + } do + contentless = + valid_chat_message + |> Map.delete("content") + + refute match?({:ok, _object, _meta}, ObjectValidator.validate(contentless, [])) + end + + test "does not validate if the message is longer than the remote_limit", %{ + valid_chat_message: valid_chat_message + } do + Pleroma.Config.put([:instance, :remote_limit], 2) + refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) + end + + test "does not validate if the recipient is blocking the actor", %{ + valid_chat_message: valid_chat_message, + user: user, + recipient: recipient + } do + Pleroma.User.block(recipient, user) + refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) + end + + test "does not validate if the recipient is not accepting chat messages", %{ + valid_chat_message: valid_chat_message, + recipient: recipient + } do + recipient + |> Ecto.Changeset.change(%{accepts_chat_messages: false}) + |> Pleroma.Repo.update!() + + refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) + end + + test "does not validate if the actor or the recipient is not in our system", %{ + valid_chat_message: valid_chat_message + } do + chat_message = + valid_chat_message + |> Map.put("actor", "https://raymoo.com/raymoo") + + {:error, _} = ObjectValidator.validate(chat_message, []) + + chat_message = + valid_chat_message + |> Map.put("to", ["https://raymoo.com/raymoo"]) + + {:error, _} = ObjectValidator.validate(chat_message, []) + end + + test "does not validate for a message with multiple recipients", %{ + valid_chat_message: valid_chat_message, + user: user, + recipient: recipient + } do + chat_message = + valid_chat_message + |> Map.put("to", [user.ap_id, recipient.ap_id]) + + assert {:error, _} = ObjectValidator.validate(chat_message, []) + end + + test "does not validate if it doesn't concern local users" do + user = insert(:user, local: false) + recipient = insert(:user, local: false) + + {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey") + assert {:error, _} = ObjectValidator.validate(valid_chat_message, []) + end + 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 @@ -0,0 +1,106 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidationTest do + use Pleroma.DataCase + + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "deletes" do + setup do + user = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{status: "cancel me daddy"}) + + {:ok, valid_post_delete, _} = Builder.delete(user, post_activity.data["object"]) + {:ok, valid_user_delete, _} = Builder.delete(user, user.ap_id) + + %{user: user, valid_post_delete: valid_post_delete, valid_user_delete: valid_user_delete} + end + + test "it is valid for a post deletion", %{valid_post_delete: valid_post_delete} do + {:ok, valid_post_delete, _} = ObjectValidator.validate(valid_post_delete, []) + + assert valid_post_delete["deleted_activity_id"] + end + + test "it is invalid if the object isn't in a list of certain types", %{ + valid_post_delete: valid_post_delete + } do + object = Object.get_by_ap_id(valid_post_delete["object"]) + + data = + object.data + |> Map.put("type", "Like") + + {:ok, _object} = + object + |> Ecto.Changeset.change(%{data: data}) + |> Object.update_and_set_cache() + + {:error, cng} = ObjectValidator.validate(valid_post_delete, []) + assert {:object, {"object not in allowed types", []}} in cng.errors + end + + test "it is valid for a user deletion", %{valid_user_delete: valid_user_delete} do + assert match?({:ok, _, _}, ObjectValidator.validate(valid_user_delete, [])) + end + + test "it's invalid if the id is missing", %{valid_post_delete: valid_post_delete} do + no_id = + valid_post_delete + |> Map.delete("id") + + {:error, cng} = ObjectValidator.validate(no_id, []) + + assert {:id, {"can't be blank", [validation: :required]}} in cng.errors + end + + test "it's invalid if the object doesn't exist", %{valid_post_delete: valid_post_delete} do + missing_object = + valid_post_delete + |> Map.put("object", "http://does.not/exist") + + {:error, cng} = ObjectValidator.validate(missing_object, []) + + assert {:object, {"can't find object", []}} in cng.errors + end + + test "it's invalid if the actor of the object and the actor of delete are from different domains", + %{valid_post_delete: valid_post_delete} do + valid_user = insert(:user) + + valid_other_actor = + valid_post_delete + |> Map.put("actor", valid_user.ap_id) + + assert match?({:ok, _, _}, ObjectValidator.validate(valid_other_actor, [])) + + invalid_other_actor = + valid_post_delete + |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") + + {:error, cng} = ObjectValidator.validate(invalid_other_actor, []) + + 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", + %{valid_post_delete: valid_post_delete} do + user = + insert(:user, local: true, is_moderator: true, ap_id: "https://gensokyo.2hu/users/raymoo") + + valid_other_actor = + valid_post_delete + |> Map.put("actor", user.ap_id) + + {:ok, _, meta} = ObjectValidator.validate(valid_other_actor, []) + assert meta[:do_not_federate] + 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 @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "EmojiReacts" do + setup do + user = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) + + object = Pleroma.Object.get_by_ap_id(post_activity.data["object"]) + + {:ok, valid_emoji_react, []} = Builder.emoji_react(user, object, "👌") + + %{user: user, post_activity: post_activity, valid_emoji_react: valid_emoji_react} + end + + test "it validates a valid EmojiReact", %{valid_emoji_react: valid_emoji_react} do + assert {:ok, _, _} = ObjectValidator.validate(valid_emoji_react, []) + end + + test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emoji_react} do + without_content = + valid_emoji_react + |> Map.delete("content") + + {:error, cng} = ObjectValidator.validate(without_content, []) + + refute cng.valid? + 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 + without_emoji_content = + valid_emoji_react + |> Map.put("content", "x") + + {:error, cng} = ObjectValidator.validate(without_emoji_content, []) + + refute cng.valid? + + assert {:content, {"must be a single character emoji", []}} in cng.errors + end + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/follow_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/follow_validation_test.exs @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidationTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + + import Pleroma.Factory + + describe "Follows" do + setup do + follower = insert(:user) + followed = insert(:user) + + {:ok, valid_follow, []} = Builder.follow(follower, followed) + %{follower: follower, followed: followed, valid_follow: valid_follow} + end + + test "validates a basic follow object", %{valid_follow: valid_follow} do + assert {:ok, _follow, []} = ObjectValidator.validate(valid_follow, []) + end + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/like_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/like_validation_test.exs @@ -0,0 +1,113 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidationTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "likes" do + setup do + user = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) + + valid_like = %{ + "to" => [user.ap_id], + "cc" => [], + "type" => "Like", + "id" => Utils.generate_activity_id(), + "object" => post_activity.data["object"], + "actor" => user.ap_id, + "context" => "a context" + } + + %{valid_like: valid_like, user: user, post_activity: post_activity} + end + + test "returns ok when called in the ObjectValidator", %{valid_like: valid_like} do + {:ok, object, _meta} = ObjectValidator.validate(valid_like, []) + + assert "id" in Map.keys(object) + end + + test "is valid for a valid object", %{valid_like: valid_like} do + assert LikeValidator.cast_and_validate(valid_like).valid? + end + + test "sets the 'to' field to the object actor if no recipients are given", %{ + valid_like: valid_like, + user: user + } do + without_recipients = + valid_like + |> Map.delete("to") + + {:ok, object, _meta} = ObjectValidator.validate(without_recipients, []) + + assert object["to"] == [user.ap_id] + end + + test "sets the context field to the context of the object if no context is given", %{ + valid_like: valid_like, + post_activity: post_activity + } do + without_context = + valid_like + |> Map.delete("context") + + {:ok, object, _meta} = ObjectValidator.validate(without_context, []) + + assert object["context"] == post_activity.data["context"] + end + + test "it errors when the actor is missing or not known", %{valid_like: valid_like} do + without_actor = Map.delete(valid_like, "actor") + + refute LikeValidator.cast_and_validate(without_actor).valid? + + with_invalid_actor = Map.put(valid_like, "actor", "invalidactor") + + refute LikeValidator.cast_and_validate(with_invalid_actor).valid? + end + + test "it errors when the object is missing or not known", %{valid_like: valid_like} do + without_object = Map.delete(valid_like, "object") + + refute LikeValidator.cast_and_validate(without_object).valid? + + with_invalid_object = Map.put(valid_like, "object", "invalidobject") + + refute LikeValidator.cast_and_validate(with_invalid_object).valid? + end + + test "it errors when the actor has already like the object", %{ + valid_like: valid_like, + user: user, + post_activity: post_activity + } do + _like = CommonAPI.favorite(user, post_activity.id) + + refute LikeValidator.cast_and_validate(valid_like).valid? + end + + test "it works when actor or object are wrapped in maps", %{valid_like: valid_like} do + wrapped_like = + valid_like + |> Map.put("actor", %{"id" => valid_like["actor"]}) + |> Map.put("object", %{"id" => valid_like["object"]}) + + validated = LikeValidator.cast_and_validate(wrapped_like) + + assert validated.valid? + + assert {:actor, valid_like["actor"]} in validated.changes + assert {:object, valid_like["object"]} in validated.changes + end + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/reject_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/reject_validation_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.RejectValidationTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.ActivityPub.Pipeline + + import Pleroma.Factory + + setup do + follower = insert(:user) + followed = insert(:user, local: false) + + {:ok, follow_data, _} = Builder.follow(follower, followed) + {:ok, follow_activity, _} = Pipeline.common_pipeline(follow_data, local: true) + + {:ok, reject_data, _} = Builder.reject(followed, follow_activity) + + %{reject_data: reject_data, followed: followed} + end + + test "it validates a basic 'reject'", %{reject_data: reject_data} do + assert {:ok, _, _} = ObjectValidator.validate(reject_data, []) + end + + test "it fails when the actor doesn't exist", %{reject_data: reject_data} do + reject_data = + reject_data + |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") + + assert {:error, _} = ObjectValidator.validate(reject_data, []) + end + + test "it fails when the rejected activity doesn't exist", %{reject_data: reject_data} do + reject_data = + reject_data + |> Map.put("object", "https://gensokyo.2hu/users/raymoo/follows/1") + + assert {:error, _} = ObjectValidator.validate(reject_data, []) + end + + test "for an rejected follow, it only validates if the actor of the reject is the followed actor", + %{reject_data: reject_data} do + stranger = insert(:user) + + reject_data = + reject_data + |> Map.put("actor", stranger.ap_id) + + assert {:error, _} = ObjectValidator.validate(reject_data, []) + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/undo_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/undo_handling_test.exs @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "Undos" do + setup do + user = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) + {:ok, like} = CommonAPI.favorite(user, post_activity.id) + {:ok, valid_like_undo, []} = Builder.undo(user, like) + + %{user: user, like: like, valid_like_undo: valid_like_undo} + end + + test "it validates a basic like undo", %{valid_like_undo: valid_like_undo} do + assert {:ok, _, _} = ObjectValidator.validate(valid_like_undo, []) + end + + test "it does not validate if the actor of the undo is not the actor of the object", %{ + valid_like_undo: valid_like_undo + } do + other_user = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo") + + bad_actor = + valid_like_undo + |> Map.put("actor", other_user.ap_id) + + {:error, cng} = ObjectValidator.validate(bad_actor, []) + + assert {:actor, {"not the same as object actor", []}} in cng.errors + end + + test "it does not validate if the object is missing", %{valid_like_undo: valid_like_undo} do + missing_object = + valid_like_undo + |> Map.put("object", "https://gensokyo.2hu/objects/1") + + {:error, cng} = ObjectValidator.validate(missing_object, []) + + assert {:object, {"can't find object", []}} in cng.errors + assert length(cng.errors) == 1 + 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 @@ -0,0 +1,44 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + + import Pleroma.Factory + + describe "updates" do + setup do + user = insert(:user) + + object = %{ + "id" => user.ap_id, + "name" => "A new name", + "summary" => "A new bio" + } + + {:ok, valid_update, []} = Builder.update(user, object) + + %{user: user, valid_update: valid_update} + end + + test "validates a basic object", %{valid_update: valid_update} do + assert {:ok, _update, []} = ObjectValidator.validate(valid_update, []) + end + + test "returns an error if the object can't be updated by the actor", %{ + valid_update: valid_update + } do + other_user = insert(:user) + + update = + valid_update + |> Map.put("actor", other_user.ap_id) + + assert {:error, _cng} = ObjectValidator.validate(update, []) + end + end +end diff --git a/test/pleroma/web/activity_pub/pipeline_test.exs b/test/pleroma/web/activity_pub/pipeline_test.exs @@ -0,0 +1,179 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.PipelineTest do + use Pleroma.DataCase + + import Mock + import Pleroma.Factory + + describe "common_pipeline/2" do + setup do + clear_config([:instance, :federating], true) + :ok + end + + test "when given an `object_data` in meta, Federation will receive a the original activity with the `object` field set to this embedded object" do + activity = insert(:note_activity) + object = %{"id" => "1", "type" => "Love"} + meta = [local: true, object_data: object] + + activity_with_object = %{activity | data: Map.put(activity.data, "object", object)} + + with_mocks([ + {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]}, + { + Pleroma.Web.ActivityPub.MRF, + [], + [pipeline_filter: fn o, m -> {:ok, o, m} end] + }, + { + Pleroma.Web.ActivityPub.ActivityPub, + [], + [persist: fn o, m -> {:ok, o, m} end] + }, + { + Pleroma.Web.ActivityPub.SideEffects, + [], + [ + handle: fn o, m -> {:ok, o, m} end, + handle_after_transaction: fn m -> m end + ] + }, + { + Pleroma.Web.Federator, + [], + [publish: fn _o -> :ok end] + } + ]) do + assert {:ok, ^activity, ^meta} = + Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) + + assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) + refute called(Pleroma.Web.Federator.publish(activity)) + assert_called(Pleroma.Web.Federator.publish(activity_with_object)) + end + end + + test "it goes through validation, filtering, persisting, side effects and federation for local activities" do + activity = insert(:note_activity) + meta = [local: true] + + with_mocks([ + {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]}, + { + Pleroma.Web.ActivityPub.MRF, + [], + [pipeline_filter: fn o, m -> {:ok, o, m} end] + }, + { + Pleroma.Web.ActivityPub.ActivityPub, + [], + [persist: fn o, m -> {:ok, o, m} end] + }, + { + Pleroma.Web.ActivityPub.SideEffects, + [], + [ + handle: fn o, m -> {:ok, o, m} end, + handle_after_transaction: fn m -> m end + ] + }, + { + Pleroma.Web.Federator, + [], + [publish: fn _o -> :ok end] + } + ]) do + assert {:ok, ^activity, ^meta} = + Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) + + assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) + assert_called(Pleroma.Web.Federator.publish(activity)) + end + end + + test "it goes through validation, filtering, persisting, side effects without federation for remote activities" do + activity = insert(:note_activity) + meta = [local: false] + + with_mocks([ + {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]}, + { + Pleroma.Web.ActivityPub.MRF, + [], + [pipeline_filter: fn o, m -> {:ok, o, m} end] + }, + { + Pleroma.Web.ActivityPub.ActivityPub, + [], + [persist: fn o, m -> {:ok, o, m} end] + }, + { + Pleroma.Web.ActivityPub.SideEffects, + [], + [handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end] + }, + { + Pleroma.Web.Federator, + [], + [] + } + ]) do + assert {:ok, ^activity, ^meta} = + Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) + + assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) + end + end + + test "it goes through validation, filtering, persisting, side effects without federation for local activities if federation is deactivated" do + clear_config([:instance, :federating], false) + + activity = insert(:note_activity) + meta = [local: true] + + with_mocks([ + {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]}, + { + Pleroma.Web.ActivityPub.MRF, + [], + [pipeline_filter: fn o, m -> {:ok, o, m} end] + }, + { + Pleroma.Web.ActivityPub.ActivityPub, + [], + [persist: fn o, m -> {:ok, o, m} end] + }, + { + Pleroma.Web.ActivityPub.SideEffects, + [], + [handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end] + }, + { + Pleroma.Web.Federator, + [], + [] + } + ]) do + assert {:ok, ^activity, ^meta} = + Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) + + assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) + end + end + end +end diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs @@ -0,0 +1,365 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.PublisherTest do + use Pleroma.Web.ConnCase + + import ExUnit.CaptureLog + import Pleroma.Factory + import Tesla.Mock + import Mock + + alias Pleroma.Activity + alias Pleroma.Instances + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Publisher + alias Pleroma.Web.CommonAPI + + @as_public "https://www.w3.org/ns/activitystreams#Public" + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup_all do: clear_config([:instance, :federating], true) + + describe "gather_webfinger_links/1" do + test "it returns links" do + user = insert(:user) + + expected_links = [ + %{"href" => user.ap_id, "rel" => "self", "type" => "application/activity+json"}, + %{ + "href" => user.ap_id, + "rel" => "self", + "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + }, + %{ + "rel" => "http://ostatus.org/schema/1.0/subscribe", + "template" => "#{Pleroma.Web.base_url()}/ostatus_subscribe?acct={uri}" + } + ] + + assert expected_links == Publisher.gather_webfinger_links(user) + end + end + + describe "determine_inbox/2" do + test "it returns sharedInbox for messages involving as:Public in to" do + user = insert(:user, %{shared_inbox: "http://example.com/inbox"}) + + activity = %Activity{ + data: %{"to" => [@as_public], "cc" => [user.follower_address]} + } + + assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" + end + + test "it returns sharedInbox for messages involving as:Public in cc" do + user = insert(:user, %{shared_inbox: "http://example.com/inbox"}) + + activity = %Activity{ + data: %{"cc" => [@as_public], "to" => [user.follower_address]} + } + + assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" + end + + test "it returns sharedInbox for messages involving multiple recipients in to" do + user = insert(:user, %{shared_inbox: "http://example.com/inbox"}) + user_two = insert(:user) + user_three = insert(:user) + + activity = %Activity{ + data: %{"cc" => [], "to" => [user.ap_id, user_two.ap_id, user_three.ap_id]} + } + + assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" + end + + test "it returns sharedInbox for messages involving multiple recipients in cc" do + user = insert(:user, %{shared_inbox: "http://example.com/inbox"}) + user_two = insert(:user) + user_three = insert(:user) + + activity = %Activity{ + data: %{"to" => [], "cc" => [user.ap_id, user_two.ap_id, user_three.ap_id]} + } + + assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" + end + + test "it returns sharedInbox for messages involving multiple recipients in total" do + user = + insert(:user, %{ + shared_inbox: "http://example.com/inbox", + inbox: "http://example.com/personal-inbox" + }) + + user_two = insert(:user) + + activity = %Activity{ + data: %{"to" => [user_two.ap_id], "cc" => [user.ap_id]} + } + + assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" + end + + test "it returns inbox for messages involving single recipients in total" do + user = + insert(:user, %{ + shared_inbox: "http://example.com/inbox", + inbox: "http://example.com/personal-inbox" + }) + + activity = %Activity{ + data: %{"to" => [user.ap_id], "cc" => []} + } + + assert Publisher.determine_inbox(activity, user) == "http://example.com/personal-inbox" + end + end + + describe "publish_one/1" do + test "publish to url with with different ports" do + inbox80 = "http://42.site/users/nick1/inbox" + inbox42 = "http://42.site:42/users/nick1/inbox" + + mock(fn + %{method: :post, url: "http://42.site:42/users/nick1/inbox"} -> + {:ok, %Tesla.Env{status: 200, body: "port 42"}} + + %{method: :post, url: "http://42.site/users/nick1/inbox"} -> + {:ok, %Tesla.Env{status: 200, body: "port 80"}} + end) + + actor = insert(:user) + + assert {:ok, %{body: "port 42"}} = + Publisher.publish_one(%{ + inbox: inbox42, + json: "{}", + actor: actor, + id: 1, + unreachable_since: true + }) + + assert {:ok, %{body: "port 80"}} = + Publisher.publish_one(%{ + inbox: inbox80, + json: "{}", + actor: actor, + id: 1, + unreachable_since: true + }) + end + + test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is not specified", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://200.site/users/nick1/inbox" + + assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + assert called(Instances.set_reachable(inbox)) + end + + test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is set", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://200.site/users/nick1/inbox" + + assert {:ok, _} = + Publisher.publish_one(%{ + inbox: inbox, + json: "{}", + actor: actor, + id: 1, + unreachable_since: NaiveDateTime.utc_now() + }) + + assert called(Instances.set_reachable(inbox)) + end + + test_with_mock "does NOT call `Instances.set_reachable` on successful federation if `unreachable_since` is nil", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://200.site/users/nick1/inbox" + + assert {:ok, _} = + Publisher.publish_one(%{ + inbox: inbox, + json: "{}", + actor: actor, + id: 1, + unreachable_since: nil + }) + + refute called(Instances.set_reachable(inbox)) + end + + test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://404.site/users/nick1/inbox" + + assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + + assert called(Instances.set_unreachable(inbox)) + end + + test_with_mock "it calls `Instances.set_unreachable` on target inbox on request error of any kind", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://connrefused.site/users/nick1/inbox" + + assert capture_log(fn -> + assert {:error, _} = + Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + end) =~ "connrefused" + + assert called(Instances.set_unreachable(inbox)) + end + + test_with_mock "does NOT call `Instances.set_unreachable` if target is reachable", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://200.site/users/nick1/inbox" + + assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + + refute called(Instances.set_unreachable(inbox)) + end + + test_with_mock "does NOT call `Instances.set_unreachable` if target instance has non-nil `unreachable_since`", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://connrefused.site/users/nick1/inbox" + + assert capture_log(fn -> + assert {:error, _} = + Publisher.publish_one(%{ + inbox: inbox, + json: "{}", + actor: actor, + id: 1, + unreachable_since: NaiveDateTime.utc_now() + }) + end) =~ "connrefused" + + refute called(Instances.set_unreachable(inbox)) + end + end + + describe "publish/2" do + test_with_mock "publishes an activity with BCC to all relevant peers.", + Pleroma.Web.Federator.Publisher, + [:passthrough], + [] do + follower = + insert(:user, %{ + local: false, + inbox: "https://domain.com/users/nick1/inbox", + ap_enabled: true + }) + + actor = insert(:user, follower_address: follower.ap_id) + user = insert(:user) + + {:ok, _follower_one} = Pleroma.User.follow(follower, actor) + actor = refresh_record(actor) + + note_activity = + insert(:note_activity, + recipients: [follower.ap_id], + data_attrs: %{"bcc" => [user.ap_id]} + ) + + res = Publisher.publish(actor, note_activity) + 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"] + }) + ) + end + + test_with_mock "publishes a delete activity to peers who signed fetch requests to the create acitvity/object.", + Pleroma.Web.Federator.Publisher, + [:passthrough], + [] do + fetcher = + insert(:user, + local: false, + inbox: "https://domain.com/users/nick1/inbox", + ap_enabled: true + ) + + another_fetcher = + insert(:user, + local: false, + inbox: "https://domain2.com/users/nick1/inbox", + ap_enabled: true + ) + + actor = insert(:user) + + note_activity = insert(:note_activity, user: actor) + object = Object.normalize(note_activity) + + activity_path = String.trim_leading(note_activity.data["id"], Pleroma.Web.Endpoint.url()) + object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url()) + + build_conn() + |> put_req_header("accept", "application/activity+json") + |> assign(:user, fetcher) + |> get(object_path) + |> json_response(200) + + build_conn() + |> put_req_header("accept", "application/activity+json") + |> assign(:user, another_fetcher) + |> get(activity_path) + |> json_response(200) + + {:ok, delete} = CommonAPI.delete(note_activity.id, actor) + + res = Publisher.publish(actor, delete) + 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"] + }) + ) + + assert called( + Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{ + inbox: "https://domain2.com/users/nick1/inbox", + actor_id: actor.id, + id: delete.data["id"] + }) + ) + end + end +end diff --git a/test/pleroma/web/activity_pub/relay_test.exs b/test/pleroma/web/activity_pub/relay_test.exs @@ -0,0 +1,168 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.RelayTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Relay + alias Pleroma.Web.CommonAPI + + import ExUnit.CaptureLog + import Pleroma.Factory + import Mock + + test "gets an actor for the relay" do + user = Relay.get_actor() + assert user.ap_id == "#{Pleroma.Web.Endpoint.url()}/relay" + end + + test "relay actor is invisible" do + user = Relay.get_actor() + assert User.invisible?(user) + end + + describe "follow/1" do + test "returns errors when user not found" do + assert capture_log(fn -> + {:error, _} = Relay.follow("test-ap-id") + end) =~ "Could not decode user at fetch" + end + + test "returns activity" do + user = insert(:user) + service_actor = Relay.get_actor() + assert {:ok, %Activity{} = activity} = Relay.follow(user.ap_id) + assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" + assert user.ap_id in activity.recipients + assert activity.data["type"] == "Follow" + assert activity.data["actor"] == service_actor.ap_id + assert activity.data["object"] == user.ap_id + end + end + + describe "unfollow/1" do + test "returns errors when user not found" do + assert capture_log(fn -> + {:error, _} = Relay.unfollow("test-ap-id") + end) =~ "Could not decode user at fetch" + end + + test "returns activity" do + user = insert(:user) + service_actor = Relay.get_actor() + CommonAPI.follow(service_actor, user) + assert "#{user.ap_id}/followers" in User.following(service_actor) + assert {:ok, %Activity{} = activity} = Relay.unfollow(user.ap_id) + assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" + assert user.ap_id in activity.recipients + assert activity.data["type"] == "Undo" + assert activity.data["actor"] == service_actor.ap_id + assert activity.data["to"] == [user.ap_id] + refute "#{user.ap_id}/followers" in User.following(service_actor) + end + + test "force unfollow when target service is dead" do + user = insert(:user) + user_ap_id = user.ap_id + user_id = user.id + + Tesla.Mock.mock(fn %{method: :get, url: ^user_ap_id} -> + %Tesla.Env{status: 404} + end) + + service_actor = Relay.get_actor() + CommonAPI.follow(service_actor, user) + assert "#{user.ap_id}/followers" in User.following(service_actor) + + assert Pleroma.Repo.get_by( + Pleroma.FollowingRelationship, + follower_id: service_actor.id, + following_id: user_id + ) + + Pleroma.Repo.delete(user) + Cachex.clear(:user_cache) + + assert {:ok, %Activity{} = activity} = Relay.unfollow(user_ap_id, %{force: true}) + + assert refresh_record(service_actor).following_count == 0 + + refute Pleroma.Repo.get_by( + Pleroma.FollowingRelationship, + follower_id: service_actor.id, + following_id: user_id + ) + + assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" + assert user.ap_id in activity.recipients + assert activity.data["type"] == "Undo" + assert activity.data["actor"] == service_actor.ap_id + assert activity.data["to"] == [user_ap_id] + refute "#{user.ap_id}/followers" in User.following(service_actor) + end + end + + describe "publish/1" do + setup do: clear_config([:instance, :federating]) + + test "returns error when activity not `Create` type" do + activity = insert(:like_activity) + assert Relay.publish(activity) == {:error, "Not implemented"} + end + + @tag capture_log: true + test "returns error when activity not public" do + activity = insert(:direct_note_activity) + assert Relay.publish(activity) == {:error, false} + end + + test "returns error when object is unknown" do + activity = + insert(:note_activity, + data: %{ + "type" => "Create", + "object" => "http://mastodon.example.org/eee/99541947525187367" + } + ) + + Tesla.Mock.mock(fn + %{method: :get, url: "http://mastodon.example.org/eee/99541947525187367"} -> + %Tesla.Env{status: 500, body: ""} + end) + + assert capture_log(fn -> + assert Relay.publish(activity) == {:error, false} + end) =~ "[error] error: false" + end + + test_with_mock "returns announce activity and publish to federate", + Pleroma.Web.Federator, + [:passthrough], + [] do + clear_config([:instance, :federating], true) + service_actor = Relay.get_actor() + note = insert(:note_activity) + assert {:ok, %Activity{} = activity} = Relay.publish(note) + assert activity.data["type"] == "Announce" + assert activity.data["actor"] == service_actor.ap_id + assert activity.data["to"] == [service_actor.follower_address] + assert called(Pleroma.Web.Federator.publish(activity)) + end + + test_with_mock "returns announce activity and not publish to federate", + Pleroma.Web.Federator, + [:passthrough], + [] do + clear_config([:instance, :federating], false) + service_actor = Relay.get_actor() + note = insert(:note_activity) + assert {:ok, %Activity{} = activity} = Relay.publish(note) + assert activity.data["type"] == "Announce" + assert activity.data["actor"] == service_actor.ap_id + refute called(Pleroma.Web.Federator.publish(activity)) + end + end +end diff --git a/test/pleroma/web/activity_pub/side_effects_test.exs b/test/pleroma/web/activity_pub/side_effects_test.exs @@ -0,0 +1,639 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.SideEffectsTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Notification + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.SideEffects + alias Pleroma.Web.CommonAPI + + import ExUnit.CaptureLog + import Mock + import Pleroma.Factory + + describe "handle_after_transaction" do + test "it streams out notifications and streams" do + author = insert(:user, local: true) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + assert [notification] = meta[:notifications] + + with_mocks([ + { + Pleroma.Web.Streamer, + [], + [ + stream: fn _, _ -> nil end + ] + }, + { + Pleroma.Web.Push, + [], + [ + send: fn _ -> nil end + ] + } + ]) do + SideEffects.handle_after_transaction(meta) + + assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) + assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) + assert called(Pleroma.Web.Push.send(notification)) + end + end + end + + describe "blocking users" do + setup do + user = insert(:user) + blocked = insert(:user) + User.follow(blocked, user) + User.follow(user, blocked) + + {:ok, block_data, []} = Builder.block(user, blocked) + {:ok, block, _meta} = ActivityPub.persist(block_data, local: true) + + %{user: user, blocked: blocked, block: block} + end + + test "it unfollows and blocks", %{user: user, blocked: blocked, block: block} do + assert User.following?(user, blocked) + assert User.following?(blocked, user) + + {:ok, _, _} = SideEffects.handle(block) + + refute User.following?(user, blocked) + refute User.following?(blocked, user) + assert User.blocks?(user, blocked) + end + + test "it blocks but does not unfollow if the relevant setting is set", %{ + user: user, + blocked: blocked, + block: block + } do + clear_config([:activitypub, :unfollow_blocked], false) + assert User.following?(user, blocked) + assert User.following?(blocked, user) + + {:ok, _, _} = SideEffects.handle(block) + + refute User.following?(user, blocked) + assert User.following?(blocked, user) + assert User.blocks?(user, blocked) + end + end + + describe "update users" do + setup do + user = insert(:user) + {:ok, update_data, []} = Builder.update(user, %{"id" => user.ap_id, "name" => "new name!"}) + {:ok, update, _meta} = ActivityPub.persist(update_data, local: true) + + %{user: user, update_data: update_data, update: update} + end + + test "it updates the user", %{user: user, update: update} do + {:ok, _, _} = SideEffects.handle(update) + user = User.get_by_id(user.id) + assert user.name == "new name!" + end + + test "it uses a given changeset to update", %{user: user, update: update} do + changeset = Ecto.Changeset.change(user, %{default_scope: "direct"}) + + assert user.default_scope == "public" + {:ok, _, _} = SideEffects.handle(update, user_update_changeset: changeset) + user = User.get_by_id(user.id) + assert user.default_scope == "direct" + end + end + + describe "delete objects" do + setup do + user = insert(:user) + other_user = insert(:user) + + {:ok, op} = CommonAPI.post(other_user, %{status: "big oof"}) + {:ok, post} = CommonAPI.post(user, %{status: "hey", in_reply_to_id: op}) + {:ok, favorite} = CommonAPI.favorite(user, post.id) + object = Object.normalize(post) + {:ok, delete_data, _meta} = Builder.delete(user, object.data["id"]) + {:ok, delete_user_data, _meta} = Builder.delete(user, user.ap_id) + {:ok, delete, _meta} = ActivityPub.persist(delete_data, local: true) + {:ok, delete_user, _meta} = ActivityPub.persist(delete_user_data, local: true) + + %{ + user: user, + delete: delete, + post: post, + object: object, + delete_user: delete_user, + op: op, + favorite: favorite + } + end + + test "it handles object deletions", %{ + delete: delete, + post: post, + object: object, + user: user, + op: op, + favorite: favorite + } do + with_mock Pleroma.Web.ActivityPub.ActivityPub, [:passthrough], + stream_out: fn _ -> nil end, + stream_out_participations: fn _, _ -> nil end do + {:ok, delete, _} = SideEffects.handle(delete) + user = User.get_cached_by_ap_id(object.data["actor"]) + + assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out(delete)) + assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out_participations(object, user)) + end + + object = Object.get_by_id(object.id) + assert object.data["type"] == "Tombstone" + refute Activity.get_by_id(post.id) + refute Activity.get_by_id(favorite.id) + + user = User.get_by_id(user.id) + assert user.note_count == 0 + + object = Object.normalize(op.data["object"], false) + + assert object.data["repliesCount"] == 0 + end + + test "it handles object deletions when the object itself has been pruned", %{ + delete: delete, + post: post, + object: object, + user: user, + op: op + } do + with_mock Pleroma.Web.ActivityPub.ActivityPub, [:passthrough], + stream_out: fn _ -> nil end, + stream_out_participations: fn _, _ -> nil end do + {:ok, delete, _} = SideEffects.handle(delete) + user = User.get_cached_by_ap_id(object.data["actor"]) + + assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out(delete)) + assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out_participations(object, user)) + end + + object = Object.get_by_id(object.id) + assert object.data["type"] == "Tombstone" + refute Activity.get_by_id(post.id) + + user = User.get_by_id(user.id) + assert user.note_count == 0 + + object = Object.normalize(op.data["object"], false) + + assert object.data["repliesCount"] == 0 + end + + test "it handles user deletions", %{delete_user: delete, user: user} do + {:ok, _delete, _} = SideEffects.handle(delete) + ObanHelpers.perform_all() + + assert User.get_cached_by_ap_id(user.ap_id).deactivated + end + + test "it logs issues with objects deletion", %{ + delete: delete, + object: object + } do + {:ok, object} = + object + |> Object.change(%{data: Map.delete(object.data, "actor")}) + |> Repo.update() + + Object.invalid_object_cache(object) + + assert capture_log(fn -> + {:error, :no_object_actor} = SideEffects.handle(delete) + end) =~ "object doesn't have an actor" + end + end + + describe "EmojiReact objects" do + setup do + poster = insert(:user) + user = insert(:user) + + {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) + + {:ok, emoji_react_data, []} = Builder.emoji_react(user, post.object, "👌") + {:ok, emoji_react, _meta} = ActivityPub.persist(emoji_react_data, local: true) + + %{emoji_react: emoji_react, user: user, poster: poster} + end + + test "adds the reaction to the object", %{emoji_react: emoji_react, user: user} do + {:ok, emoji_react, _} = SideEffects.handle(emoji_react) + object = Object.get_by_ap_id(emoji_react.data["object"]) + + assert object.data["reaction_count"] == 1 + assert ["👌", [user.ap_id]] in object.data["reactions"] + end + + test "creates a notification", %{emoji_react: emoji_react, poster: poster} do + {:ok, emoji_react, _} = SideEffects.handle(emoji_react) + assert Repo.get_by(Notification, user_id: poster.id, activity_id: emoji_react.id) + end + end + + describe "delete users with confirmation pending" do + setup do + user = insert(:user, confirmation_pending: true) + {:ok, delete_user_data, _meta} = Builder.delete(user, user.ap_id) + {:ok, delete_user, _meta} = ActivityPub.persist(delete_user_data, local: true) + {:ok, delete: delete_user, user: user} + end + + test "when activation is not required", %{delete: delete, user: user} do + clear_config([:instance, :account_activation_required], false) + {:ok, _, _} = SideEffects.handle(delete) + ObanHelpers.perform_all() + + assert User.get_cached_by_id(user.id).deactivated + end + + test "when activation is required", %{delete: delete, user: user} do + clear_config([:instance, :account_activation_required], true) + {:ok, _, _} = SideEffects.handle(delete) + ObanHelpers.perform_all() + + refute User.get_cached_by_id(user.id) + end + end + + describe "Undo objects" do + setup do + poster = insert(:user) + user = insert(:user) + {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) + {:ok, like} = CommonAPI.favorite(user, post.id) + {:ok, reaction} = CommonAPI.react_with_emoji(post.id, user, "👍") + {:ok, announce} = CommonAPI.repeat(post.id, user) + {:ok, block} = CommonAPI.block(user, poster) + + {:ok, undo_data, _meta} = Builder.undo(user, like) + {:ok, like_undo, _meta} = ActivityPub.persist(undo_data, local: true) + + {:ok, undo_data, _meta} = Builder.undo(user, reaction) + {:ok, reaction_undo, _meta} = ActivityPub.persist(undo_data, local: true) + + {:ok, undo_data, _meta} = Builder.undo(user, announce) + {:ok, announce_undo, _meta} = ActivityPub.persist(undo_data, local: true) + + {:ok, undo_data, _meta} = Builder.undo(user, block) + {:ok, block_undo, _meta} = ActivityPub.persist(undo_data, local: true) + + %{ + like_undo: like_undo, + post: post, + like: like, + reaction_undo: reaction_undo, + reaction: reaction, + announce_undo: announce_undo, + announce: announce, + block_undo: block_undo, + block: block, + poster: poster, + user: user + } + end + + test "deletes the original block", %{ + block_undo: block_undo, + block: block + } do + {:ok, _block_undo, _meta} = SideEffects.handle(block_undo) + + refute Activity.get_by_id(block.id) + end + + test "unblocks the blocked user", %{block_undo: block_undo, block: block} do + blocker = User.get_by_ap_id(block.data["actor"]) + blocked = User.get_by_ap_id(block.data["object"]) + + {:ok, _block_undo, _} = SideEffects.handle(block_undo) + refute User.blocks?(blocker, blocked) + end + + test "an announce undo removes the announce from the object", %{ + announce_undo: announce_undo, + post: post + } do + {:ok, _announce_undo, _} = SideEffects.handle(announce_undo) + + object = Object.get_by_ap_id(post.data["object"]) + + assert object.data["announcement_count"] == 0 + assert object.data["announcements"] == [] + end + + test "deletes the original announce", %{announce_undo: announce_undo, announce: announce} do + {:ok, _announce_undo, _} = SideEffects.handle(announce_undo) + refute Activity.get_by_id(announce.id) + end + + test "a reaction undo removes the reaction from the object", %{ + reaction_undo: reaction_undo, + post: post + } do + {:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo) + + object = Object.get_by_ap_id(post.data["object"]) + + assert object.data["reaction_count"] == 0 + assert object.data["reactions"] == [] + end + + test "deletes the original reaction", %{reaction_undo: reaction_undo, reaction: reaction} do + {:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo) + refute Activity.get_by_id(reaction.id) + end + + test "a like undo removes the like from the object", %{like_undo: like_undo, post: post} do + {:ok, _like_undo, _} = SideEffects.handle(like_undo) + + object = Object.get_by_ap_id(post.data["object"]) + + assert object.data["like_count"] == 0 + assert object.data["likes"] == [] + end + + test "deletes the original like", %{like_undo: like_undo, like: like} do + {:ok, _like_undo, _} = SideEffects.handle(like_undo) + refute Activity.get_by_id(like.id) + end + end + + describe "like objects" do + setup do + poster = insert(:user) + user = insert(:user) + {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) + + {:ok, like_data, _meta} = Builder.like(user, post.object) + {:ok, like, _meta} = ActivityPub.persist(like_data, local: true) + + %{like: like, user: user, poster: poster} + end + + test "add the like to the original object", %{like: like, user: user} do + {:ok, like, _} = SideEffects.handle(like) + object = Object.get_by_ap_id(like.data["object"]) + assert object.data["like_count"] == 1 + assert user.ap_id in object.data["likes"] + end + + test "creates a notification", %{like: like, poster: poster} do + {:ok, like, _} = SideEffects.handle(like) + assert Repo.get_by(Notification, user_id: poster.id, activity_id: like.id) + end + end + + describe "creation of ChatMessages" do + test "notifies the recipient" do + author = insert(:user, local: false) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + assert Repo.get_by(Notification, user_id: recipient.id, activity_id: create_activity.id) + end + + test "it streams the created ChatMessage" do + author = insert(:user, local: true) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + assert [_, _] = meta[:streamables] + end + + test "it creates a Chat and MessageReferences for the local users and bumps the unread count, except for the author" do + author = insert(:user, local: true) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + with_mocks([ + { + Pleroma.Web.Streamer, + [], + [ + stream: fn _, _ -> nil end + ] + }, + { + Pleroma.Web.Push, + [], + [ + send: fn _ -> nil end + ] + } + ]) do + {:ok, _create_activity, meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + # The notification gets created + assert [notification] = meta[:notifications] + assert notification.activity_id == create_activity.id + + # But it is not sent out + refute called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) + refute called(Pleroma.Web.Push.send(notification)) + + # Same for the user chat stream + assert [{topics, _}, _] = meta[:streamables] + assert topics == ["user", "user:pleroma_chat"] + refute called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) + + chat = Chat.get(author.id, recipient.ap_id) + + [cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all() + + assert cm_ref.object.data["content"] == "hey" + assert cm_ref.unread == false + + chat = Chat.get(recipient.id, author.ap_id) + + [cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all() + + assert cm_ref.object.data["content"] == "hey" + assert cm_ref.unread == true + end + end + + test "it creates a Chat for the local users and bumps the unread count" do + author = insert(:user, local: false) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + # An object is created + assert Object.get_by_ap_id(chat_message_data["id"]) + + # The remote user won't get a chat + chat = Chat.get(author.id, recipient.ap_id) + refute chat + + # The local user will get a chat + chat = Chat.get(recipient.id, author.ap_id) + assert chat + + author = insert(:user, local: true) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + # Both users are local and get the chat + chat = Chat.get(author.id, recipient.ap_id) + assert chat + + chat = Chat.get(recipient.id, author.ap_id) + assert chat + end + end + + describe "announce objects" do + setup do + poster = insert(:user) + user = insert(:user) + {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) + {:ok, private_post} = CommonAPI.post(poster, %{status: "hey", visibility: "private"}) + + {:ok, announce_data, _meta} = Builder.announce(user, post.object, public: true) + + {:ok, private_announce_data, _meta} = + Builder.announce(user, private_post.object, public: false) + + {:ok, relay_announce_data, _meta} = + Builder.announce(Pleroma.Web.ActivityPub.Relay.get_actor(), post.object, public: true) + + {:ok, announce, _meta} = ActivityPub.persist(announce_data, local: true) + {:ok, private_announce, _meta} = ActivityPub.persist(private_announce_data, local: true) + {:ok, relay_announce, _meta} = ActivityPub.persist(relay_announce_data, local: true) + + %{ + announce: announce, + user: user, + poster: poster, + private_announce: private_announce, + relay_announce: relay_announce + } + end + + test "adds the announce to the original object", %{announce: announce, user: user} do + {:ok, announce, _} = SideEffects.handle(announce) + object = Object.get_by_ap_id(announce.data["object"]) + assert object.data["announcement_count"] == 1 + assert user.ap_id in object.data["announcements"] + end + + test "does not add the announce to the original object if the actor is a service actor", %{ + relay_announce: announce + } do + {:ok, announce, _} = SideEffects.handle(announce) + object = Object.get_by_ap_id(announce.data["object"]) + assert object.data["announcement_count"] == nil + end + + test "creates a notification", %{announce: announce, poster: poster} 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 +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/accept_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/accept_handling_test.exs @@ -0,0 +1,91 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.AcceptHandlingTest do + use Pleroma.DataCase + + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it works for incoming accepts which were pre-accepted" do + follower = insert(:user) + followed = insert(:user) + + {:ok, follower} = User.follow(follower, followed) + assert User.following?(follower, followed) == true + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) + + accept_data = + File.read!("test/fixtures/mastodon-accept-activity.json") + |> Poison.decode!() + |> Map.put("actor", followed.ap_id) + + object = + accept_data["object"] + |> Map.put("actor", follower.ap_id) + |> Map.put("id", follow_activity.data["id"]) + + accept_data = Map.put(accept_data, "object", object) + + {:ok, activity} = Transmogrifier.handle_incoming(accept_data) + refute activity.local + + assert activity.data["object"] == follow_activity.data["id"] + + assert activity.data["id"] == accept_data["id"] + + follower = User.get_cached_by_id(follower.id) + + assert User.following?(follower, followed) == true + end + + test "it works for incoming accepts which are referenced by IRI only" do + follower = insert(:user) + followed = insert(:user, locked: true) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) + + accept_data = + File.read!("test/fixtures/mastodon-accept-activity.json") + |> Poison.decode!() + |> Map.put("actor", followed.ap_id) + |> Map.put("object", follow_activity.data["id"]) + + {:ok, activity} = Transmogrifier.handle_incoming(accept_data) + assert activity.data["object"] == follow_activity.data["id"] + + follower = User.get_cached_by_id(follower.id) + + assert User.following?(follower, followed) == true + + follower = User.get_by_id(follower.id) + assert follower.following_count == 1 + + followed = User.get_by_id(followed.id) + assert followed.follower_count == 1 + end + + test "it fails for incoming accepts which cannot be correlated" do + follower = insert(:user) + followed = insert(:user, locked: true) + + accept_data = + File.read!("test/fixtures/mastodon-accept-activity.json") + |> Poison.decode!() + |> Map.put("actor", followed.ap_id) + + accept_data = + Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) + + {:error, _} = Transmogrifier.handle_incoming(accept_data) + + follower = User.get_cached_by_id(follower.id) + + refute User.following?(follower, followed) == true + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs @@ -0,0 +1,172 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnnounceHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it works for incoming honk announces" do + user = insert(:user, ap_id: "https://honktest/u/test", local: false) + other_user = insert(:user) + {:ok, post} = CommonAPI.post(other_user, %{status: "bonkeronk"}) + + announce = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => "https://honktest/u/test", + "id" => "https://honktest/u/test/bonk/1793M7B9MQ48847vdx", + "object" => post.data["object"], + "published" => "2019-06-25T19:33:58Z", + "to" => "https://www.w3.org/ns/activitystreams#Public", + "type" => "Announce" + } + + {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(announce) + + object = Object.get_by_ap_id(post.data["object"]) + + assert length(object.data["announcements"]) == 1 + assert user.ap_id in object.data["announcements"] + end + + test "it works for incoming announces with actor being inlined (kroeg)" do + data = File.read!("test/fixtures/kroeg-announce-with-inline-actor.json") |> Poison.decode!() + + _user = insert(:user, local: false, ap_id: data["actor"]["id"]) + other_user = insert(:user) + + {:ok, post} = CommonAPI.post(other_user, %{status: "kroegeroeg"}) + + data = + data + |> put_in(["object", "id"], post.data["object"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "https://puckipedia.com/" + end + + test "it works for incoming announces, fetching the announced object" do + data = + File.read!("test/fixtures/mastodon-announce.json") + |> Poison.decode!() + |> Map.put("object", "http://mastodon.example.org/users/admin/statuses/99541947525187367") + + Tesla.Mock.mock(fn + %{method: :get} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/mastodon-note-object.json")} + end) + + _user = insert(:user, local: false, ap_id: data["actor"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "http://mastodon.example.org/users/admin" + assert data["type"] == "Announce" + + assert data["id"] == + "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" + + assert data["object"] == + "http://mastodon.example.org/users/admin/statuses/99541947525187367" + + assert(Activity.get_create_by_object_ap_id(data["object"])) + end + + @tag capture_log: true + test "it works for incoming announces with an existing activity" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + + data = + File.read!("test/fixtures/mastodon-announce.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + _user = insert(:user, local: false, ap_id: data["actor"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "http://mastodon.example.org/users/admin" + assert data["type"] == "Announce" + + assert data["id"] == + "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" + + assert data["object"] == activity.data["object"] + + assert Activity.get_create_by_object_ap_id(data["object"]).id == activity.id + end + + # Ignore inlined activities for now + @tag skip: true + test "it works for incoming announces with an inlined activity" do + data = + File.read!("test/fixtures/mastodon-announce-private.json") + |> Poison.decode!() + + _user = + insert(:user, + local: false, + ap_id: data["actor"], + follower_address: data["actor"] <> "/followers" + ) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "http://mastodon.example.org/users/admin" + assert data["type"] == "Announce" + + assert data["id"] == + "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" + + object = Object.normalize(data["object"]) + + assert object.data["id"] == "http://mastodon.example.org/@admin/99541947525187368" + assert object.data["content"] == "this is a private toot" + end + + @tag capture_log: true + test "it rejects incoming announces with an inlined activity from another origin" do + Tesla.Mock.mock(fn + %{method: :get} -> %Tesla.Env{status: 404, body: ""} + end) + + data = + File.read!("test/fixtures/bogus-mastodon-announce.json") + |> Poison.decode!() + + _user = insert(:user, local: false, ap_id: data["actor"]) + + assert {:error, e} = Transmogrifier.handle_incoming(data) + end + + test "it does not clobber the addressing on announce activities" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + + data = + File.read!("test/fixtures/mastodon-announce.json") + |> Poison.decode!() + |> Map.put("object", Object.normalize(activity).data["id"]) + |> Map.put("to", ["http://mastodon.example.org/users/admin/followers"]) + |> Map.put("cc", []) + + _user = + insert(:user, + local: false, + ap_id: data["actor"], + follower_address: "http://mastodon.example.org/users/admin/followers" + ) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["to"] == ["http://mastodon.example.org/users/admin/followers"] + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs @@ -0,0 +1,78 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnswerHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "incoming, rewrites Note to Answer and increments vote counters" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "suya...", + poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10} + }) + + object = Object.normalize(activity) + + data = + File.read!("test/fixtures/mastodon-vote.json") + |> Poison.decode!() + |> Kernel.put_in(["to"], user.ap_id) + |> Kernel.put_in(["object", "inReplyTo"], object.data["id"]) + |> Kernel.put_in(["object", "to"], user.ap_id) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + answer_object = Object.normalize(activity) + assert answer_object.data["type"] == "Answer" + assert answer_object.data["inReplyTo"] == object.data["id"] + + new_object = Object.get_by_ap_id(object.data["id"]) + assert new_object.data["replies_count"] == object.data["replies_count"] + + assert Enum.any?( + new_object.data["oneOf"], + fn + %{"name" => "suya..", "replies" => %{"totalItems" => 1}} -> true + _ -> false + end + ) + end + + test "outgoing, rewrites Answer to Note" do + user = insert(:user) + + {:ok, poll_activity} = + CommonAPI.post(user, %{ + status: "suya...", + poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10} + }) + + poll_object = Object.normalize(poll_activity) + # TODO: Replace with CommonAPI vote creation when implemented + data = + File.read!("test/fixtures/mastodon-vote.json") + |> Poison.decode!() + |> Kernel.put_in(["to"], user.ap_id) + |> Kernel.put_in(["object", "inReplyTo"], poll_object.data["id"]) + |> Kernel.put_in(["object", "to"], user.ap_id) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + + assert data["object"]["type"] == "Note" + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/article_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/article_handling_test.exs @@ -0,0 +1,75 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.ArticleHandlingTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Object.Fetcher + alias Pleroma.Web.ActivityPub.Transmogrifier + + test "Pterotype (Wordpress Plugin) Article" do + Tesla.Mock.mock(fn %{url: "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/wedistribute-user.json")} + end) + + data = + File.read!("test/fixtures/tesla_mock/wedistribute-create-article.json") |> Jason.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + object = Object.normalize(data["object"]) + + assert object.data["name"] == "The end is near: Mastodon plans to drop OStatus support" + + assert object.data["summary"] == + "One of the largest platforms in the federated social web is dropping the protocol that it started with." + + assert object.data["url"] == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/" + end + + test "Plume Article" do + Tesla.Mock.mock(fn + %{url: "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json") + } + + %{url: "https://baptiste.gelez.xyz/@/BaptisteGelez"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-user.json") + } + end) + + {:ok, object} = + Fetcher.fetch_object_from_id( + "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/" + ) + + assert object.data["name"] == "This Month in Plume: June 2018" + + assert object.data["url"] == + "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/" + end + + test "Prismo Article" do + Tesla.Mock.mock(fn %{url: "https://prismo.news/@mxb"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/https___prismo.news__mxb.json") + } + end) + + data = File.read!("test/fixtures/prismo-url-map.json") |> Jason.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + object = Object.normalize(data["object"]) + + assert object.data["url"] == "https://prismo.news/posts/83" + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/audio_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/audio_handling_test.exs @@ -0,0 +1,83 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.AudioHandlingTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + test "it works for incoming listens" do + _user = insert(:user, ap_id: "http://mastodon.example.org/users/admin") + + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "type" => "Listen", + "id" => "http://mastodon.example.org/users/admin/listens/1234/activity", + "actor" => "http://mastodon.example.org/users/admin", + "object" => %{ + "type" => "Audio", + "id" => "http://mastodon.example.org/users/admin/listens/1234", + "attributedTo" => "http://mastodon.example.org/users/admin", + "title" => "lain radio episode 1", + "artist" => "lain", + "album" => "lain radio", + "length" => 180_000 + } + } + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + object = Object.normalize(activity) + + assert object.data["title"] == "lain radio episode 1" + assert object.data["artist"] == "lain" + assert object.data["album"] == "lain radio" + assert object.data["length"] == 180_000 + end + + test "Funkwhale Audio object" do + Tesla.Mock.mock(fn + %{url: "https://channels.tests.funkwhale.audio/federation/actors/compositions"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/funkwhale_channel.json") + } + end) + + data = File.read!("test/fixtures/tesla_mock/funkwhale_create_audio.json") |> Poison.decode!() + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert object = Object.normalize(activity, false) + + assert object.data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] + + assert object.data["cc"] == [] + + assert object.data["url"] == "https://channels.tests.funkwhale.audio/library/tracks/74" + + assert object.data["attachment"] == [ + %{ + "mediaType" => "audio/ogg", + "type" => "Link", + "name" => nil, + "url" => [ + %{ + "href" => + "https://channels.tests.funkwhale.audio/api/v1/listen/3901e5d8-0445-49d5-9711-e096cf32e515/?upload=42342395-0208-4fee-a38d-259a6dae0871&download=false", + "mediaType" => "audio/ogg", + "type" => "Link" + } + ] + } + ] + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/block_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/block_handling_test.exs @@ -0,0 +1,63 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.BlockHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + test "it works for incoming blocks" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-block-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + blocker = insert(:user, ap_id: data["actor"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Block" + assert data["object"] == user.ap_id + assert data["actor"] == "http://mastodon.example.org/users/admin" + + assert User.blocks?(blocker, user) + end + + test "incoming blocks successfully tear down any follow relationship" do + blocker = insert(:user) + blocked = insert(:user) + + data = + File.read!("test/fixtures/mastodon-block-activity.json") + |> Poison.decode!() + |> Map.put("object", blocked.ap_id) + |> Map.put("actor", blocker.ap_id) + + {:ok, blocker} = User.follow(blocker, blocked) + {:ok, blocked} = User.follow(blocked, blocker) + + assert User.following?(blocker, blocked) + assert User.following?(blocked, blocker) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Block" + assert data["object"] == blocked.ap_id + assert data["actor"] == blocker.ap_id + + blocker = User.get_cached_by_ap_id(data["actor"]) + blocked = User.get_cached_by_ap_id(data["object"]) + + assert User.blocks?(blocker, blocked) + + refute User.following?(blocker, blocked) + refute User.following?(blocked, blocker) + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/chat_message_test.exs b/test/pleroma/web/activity_pub/transmogrifier/chat_message_test.exs @@ -0,0 +1,171 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Activity + alias Pleroma.Chat + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + + describe "handle_incoming" do + test "handles chonks with attachment" do + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => "https://honk.tedunangst.com/u/tedu", + "id" => "https://honk.tedunangst.com/u/tedu/honk/x6gt8X8PcyGkQcXxzg1T", + "object" => %{ + "attachment" => [ + %{ + "mediaType" => "image/jpeg", + "name" => "298p3RG7j27tfsZ9RQ.jpg", + "summary" => "298p3RG7j27tfsZ9RQ.jpg", + "type" => "Document", + "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg" + } + ], + "attributedTo" => "https://honk.tedunangst.com/u/tedu", + "content" => "", + "id" => "https://honk.tedunangst.com/u/tedu/chonk/26L4wl5yCbn4dr4y1b", + "published" => "2020-05-18T01:13:03Z", + "to" => [ + "https://dontbulling.me/users/lain" + ], + "type" => "ChatMessage" + }, + "published" => "2020-05-18T01:13:03Z", + "to" => [ + "https://dontbulling.me/users/lain" + ], + "type" => "Create" + } + + _user = insert(:user, ap_id: data["actor"]) + _user = insert(:user, ap_id: hd(data["to"])) + + assert {:ok, _activity} = Transmogrifier.handle_incoming(data) + end + + test "it rejects messages that don't contain content" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + object = + data["object"] + |> Map.delete("content") + + data = + data + |> Map.put("object", object) + + _author = + insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) + + _recipient = + insert(:user, + ap_id: List.first(data["to"]), + local: true, + last_refreshed_at: DateTime.utc_now() + ) + + {:error, _} = Transmogrifier.handle_incoming(data) + end + + test "it rejects messages that don't concern local users" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + _author = + insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) + + _recipient = + insert(:user, + ap_id: List.first(data["to"]), + local: false, + last_refreshed_at: DateTime.utc_now() + ) + + {:error, _} = Transmogrifier.handle_incoming(data) + end + + test "it rejects messages where the `to` field of activity and object don't match" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + author = insert(:user, ap_id: data["actor"]) + _recipient = insert(:user, ap_id: List.first(data["to"])) + + data = + data + |> Map.put("to", author.ap_id) + + assert match?({:error, _}, Transmogrifier.handle_incoming(data)) + refute Object.get_by_ap_id(data["object"]["id"]) + end + + test "it fetches the actor if they aren't in our system" do + Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + |> Map.put("actor", "http://mastodon.example.org/users/admin") + |> put_in(["object", "actor"], "http://mastodon.example.org/users/admin") + + _recipient = insert(:user, ap_id: List.first(data["to"]), local: true) + + {:ok, %Activity{} = _activity} = Transmogrifier.handle_incoming(data) + end + + test "it doesn't work for deactivated users" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + _author = + insert(:user, + ap_id: data["actor"], + local: false, + last_refreshed_at: DateTime.utc_now(), + deactivated: true + ) + + _recipient = insert(:user, ap_id: List.first(data["to"]), local: true) + + assert {:error, _} = Transmogrifier.handle_incoming(data) + end + + test "it inserts it and creates a chat" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + author = + insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) + + recipient = insert(:user, ap_id: List.first(data["to"]), local: true) + + {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(data) + assert activity.local == false + + assert activity.actor == author.ap_id + assert activity.recipients == [recipient.ap_id, author.ap_id] + + %Object{} = object = Object.get_by_ap_id(activity.data["object"]) + + assert object + assert object.data["content"] == "You expected a cute girl? Too bad. alert(&#39;XSS&#39;)" + assert match?(%{"firefox" => _}, object.data["emoji"]) + + refute Chat.get(author.id, recipient.ap_id) + assert Chat.get(recipient.id, author.ap_id) + end + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/delete_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/delete_handling_test.exs @@ -0,0 +1,114 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.DeleteHandlingTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "it works for incoming deletes" do + activity = insert(:note_activity) + deleting_user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-delete.json") + |> Poison.decode!() + |> Map.put("actor", deleting_user.ap_id) + |> put_in(["object", "id"], activity.data["object"]) + + {:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} = + Transmogrifier.handle_incoming(data) + + assert id == data["id"] + + # We delete the Create activity because we base our timelines on it. + # This should be changed after we unify objects and activities + refute Activity.get_by_id(activity.id) + assert actor == deleting_user.ap_id + + # Objects are replaced by a tombstone object. + object = Object.normalize(activity.data["object"]) + assert object.data["type"] == "Tombstone" + end + + test "it works for incoming when the object has been pruned" do + activity = insert(:note_activity) + + {:ok, object} = + Object.normalize(activity.data["object"]) + |> Repo.delete() + + Cachex.del(:object_cache, "object:#{object.data["id"]}") + + deleting_user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-delete.json") + |> Poison.decode!() + |> Map.put("actor", deleting_user.ap_id) + |> put_in(["object", "id"], activity.data["object"]) + + {:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} = + Transmogrifier.handle_incoming(data) + + assert id == data["id"] + + # We delete the Create activity because we base our timelines on it. + # This should be changed after we unify objects and activities + refute Activity.get_by_id(activity.id) + assert actor == deleting_user.ap_id + end + + test "it fails for incoming deletes with spoofed origin" do + activity = insert(:note_activity) + %{ap_id: ap_id} = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo") + + data = + File.read!("test/fixtures/mastodon-delete.json") + |> Poison.decode!() + |> Map.put("actor", ap_id) + |> put_in(["object", "id"], activity.data["object"]) + + assert match?({:error, _}, Transmogrifier.handle_incoming(data)) + end + + @tag capture_log: true + test "it works for incoming user deletes" do + %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin") + + data = + File.read!("test/fixtures/mastodon-delete-user.json") + |> Poison.decode!() + + {:ok, _} = Transmogrifier.handle_incoming(data) + ObanHelpers.perform_all() + + assert User.get_cached_by_ap_id(ap_id).deactivated + end + + test "it fails for incoming user deletes with spoofed origin" do + %{ap_id: ap_id} = insert(:user) + + data = + File.read!("test/fixtures/mastodon-delete-user.json") + |> Poison.decode!() + |> Map.put("actor", ap_id) + + assert match?({:error, _}, Transmogrifier.handle_incoming(data)) + + assert User.get_cached_by_ap_id(ap_id) + 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 @@ -0,0 +1,61 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it works for incoming emoji reactions" do + user = insert(:user) + other_user = insert(:user, local: false) + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/emoji-reaction.json") + |> Poison.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"] == "http://mastodon.example.org/users/admin#reactions/2" + assert data["object"] == activity.data["object"] + assert data["content"] == "👌" + + object = Object.get_by_ap_id(data["object"]) + + assert object.data["reaction_count"] == 1 + assert match?([["👌", _]], object.data["reactions"]) + end + + test "it reject invalid emoji reactions" do + user = insert(:user) + other_user = insert(:user, local: false) + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/emoji-reaction-too-long.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("actor", other_user.ap_id) + + assert {:error, _} = Transmogrifier.handle_incoming(data) + + data = + File.read!("test/fixtures/emoji-reaction-no-emoji.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("actor", other_user.ap_id) + + assert {:error, _} = Transmogrifier.handle_incoming(data) + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/event_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/event_handling_test.exs @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.EventHandlingTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Object.Fetcher + + test "Mobilizon Event object" do + Tesla.Mock.mock(fn + %{url: "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/mobilizon.org-event.json") + } + + %{url: "https://mobilizon.org/@tcit"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/mobilizon.org-user.json") + } + end) + + assert {:ok, object} = + Fetcher.fetch_object_from_id( + "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" + ) + + assert object.data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] + assert object.data["cc"] == [] + + assert object.data["url"] == + "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" + + assert object.data["published"] == "2019-12-17T11:33:56Z" + assert object.data["name"] == "Mobilizon Launching Party" + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/follow_handling_test.exs @@ -0,0 +1,208 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do + use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.Notification + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.ActivityPub.Utils + + import Pleroma.Factory + import Ecto.Query + import Mock + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "handle_incoming" do + setup do: clear_config([:user, :deny_follow_blocked]) + + test "it works for osada follow request" do + user = insert(:user) + + data = + File.read!("test/fixtures/osada-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "https://apfed.club/channel/indio" + assert data["type"] == "Follow" + assert data["id"] == "https://apfed.club/follow/9" + + activity = Repo.get(Activity, activity.id) + assert activity.data["state"] == "accept" + assert User.following?(User.get_cached_by_ap_id(data["actor"]), user) + end + + test "it works for incoming follow requests" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "http://mastodon.example.org/users/admin" + assert data["type"] == "Follow" + assert data["id"] == "http://mastodon.example.org/users/admin#follows/2" + + activity = Repo.get(Activity, activity.id) + assert activity.data["state"] == "accept" + assert User.following?(User.get_cached_by_ap_id(data["actor"]), user) + + [notification] = Notification.for_user(user) + assert notification.type == "follow" + end + + test "with locked accounts, it does create a Follow, but not an Accept" do + user = insert(:user, locked: true) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["state"] == "pending" + + refute User.following?(User.get_cached_by_ap_id(data["actor"]), user) + + accepts = + from( + a in Activity, + where: fragment("?->>'type' = ?", a.data, "Accept") + ) + |> Repo.all() + + assert Enum.empty?(accepts) + + [notification] = Notification.for_user(user) + assert notification.type == "follow_request" + end + + test "it works for follow requests when you are already followed, creating a new accept activity" do + # This is important because the remote might have the wrong idea about the + # current follow status. This can lead to instance A thinking that x@A is + # followed by y@B, but B thinks they are not. In this case, the follow can + # never go through again because it will never get an Accept. + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) + + accepts = + from( + a in Activity, + where: fragment("?->>'type' = ?", a.data, "Accept") + ) + |> Repo.all() + + assert length(accepts) == 1 + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("id", String.replace(data["id"], "2", "3")) + |> Map.put("object", user.ap_id) + + {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) + + accepts = + from( + a in Activity, + where: fragment("?->>'type' = ?", a.data, "Accept") + ) + |> Repo.all() + + assert length(accepts) == 2 + end + + test "it rejects incoming follow requests from blocked users when deny_follow_blocked is enabled" do + Pleroma.Config.put([:user, :deny_follow_blocked], true) + + user = insert(:user) + {:ok, target} = User.get_or_fetch("http://mastodon.example.org/users/admin") + + {:ok, _user_relationship} = User.block(user, target) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + {:ok, %Activity{data: %{"id" => id}}} = Transmogrifier.handle_incoming(data) + + %Activity{} = activity = Activity.get_by_ap_id(id) + + assert activity.data["state"] == "reject" + end + + test "it rejects incoming follow requests if the following errors for some reason" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + with_mock Pleroma.User, [:passthrough], follow: fn _, _, _ -> {:error, :testing} end do + {:ok, %Activity{data: %{"id" => id}}} = Transmogrifier.handle_incoming(data) + + %Activity{} = activity = Activity.get_by_ap_id(id) + + assert activity.data["state"] == "reject" + end + end + + test "it works for incoming follow requests from hubzilla" do + user = insert(:user) + + data = + File.read!("test/fixtures/hubzilla-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + |> Utils.normalize_params() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "https://hubzilla.example.org/channel/kaniini" + assert data["type"] == "Follow" + assert data["id"] == "https://hubzilla.example.org/channel/kaniini#follows/2" + assert User.following?(User.get_cached_by_ap_id(data["actor"]), user) + end + + test "it works for incoming follows to locked account" do + pending_follower = insert(:user, ap_id: "http://mastodon.example.org/users/admin") + user = insert(:user, locked: true) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Follow" + assert data["object"] == user.ap_id + assert data["state"] == "pending" + assert data["actor"] == "http://mastodon.example.org/users/admin" + + assert [^pending_follower] = User.get_follow_requests(user) + end + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs @@ -0,0 +1,78 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it works for incoming likes" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/mastodon-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + _actor = insert(:user, ap_id: data["actor"], local: false) + + {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) + + refute Enum.empty?(activity.recipients) + + assert data["actor"] == "http://mastodon.example.org/users/admin" + assert data["type"] == "Like" + assert data["id"] == "http://mastodon.example.org/users/admin#likes/2" + assert data["object"] == activity.data["object"] + end + + test "it works for incoming misskey likes, turning them into EmojiReacts" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/misskey-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + _actor = insert(:user, ap_id: data["actor"], local: false) + + {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data) + + assert activity_data["actor"] == data["actor"] + assert activity_data["type"] == "EmojiReact" + assert activity_data["id"] == data["id"] + assert activity_data["object"] == activity.data["object"] + assert activity_data["content"] == "🍮" + end + + test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReacts" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/misskey-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("_misskey_reaction", "⭐") + + _actor = insert(:user, ap_id: data["actor"], local: false) + + {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data) + + assert activity_data["actor"] == data["actor"] + assert activity_data["type"] == "EmojiReact" + assert activity_data["id"] == data["id"] + assert activity_data["object"] == activity.data["object"] + assert activity_data["content"] == "⭐" + 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 @@ -0,0 +1,176 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.QuestionHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "Mastodon Question activity" do + data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!() + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + object = Object.normalize(activity, false) + + assert object.data["url"] == "https://mastodon.sdf.org/@rinpatch/102070944809637304" + + assert object.data["closed"] == "2019-05-11T09:03:36Z" + + assert object.data["context"] == activity.data["context"] + + 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"]) == + Enum.sort([ + %{ + "name" => "25 char limit is dumb", + "replies" => %{"totalItems" => 0, "type" => "Collection"}, + "type" => "Note" + }, + %{ + "name" => "Dunno", + "replies" => %{"totalItems" => 0, "type" => "Collection"}, + "type" => "Note" + }, + %{ + "name" => "Everyone knows that!", + "replies" => %{"totalItems" => 1, "type" => "Collection"}, + "type" => "Note" + }, + %{ + "name" => "I can't even fit a funny", + "replies" => %{"totalItems" => 1, "type" => "Collection"}, + "type" => "Note" + } + ]) + + user = insert(:user) + + {:ok, reply_activity} = CommonAPI.post(user, %{status: "hewwo", in_reply_to_id: activity.id}) + + reply_object = Object.normalize(reply_activity, 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 + options = [ + %{ + "type" => "Note", + "name" => "<input type=\"date\">", + "replies" => %{"totalItems" => 0, "type" => "Collection"} + }, + %{ + "type" => "Note", + "name" => "<input type=\"date\"/>", + "replies" => %{"totalItems" => 0, "type" => "Collection"} + }, + %{ + "type" => "Note", + "name" => "<input type=\"date\" />", + "replies" => %{"totalItems" => 1, "type" => "Collection"} + }, + %{ + "type" => "Note", + "name" => "<input type=\"date\"></input>", + "replies" => %{"totalItems" => 1, "type" => "Collection"} + } + ] + + data = + File.read!("test/fixtures/mastodon-question-activity.json") + |> Poison.decode!() + |> Kernel.put_in(["object", "oneOf"], options) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + object = Object.normalize(activity, false) + + assert Enum.sort(object.data["oneOf"]) == Enum.sort(options) + end + + test "Mastodon Question activity with custom emojis" do + options = [ + %{ + "type" => "Note", + "name" => ":blobcat:", + "replies" => %{"totalItems" => 0, "type" => "Collection"} + }, + %{ + "type" => "Note", + "name" => ":blobfox:", + "replies" => %{"totalItems" => 0, "type" => "Collection"} + } + ] + + tag = [ + %{ + "icon" => %{ + "type" => "Image", + "url" => "https://blob.cat/emoji/custom/blobcats/blobcat.png" + }, + "id" => "https://blob.cat/emoji/custom/blobcats/blobcat.png", + "name" => ":blobcat:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + }, + %{ + "icon" => %{"type" => "Image", "url" => "https://blob.cat/emoji/blobfox/blobfox.png"}, + "id" => "https://blob.cat/emoji/blobfox/blobfox.png", + "name" => ":blobfox:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + } + ] + + data = + File.read!("test/fixtures/mastodon-question-activity.json") + |> Poison.decode!() + |> Kernel.put_in(["object", "oneOf"], options) + |> Kernel.put_in(["object", "tag"], tag) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + object = Object.normalize(activity, false) + + assert object.data["oneOf"] == options + + assert object.data["emoji"] == %{ + "blobcat" => "https://blob.cat/emoji/custom/blobcats/blobcat.png", + "blobfox" => "https://blob.cat/emoji/blobfox/blobfox.png" + } + end + + test "returns same activity if received a second time" do + data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!() + + assert {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert {:ok, ^activity} = Transmogrifier.handle_incoming(data) + end + + test "accepts a Question with no content" do + data = + File.read!("test/fixtures/mastodon-question-activity.json") + |> Poison.decode!() + |> Kernel.put_in(["object", "content"], "") + + assert {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/reject_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/reject_handling_test.exs @@ -0,0 +1,67 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.RejectHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it fails for incoming rejects which cannot be correlated" do + follower = insert(:user) + followed = insert(:user, locked: true) + + accept_data = + File.read!("test/fixtures/mastodon-reject-activity.json") + |> Poison.decode!() + |> Map.put("actor", followed.ap_id) + + accept_data = + Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) + + {:error, _} = Transmogrifier.handle_incoming(accept_data) + + follower = User.get_cached_by_id(follower.id) + + refute User.following?(follower, followed) == true + end + + test "it works for incoming rejects which are referenced by IRI only" do + follower = insert(:user) + followed = insert(:user, locked: true) + + {:ok, follower} = User.follow(follower, followed) + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) + + assert User.following?(follower, followed) == true + + reject_data = + File.read!("test/fixtures/mastodon-reject-activity.json") + |> Poison.decode!() + |> Map.put("actor", followed.ap_id) + |> Map.put("object", follow_activity.data["id"]) + + {:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data) + + follower = User.get_cached_by_id(follower.id) + + assert User.following?(follower, followed) == false + end + + test "it rejects activities without a valid ID" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + |> Map.put("id", "") + + :error = Transmogrifier.handle_incoming(data) + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/undo_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/undo_handling_test.exs @@ -0,0 +1,185 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.UndoHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it works for incoming emoji reaction undos" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + {:ok, reaction_activity} = CommonAPI.react_with_emoji(activity.id, user, "👌") + + data = + File.read!("test/fixtures/mastodon-undo-like.json") + |> Poison.decode!() + |> Map.put("object", reaction_activity.data["id"]) + |> Map.put("actor", user.ap_id) + + {:ok, activity} = Transmogrifier.handle_incoming(data) + + assert activity.actor == user.ap_id + assert activity.data["id"] == data["id"] + assert activity.data["type"] == "Undo" + end + + test "it returns an error for incoming unlikes wihout a like activity" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "leave a like pls"}) + + data = + File.read!("test/fixtures/mastodon-undo-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + assert Transmogrifier.handle_incoming(data) == :error + end + + test "it works for incoming unlikes with an existing like activity" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "leave a like pls"}) + + like_data = + File.read!("test/fixtures/mastodon-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + _liker = insert(:user, ap_id: like_data["actor"], local: false) + + {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data) + + data = + File.read!("test/fixtures/mastodon-undo-like.json") + |> Poison.decode!() + |> Map.put("object", like_data) + |> Map.put("actor", like_data["actor"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "http://mastodon.example.org/users/admin" + assert data["type"] == "Undo" + assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo" + assert data["object"] == "http://mastodon.example.org/users/admin#likes/2" + + note = Object.get_by_ap_id(like_data["object"]) + assert note.data["like_count"] == 0 + assert note.data["likes"] == [] + end + + test "it works for incoming unlikes with an existing like activity and a compact object" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "leave a like pls"}) + + like_data = + File.read!("test/fixtures/mastodon-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + _liker = insert(:user, ap_id: like_data["actor"], local: false) + + {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data) + + data = + File.read!("test/fixtures/mastodon-undo-like.json") + |> Poison.decode!() + |> Map.put("object", like_data["id"]) + |> Map.put("actor", like_data["actor"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "http://mastodon.example.org/users/admin" + assert data["type"] == "Undo" + assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo" + assert data["object"] == "http://mastodon.example.org/users/admin#likes/2" + end + + test "it works for incoming unannounces with an existing notice" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + + announce_data = + File.read!("test/fixtures/mastodon-announce.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + _announcer = insert(:user, ap_id: announce_data["actor"], local: false) + + {:ok, %Activity{data: announce_data, local: false}} = + Transmogrifier.handle_incoming(announce_data) + + data = + File.read!("test/fixtures/mastodon-undo-announce.json") + |> Poison.decode!() + |> Map.put("object", announce_data) + |> Map.put("actor", announce_data["actor"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Undo" + + assert data["object"] == + "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" + end + + test "it works for incoming unfollows with an existing follow" do + user = insert(:user) + + follow_data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + _follower = insert(:user, ap_id: follow_data["actor"], local: false) + + {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data) + + data = + File.read!("test/fixtures/mastodon-unfollow-activity.json") + |> Poison.decode!() + |> Map.put("object", follow_data) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Undo" + assert data["object"]["type"] == "Follow" + assert data["object"]["object"] == user.ap_id + assert data["actor"] == "http://mastodon.example.org/users/admin" + + refute User.following?(User.get_cached_by_ap_id(data["actor"]), user) + end + + test "it works for incoming unblocks with an existing block" do + user = insert(:user) + + block_data = + File.read!("test/fixtures/mastodon-block-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + _blocker = insert(:user, ap_id: block_data["actor"], local: false) + + {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data) + + data = + File.read!("test/fixtures/mastodon-unblock-activity.json") + |> Poison.decode!() + |> Map.put("object", block_data) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + assert data["type"] == "Undo" + assert data["object"] == block_data["id"] + + blocker = User.get_cached_by_ap_id(data["actor"]) + + refute User.blocks?(blocker, user) + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/user_update_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/user_update_handling_test.exs @@ -0,0 +1,159 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.UserUpdateHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + test "it works for incoming update activities" do + user = insert(:user, local: false) + + update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() + + object = + update_data["object"] + |> Map.put("actor", user.ap_id) + |> Map.put("id", user.ap_id) + + update_data = + update_data + |> Map.put("actor", user.ap_id) + |> Map.put("object", object) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data) + + assert data["id"] == update_data["id"] + + user = User.get_cached_by_ap_id(data["actor"]) + assert user.name == "gargle" + + assert user.avatar["url"] == [ + %{ + "href" => + "https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg" + } + ] + + assert user.banner["url"] == [ + %{ + "href" => + "https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" + } + ] + + assert user.bio == "<p>Some bio</p>" + end + + test "it works with alsoKnownAs" do + %{ap_id: actor} = insert(:user, local: false) + + assert User.get_cached_by_ap_id(actor).also_known_as == [] + + {:ok, _activity} = + "test/fixtures/mastodon-update.json" + |> File.read!() + |> Poison.decode!() + |> Map.put("actor", actor) + |> Map.update!("object", fn object -> + object + |> Map.put("actor", actor) + |> Map.put("id", actor) + |> Map.put("alsoKnownAs", [ + "http://mastodon.example.org/users/foo", + "http://example.org/users/bar" + ]) + end) + |> Transmogrifier.handle_incoming() + + assert User.get_cached_by_ap_id(actor).also_known_as == [ + "http://mastodon.example.org/users/foo", + "http://example.org/users/bar" + ] + end + + test "it works with custom profile fields" do + user = insert(:user, local: false) + + assert user.fields == [] + + update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() + + object = + update_data["object"] + |> Map.put("actor", user.ap_id) + |> Map.put("id", user.ap_id) + + update_data = + update_data + |> Map.put("actor", user.ap_id) + |> Map.put("object", object) + + {:ok, _update_activity} = Transmogrifier.handle_incoming(update_data) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert user.fields == [ + %{"name" => "foo", "value" => "updated"}, + %{"name" => "foo1", "value" => "updated"} + ] + + Pleroma.Config.put([:instance, :max_remote_account_fields], 2) + + update_data = + update_data + |> put_in(["object", "attachment"], [ + %{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}, + %{"name" => "foo11", "type" => "PropertyValue", "value" => "bar11"}, + %{"name" => "foo22", "type" => "PropertyValue", "value" => "bar22"} + ]) + |> Map.put("id", update_data["id"] <> ".") + + {:ok, _} = Transmogrifier.handle_incoming(update_data) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert user.fields == [ + %{"name" => "foo", "value" => "updated"}, + %{"name" => "foo1", "value" => "updated"} + ] + + update_data = + update_data + |> put_in(["object", "attachment"], []) + |> Map.put("id", update_data["id"] <> ".") + + {:ok, _} = Transmogrifier.handle_incoming(update_data) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert user.fields == [] + end + + test "it works for incoming update activities which lock the account" do + user = insert(:user, local: false) + + update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() + + object = + update_data["object"] + |> Map.put("actor", user.ap_id) + |> Map.put("id", user.ap_id) + |> Map.put("manuallyApprovesFollowers", true) + + update_data = + update_data + |> Map.put("actor", user.ap_id) + |> Map.put("object", object) + + {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(update_data) + + user = User.get_cached_by_ap_id(user.ap_id) + assert user.locked == true + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/video_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/video_handling_test.exs @@ -0,0 +1,93 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.VideoHandlingTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Object.Fetcher + alias Pleroma.Web.ActivityPub.Transmogrifier + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "skip converting the content when it is nil" do + data = + File.read!("test/fixtures/tesla_mock/framatube.org-video.json") + |> Jason.decode!() + |> Kernel.put_in(["object", "content"], nil) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert object = Object.normalize(activity, false) + + assert object.data["content"] == nil + end + + test "it converts content of object to html" do + data = File.read!("test/fixtures/tesla_mock/framatube.org-video.json") |> Jason.decode!() + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert object = Object.normalize(activity, false) + + assert object.data["content"] == + "<p>Après avoir mené avec un certain succès la campagne « Dégooglisons Internet » en 2014, l’association Framasoft annonce fin 2019 arrêter progressivement un certain nombre de ses services alternatifs aux GAFAM. Pourquoi ?</p><p>Transcription par @aprilorg ici : <a href=\"https://www.april.org/deframasoftisons-internet-pierre-yves-gosset-framasoft\">https://www.april.org/deframasoftisons-internet-pierre-yves-gosset-framasoft</a></p>" + end + + test "it remaps video URLs as attachments if necessary" do + {:ok, object} = + Fetcher.fetch_object_from_id( + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" + ) + + assert object.data["url"] == + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" + + assert object.data["attachment"] == [ + %{ + "type" => "Link", + "mediaType" => "video/mp4", + "name" => nil, + "url" => [ + %{ + "href" => + "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + } + ] + + data = File.read!("test/fixtures/tesla_mock/framatube.org-video.json") |> Jason.decode!() + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert object = Object.normalize(activity, false) + + assert object.data["attachment"] == [ + %{ + "type" => "Link", + "mediaType" => "video/mp4", + "name" => nil, + "url" => [ + %{ + "href" => + "https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + } + ] + + assert object.data["url"] == + "https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206" + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -0,0 +1,1220 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.CommonAPI + + import Mock + import Pleroma.Factory + import ExUnit.CaptureLog + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup do: clear_config([:instance, :max_remote_account_fields]) + + describe "handle_incoming" do + test "it works for incoming notices with tag not being an array (kroeg)" do + data = File.read!("test/fixtures/kroeg-array-less-emoji.json") |> Poison.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + object = Object.normalize(data["object"]) + + assert object.data["emoji"] == %{ + "icon_e_smile" => "https://puckipedia.com/forum/images/smilies/icon_e_smile.png" + } + + data = File.read!("test/fixtures/kroeg-array-less-hashtag.json") |> Poison.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + object = Object.normalize(data["object"]) + + assert "test" in object.data["tag"] + end + + test "it cleans up incoming notices which are not really DMs" do + user = insert(:user) + other_user = insert(:user) + + to = [user.ap_id, other_user.ap_id] + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + |> Map.put("to", to) + |> Map.put("cc", []) + + object = + data["object"] + |> Map.put("to", to) + |> Map.put("cc", []) + + data = Map.put(data, "object", object) + + {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert data["to"] == [] + assert data["cc"] == to + + object_data = Object.normalize(activity).data + + assert object_data["to"] == [] + assert object_data["cc"] == to + end + + test "it ignores an incoming notice if we already have it" do + activity = insert(:note_activity) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + |> Map.put("object", Object.normalize(activity).data) + + {:ok, returned_activity} = Transmogrifier.handle_incoming(data) + + assert activity == returned_activity + end + + @tag capture_log: true + test "it fetches reply-to activities if we don't have them" do + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + object = + data["object"] + |> Map.put("inReplyTo", "https://mstdn.io/users/mayuutann/statuses/99568293732299394") + + data = Map.put(data, "object", object) + {:ok, returned_activity} = Transmogrifier.handle_incoming(data) + returned_object = Object.normalize(returned_activity, false) + + assert activity = + Activity.get_create_by_object_ap_id( + "https://mstdn.io/users/mayuutann/statuses/99568293732299394" + ) + + assert returned_object.data["inReplyTo"] == + "https://mstdn.io/users/mayuutann/statuses/99568293732299394" + end + + test "it does not fetch reply-to activities beyond max replies depth limit" do + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + object = + data["object"] + |> Map.put("inReplyTo", "https://shitposter.club/notice/2827873") + + data = Map.put(data, "object", object) + + with_mock Pleroma.Web.Federator, + allowed_thread_distance?: fn _ -> false end do + {:ok, returned_activity} = Transmogrifier.handle_incoming(data) + + returned_object = Object.normalize(returned_activity, false) + + refute Activity.get_create_by_object_ap_id( + "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" + ) + + assert returned_object.data["inReplyTo"] == "https://shitposter.club/notice/2827873" + end + end + + test "it does not crash if the object in inReplyTo can't be fetched" do + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + object = + data["object"] + |> Map.put("inReplyTo", "https://404.site/whatever") + + data = + data + |> Map.put("object", object) + + assert capture_log(fn -> + {:ok, _returned_activity} = Transmogrifier.handle_incoming(data) + end) =~ "[warn] Couldn't fetch \"https://404.site/whatever\", error: nil" + end + + test "it does not work for deactivated users" do + data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() + + insert(:user, ap_id: data["actor"], deactivated: true) + + assert {:error, _} = Transmogrifier.handle_incoming(data) + end + + test "it works for incoming notices" do + data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["id"] == + "http://mastodon.example.org/users/admin/statuses/99512778738411822/activity" + + assert data["context"] == + "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation" + + assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] + + assert data["cc"] == [ + "http://mastodon.example.org/users/admin/followers", + "http://localtesting.pleroma.lol/users/lain" + ] + + assert data["actor"] == "http://mastodon.example.org/users/admin" + + object_data = Object.normalize(data["object"]).data + + assert object_data["id"] == + "http://mastodon.example.org/users/admin/statuses/99512778738411822" + + assert object_data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] + + assert object_data["cc"] == [ + "http://mastodon.example.org/users/admin/followers", + "http://localtesting.pleroma.lol/users/lain" + ] + + assert object_data["actor"] == "http://mastodon.example.org/users/admin" + assert object_data["attributedTo"] == "http://mastodon.example.org/users/admin" + + assert object_data["context"] == + "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation" + + assert object_data["sensitive"] == true + + user = User.get_cached_by_ap_id(object_data["actor"]) + + assert user.note_count == 1 + end + + test "it works for incoming notices with hashtags" do + data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Poison.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + object = Object.normalize(data["object"]) + + assert Enum.at(object.data["tag"], 2) == "moo" + end + + test "it works for incoming notices with contentMap" do + data = + File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + object = Object.normalize(data["object"]) + + 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") |> Poison.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + object = Object.normalize(data["object"]) + + assert object.data["content"] == + "<p>henlo from my Psion netBook</p><p>message sent from my Psion netBook</p>" + end + + test "it ensures that as:Public activities make it to their followers collection" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + |> Map.put("actor", user.ap_id) + |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"]) + |> Map.put("cc", []) + + object = + data["object"] + |> Map.put("attributedTo", user.ap_id) + |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"]) + |> Map.put("cc", []) + |> Map.put("id", user.ap_id <> "/activities/12345678") + + data = Map.put(data, "object", object) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["cc"] == [User.ap_followers(user)] + end + + test "it ensures that address fields become lists" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + |> Map.put("actor", user.ap_id) + |> Map.put("to", nil) + |> Map.put("cc", nil) + + object = + data["object"] + |> Map.put("attributedTo", user.ap_id) + |> Map.put("to", nil) + |> Map.put("cc", nil) + |> Map.put("id", user.ap_id <> "/activities/12345678") + + data = Map.put(data, "object", object) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert !is_nil(data["to"]) + assert !is_nil(data["cc"]) + end + + test "it strips internal likes" do + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + likes = %{ + "first" => + "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1", + "id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes", + "totalItems" => 3, + "type" => "OrderedCollection" + } + + object = Map.put(data["object"], "likes", likes) + data = Map.put(data, "object", object) + + {:ok, %Activity{object: object}} = Transmogrifier.handle_incoming(data) + + refute Map.has_key?(object.data, "likes") + end + + test "it strips internal reactions" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "📢") + + %{object: object} = Activity.get_by_id_with_object(activity.id) + assert Map.has_key?(object.data, "reactions") + assert Map.has_key?(object.data, "reaction_count") + + object_data = Transmogrifier.strip_internal_fields(object.data) + refute Map.has_key?(object_data, "reactions") + refute Map.has_key?(object_data, "reaction_count") + end + + test "it works for incoming unfollows with an existing follow" do + user = insert(:user) + + follow_data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data) + + data = + File.read!("test/fixtures/mastodon-unfollow-activity.json") + |> Poison.decode!() + |> Map.put("object", follow_data) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Undo" + assert data["object"]["type"] == "Follow" + assert data["object"]["object"] == user.ap_id + assert data["actor"] == "http://mastodon.example.org/users/admin" + + refute User.following?(User.get_cached_by_ap_id(data["actor"]), user) + end + + test "it accepts Flag activities" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) + object = Object.normalize(activity) + + note_obj = %{ + "type" => "Note", + "id" => activity.data["id"], + "content" => "test post", + "published" => object.data["published"], + "actor" => AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + } + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "cc" => [user.ap_id], + "object" => [user.ap_id, activity.data["id"]], + "type" => "Flag", + "content" => "blocked AND reported!!!", + "actor" => other_user.ap_id + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + assert activity.data["object"] == [user.ap_id, note_obj] + assert activity.data["content"] == "blocked AND reported!!!" + assert activity.data["actor"] == other_user.ap_id + assert activity.data["cc"] == [user.ap_id] + end + + test "it correctly processes messages with non-array to field" do + user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => "https://www.w3.org/ns/activitystreams#Public", + "type" => "Create", + "object" => %{ + "content" => "blah blah blah", + "type" => "Note", + "attributedTo" => user.ap_id, + "inReplyTo" => nil + }, + "actor" => user.ap_id + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["to"] + end + + test "it correctly processes messages with non-array cc field" do + user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => user.follower_address, + "cc" => "https://www.w3.org/ns/activitystreams#Public", + "type" => "Create", + "object" => %{ + "content" => "blah blah blah", + "type" => "Note", + "attributedTo" => user.ap_id, + "inReplyTo" => nil + }, + "actor" => user.ap_id + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"] + assert [user.follower_address] == activity.data["to"] + end + + test "it correctly processes messages with weirdness in address fields" do + user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => [nil, user.follower_address], + "cc" => ["https://www.w3.org/ns/activitystreams#Public", ["¿"]], + "type" => "Create", + "object" => %{ + "content" => "…", + "type" => "Note", + "attributedTo" => user.ap_id, + "inReplyTo" => nil + }, + "actor" => user.ap_id + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"] + assert [user.follower_address] == activity.data["to"] + end + + test "it accepts Move activities" do + old_user = insert(:user) + new_user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Move", + "actor" => old_user.ap_id, + "object" => old_user.ap_id, + "target" => new_user.ap_id + } + + assert :error = Transmogrifier.handle_incoming(message) + + {:ok, _new_user} = User.update_and_set_cache(new_user, %{also_known_as: [old_user.ap_id]}) + + assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(message) + assert activity.actor == old_user.ap_id + assert activity.data["actor"] == old_user.ap_id + assert activity.data["object"] == old_user.ap_id + assert activity.data["target"] == new_user.ap_id + assert activity.data["type"] == "Move" + end + end + + describe "`handle_incoming/2`, Mastodon format `replies` handling" do + setup do: clear_config([:activitypub, :note_replies_output_limit], 5) + setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) + + setup do + data = + "test/fixtures/mastodon-post-activity.json" + |> File.read!() + |> Poison.decode!() + + items = get_in(data, ["object", "replies", "first", "items"]) + assert length(items) > 0 + + %{data: data, items: items} + end + + test "schedules background fetching of `replies` items if max thread depth limit allows", %{ + data: data, + items: items + } do + Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 10) + + {:ok, _activity} = Transmogrifier.handle_incoming(data) + + for id <- items do + job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1} + assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args) + end + end + + test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows", + %{data: data} do + Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0) + + {:ok, _activity} = Transmogrifier.handle_incoming(data) + + assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == [] + end + end + + describe "`handle_incoming/2`, Pleroma format `replies` handling" do + setup do: clear_config([:activitypub, :note_replies_output_limit], 5) + setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) + + setup do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "post1"}) + + {:ok, reply1} = + CommonAPI.post(user, %{status: "reply1", in_reply_to_status_id: activity.id}) + + {:ok, reply2} = + CommonAPI.post(user, %{status: "reply2", in_reply_to_status_id: activity.id}) + + replies_uris = Enum.map([reply1, reply2], fn a -> a.object.data["id"] end) + + {:ok, federation_output} = Transmogrifier.prepare_outgoing(activity.data) + + Repo.delete(activity.object) + Repo.delete(activity) + + %{federation_output: federation_output, replies_uris: replies_uris} + end + + test "schedules background fetching of `replies` items if max thread depth limit allows", %{ + federation_output: federation_output, + replies_uris: replies_uris + } do + Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 1) + + {:ok, _activity} = Transmogrifier.handle_incoming(federation_output) + + for id <- replies_uris do + job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1} + assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args) + end + end + + test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows", + %{federation_output: federation_output} do + Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0) + + {:ok, _activity} = Transmogrifier.handle_incoming(federation_output) + + assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == [] + end + end + + describe "prepare outgoing" do + test "it inlines private announced objects" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey", visibility: "private"}) + + {:ok, announce_activity} = CommonAPI.repeat(activity.id, user) + + {:ok, modified} = Transmogrifier.prepare_outgoing(announce_activity.data) + + assert modified["object"]["content"] == "hey" + assert modified["object"]["actor"] == modified["object"]["attributedTo"] + end + + test "it turns mentions into tags" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{status: "hey, @#{other_user.nickname}, how are ya? #2hu"}) + + with_mock Pleroma.Notification, + get_notified_from_activity: fn _, _ -> [] end do + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + object = modified["object"] + + expected_mention = %{ + "href" => other_user.ap_id, + "name" => "@#{other_user.nickname}", + "type" => "Mention" + } + + expected_tag = %{ + "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu", + "type" => "Hashtag", + "name" => "#2hu" + } + + refute called(Pleroma.Notification.get_notified_from_activity(:_, :_)) + assert Enum.member?(object["tag"], expected_tag) + assert Enum.member?(object["tag"], expected_mention) + end + end + + test "it adds the sensitive property" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#nsfw hey"}) + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["object"]["sensitive"] + end + + test "it adds the json-ld context and the conversation property" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["@context"] == + Pleroma.Web.ActivityPub.Utils.make_json_ld_header()["@context"] + + assert modified["object"]["conversation"] == modified["context"] + end + + test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["object"]["actor"] == modified["object"]["attributedTo"] + end + + test "it strips internal hashtag data" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#2hu"}) + + expected_tag = %{ + "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu", + "type" => "Hashtag", + "name" => "#2hu" + } + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["object"]["tag"] == [expected_tag] + end + + test "it strips internal fields" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#2hu :firefox:"}) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert length(modified["object"]["tag"]) == 2 + + assert is_nil(modified["object"]["emoji"]) + 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"]) + end + + test "it strips internal fields of article" do + activity = insert(:article_activity) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert length(modified["object"]["tag"]) == 2 + + assert is_nil(modified["object"]["emoji"]) + 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 + + test "the directMessage flag is present" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "2hu :moominmamma:"}) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["directMessage"] == false + + {:ok, activity} = CommonAPI.post(user, %{status: "@#{other_user.nickname} :moominmamma:"}) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["directMessage"] == false + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "@#{other_user.nickname} :moominmamma:", + visibility: "direct" + }) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["directMessage"] == true + end + + test "it strips BCC field" do + user = insert(:user) + {:ok, list} = Pleroma.List.create("foo", user) + + {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert is_nil(modified["bcc"]) + end + + test "it can handle Listen activities" do + listen_activity = insert(:listen) + + {:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data) + + assert modified["type"] == "Listen" + + user = insert(:user) + + {:ok, activity} = CommonAPI.listen(user, %{"title" => "lain radio episode 1"}) + + {:ok, _modified} = Transmogrifier.prepare_outgoing(activity.data) + 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" + + user = User.get_cached_by_id(user.id) + assert user.note_count == 1 + + activity = Activity.get_by_id(activity.id) + assert user.follower_address in activity.recipients + + assert %{ + "url" => [ + %{ + "href" => + "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg" + } + ] + } = user.avatar + + assert %{ + "url" => [ + %{ + "href" => + "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" + } + ] + } = user.banner + + refute "..." in activity.recipients + + unrelated_activity = Activity.get_by_id(unrelated_activity.id) + refute user.follower_address in unrelated_activity.recipients + + user_two = User.get_cached_by_id(user_two.id) + assert User.following?(user_two, user) + refute "..." in User.following(user_two) + end + end + + describe "actor rewriting" do + test "it fixes the actor URL property to be a proper URI" do + data = %{ + "url" => %{"href" => "http://example.com"} + } + + rewritten = Transmogrifier.maybe_fix_user_object(data) + assert rewritten["url"] == "http://example.com" + end + end + + describe "actor origin containment" do + test "it rejects activities which reference objects with bogus origins" do + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "http://mastodon.example.org/users/admin/activities/1234", + "actor" => "http://mastodon.example.org/users/admin", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => "https://info.pleroma.site/activity.json", + "type" => "Announce" + } + + assert capture_log(fn -> + {:error, _} = Transmogrifier.handle_incoming(data) + end) =~ "Object containment failed" + end + + test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "http://mastodon.example.org/users/admin/activities/1234", + "actor" => "http://mastodon.example.org/users/admin", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => "https://info.pleroma.site/activity2.json", + "type" => "Announce" + } + + assert capture_log(fn -> + {:error, _} = Transmogrifier.handle_incoming(data) + end) =~ "Object containment failed" + end + + test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "http://mastodon.example.org/users/admin/activities/1234", + "actor" => "http://mastodon.example.org/users/admin", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => "https://info.pleroma.site/activity3.json", + "type" => "Announce" + } + + assert capture_log(fn -> + {:error, _} = Transmogrifier.handle_incoming(data) + end) =~ "Object containment failed" + end + end + + describe "reserialization" do + test "successfully reserializes a message with inReplyTo == nil" do + user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "type" => "Create", + "object" => %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "type" => "Note", + "content" => "Hi", + "inReplyTo" => nil, + "attributedTo" => user.ap_id + }, + "actor" => user.ap_id + } + + {:ok, activity} = Transmogrifier.handle_incoming(message) + + {:ok, _} = Transmogrifier.prepare_outgoing(activity.data) + end + + test "successfully reserializes a message with AS2 objects in IR" do + user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "type" => "Create", + "object" => %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "type" => "Note", + "content" => "Hi", + "inReplyTo" => nil, + "attributedTo" => user.ap_id, + "tag" => [ + %{"name" => "#2hu", "href" => "http://example.com/2hu", "type" => "Hashtag"}, + %{"name" => "Bob", "href" => "http://example.com/bob", "type" => "Mention"} + ] + }, + "actor" => user.ap_id + } + + {:ok, activity} = Transmogrifier.handle_incoming(message) + + {:ok, _} = Transmogrifier.prepare_outgoing(activity.data) + end + end + + describe "fix_explicit_addressing" do + setup do + user = insert(:user) + [user: user] + end + + test "moves non-explicitly mentioned actors to cc", %{user: user} do + explicitly_mentioned_actors = [ + "https://pleroma.gold/users/user1", + "https://pleroma.gold/user2" + ] + + object = %{ + "actor" => user.ap_id, + "to" => explicitly_mentioned_actors ++ ["https://social.beepboop.ga/users/dirb"], + "cc" => [], + "tag" => + Enum.map(explicitly_mentioned_actors, fn href -> + %{"type" => "Mention", "href" => href} + end) + } + + fixed_object = Transmogrifier.fix_explicit_addressing(object) + assert Enum.all?(explicitly_mentioned_actors, &(&1 in fixed_object["to"])) + refute "https://social.beepboop.ga/users/dirb" in fixed_object["to"] + assert "https://social.beepboop.ga/users/dirb" in fixed_object["cc"] + end + + test "does not move actor's follower collection to cc", %{user: user} do + object = %{ + "actor" => user.ap_id, + "to" => [user.follower_address], + "cc" => [] + } + + fixed_object = Transmogrifier.fix_explicit_addressing(object) + assert user.follower_address in fixed_object["to"] + refute user.follower_address in fixed_object["cc"] + end + + test "removes recipient's follower collection from cc", %{user: user} do + recipient = insert(:user) + + object = %{ + "actor" => user.ap_id, + "to" => [recipient.ap_id, "https://www.w3.org/ns/activitystreams#Public"], + "cc" => [user.follower_address, recipient.follower_address] + } + + fixed_object = Transmogrifier.fix_explicit_addressing(object) + + assert user.follower_address in fixed_object["cc"] + refute recipient.follower_address in fixed_object["cc"] + refute recipient.follower_address in fixed_object["to"] + end + end + + describe "fix_summary/1" do + test "returns fixed object" do + assert Transmogrifier.fix_summary(%{"summary" => nil}) == %{"summary" => ""} + assert Transmogrifier.fix_summary(%{"summary" => "ok"}) == %{"summary" => "ok"} + assert Transmogrifier.fix_summary(%{}) == %{"summary" => ""} + end + end + + describe "fix_in_reply_to/2" do + setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) + + setup do + data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) + [data: data] + end + + test "returns not modified object when hasn't containts inReplyTo field", %{data: data} do + assert Transmogrifier.fix_in_reply_to(data) == data + end + + test "returns object with inReplyTo when denied incoming reply", %{data: data} do + Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0) + + object_with_reply = + Map.put(data["object"], "inReplyTo", "https://shitposter.club/notice/2827873") + + modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) + assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873" + + object_with_reply = + Map.put(data["object"], "inReplyTo", %{"id" => "https://shitposter.club/notice/2827873"}) + + modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) + assert modified_object["inReplyTo"] == %{"id" => "https://shitposter.club/notice/2827873"} + + object_with_reply = + Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"]) + + modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) + assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"] + + object_with_reply = Map.put(data["object"], "inReplyTo", []) + modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) + assert modified_object["inReplyTo"] == [] + end + + @tag capture_log: true + test "returns modified object when allowed incoming reply", %{data: data} do + object_with_reply = + Map.put( + data["object"], + "inReplyTo", + "https://mstdn.io/users/mayuutann/statuses/99568293732299394" + ) + + Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 5) + modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) + + assert modified_object["inReplyTo"] == + "https://mstdn.io/users/mayuutann/statuses/99568293732299394" + + assert modified_object["context"] == + "tag:shitposter.club,2018-02-22:objectType=thread:nonce=e5a7c72d60a9c0e4" + end + end + + describe "fix_url/1" do + test "fixes data for object when url is map" do + object = %{ + "url" => %{ + "type" => "Link", + "mimeType" => "video/mp4", + "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4" + } + } + + assert Transmogrifier.fix_url(object) == %{ + "url" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4" + } + end + + test "returns non-modified object" do + assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"} + end + end + + describe "get_obj_helper/2" do + test "returns nil when cannot normalize object" do + assert capture_log(fn -> + refute Transmogrifier.get_obj_helper("test-obj-id") + end) =~ "Unsupported URI scheme" + end + + @tag capture_log: true + test "returns {:ok, %Object{}} for success case" do + assert {:ok, %Object{}} = + Transmogrifier.get_obj_helper( + "https://mstdn.io/users/mayuutann/statuses/99568293732299394" + ) + end + end + + describe "fix_attachments/1" do + test "returns not modified object" do + data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) + assert Transmogrifier.fix_attachments(data) == data + end + + test "returns modified object when attachment is map" do + assert Transmogrifier.fix_attachments(%{ + "attachment" => %{ + "mediaType" => "video/mp4", + "url" => "https://peertube.moe/stat-480.mp4" + } + }) == %{ + "attachment" => [ + %{ + "mediaType" => "video/mp4", + "type" => "Document", + "url" => [ + %{ + "href" => "https://peertube.moe/stat-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + } + ] + } + end + + test "returns modified object when attachment is list" do + assert Transmogrifier.fix_attachments(%{ + "attachment" => [ + %{"mediaType" => "video/mp4", "url" => "https://pe.er/stat-480.mp4"}, + %{"mimeType" => "video/mp4", "href" => "https://pe.er/stat-480.mp4"} + ] + }) == %{ + "attachment" => [ + %{ + "mediaType" => "video/mp4", + "type" => "Document", + "url" => [ + %{ + "href" => "https://pe.er/stat-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + }, + %{ + "mediaType" => "video/mp4", + "type" => "Document", + "url" => [ + %{ + "href" => "https://pe.er/stat-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + } + ] + } + end + end + + describe "fix_emoji/1" do + test "returns not modified object when object not contains tags" do + data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) + assert Transmogrifier.fix_emoji(data) == data + end + + test "returns object with emoji when object contains list tags" do + assert Transmogrifier.fix_emoji(%{ + "tag" => [ + %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}}, + %{"type" => "Hashtag"} + ] + }) == %{ + "emoji" => %{"bib" => "/test"}, + "tag" => [ + %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"}, + %{"type" => "Hashtag"} + ] + } + end + + test "returns object with emoji when object contains map tag" do + assert Transmogrifier.fix_emoji(%{ + "tag" => %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}} + }) == %{ + "emoji" => %{"bib" => "/test"}, + "tag" => %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"} + } + end + end + + describe "set_replies/1" do + setup do: clear_config([:activitypub, :note_replies_output_limit], 2) + + test "returns unmodified object if activity doesn't have self-replies" do + data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) + assert Transmogrifier.set_replies(data) == data + end + + test "sets `replies` collection with a limited number of self-replies" do + [user, another_user] = insert_list(2, :user) + + {:ok, %{id: id1} = activity} = CommonAPI.post(user, %{status: "1"}) + + {:ok, %{id: id2} = self_reply1} = + CommonAPI.post(user, %{status: "self-reply 1", in_reply_to_status_id: id1}) + + {:ok, self_reply2} = + CommonAPI.post(user, %{status: "self-reply 2", in_reply_to_status_id: id1}) + + # Assuming to _not_ be present in `replies` due to :note_replies_output_limit is set to 2 + {:ok, _} = CommonAPI.post(user, %{status: "self-reply 3", in_reply_to_status_id: id1}) + + {:ok, _} = + CommonAPI.post(user, %{ + status: "self-reply to self-reply", + in_reply_to_status_id: id2 + }) + + {:ok, _} = + CommonAPI.post(another_user, %{ + status: "another user's reply", + in_reply_to_status_id: id1 + }) + + object = Object.normalize(activity) + replies_uris = Enum.map([self_reply1, self_reply2], fn a -> a.object.data["id"] end) + + assert %{"type" => "Collection", "items" => ^replies_uris} = + Transmogrifier.set_replies(object.data)["replies"] + end + end + + test "take_emoji_tags/1" do + user = insert(:user, %{emoji: %{"firefox" => "https://example.org/firefox.png"}}) + + assert Transmogrifier.take_emoji_tags(user) == [ + %{ + "icon" => %{"type" => "Image", "url" => "https://example.org/firefox.png"}, + "id" => "https://example.org/firefox.png", + "name" => ":firefox:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + } + ] + end +end diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs @@ -0,0 +1,548 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.UtilsTest do + use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + require Pleroma.Constants + + describe "fetch the latest Follow" do + test "fetches the latest Follow activity" do + %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity) + follower = User.get_cached_by_ap_id(activity.data["actor"]) + followed = User.get_cached_by_ap_id(activity.data["object"]) + + assert activity == Utils.fetch_latest_follow(follower, followed) + end + end + + describe "determine_explicit_mentions()" do + test "works with an object that has mentions" do + object = %{ + "tag" => [ + %{ + "type" => "Mention", + "href" => "https://example.com/~alyssa", + "name" => "Alyssa P. Hacker" + } + ] + } + + assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"] + end + + test "works with an object that does not have mentions" do + object = %{ + "tag" => [ + %{"type" => "Hashtag", "href" => "https://example.com/tag/2hu", "name" => "2hu"} + ] + } + + assert Utils.determine_explicit_mentions(object) == [] + end + + test "works with an object that has mentions and other tags" do + object = %{ + "tag" => [ + %{ + "type" => "Mention", + "href" => "https://example.com/~alyssa", + "name" => "Alyssa P. Hacker" + }, + %{"type" => "Hashtag", "href" => "https://example.com/tag/2hu", "name" => "2hu"} + ] + } + + assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"] + end + + test "works with an object that has no tags" do + object = %{} + + assert Utils.determine_explicit_mentions(object) == [] + end + + test "works with an object that has only IR tags" do + object = %{"tag" => ["2hu"]} + + assert Utils.determine_explicit_mentions(object) == [] + end + + test "works with an object has tags as map" do + object = %{ + "tag" => %{ + "type" => "Mention", + "href" => "https://example.com/~alyssa", + "name" => "Alyssa P. Hacker" + } + } + + assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"] + end + end + + describe "make_like_data" do + setup do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + [user: user, other_user: other_user, third_user: third_user] + end + + test "addresses actor's follower address if the activity is public", %{ + user: user, + other_user: other_user, + third_user: third_user + } do + expected_to = Enum.sort([user.ap_id, other_user.follower_address]) + expected_cc = Enum.sort(["https://www.w3.org/ns/activitystreams#Public", third_user.ap_id]) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: + "hey @#{other_user.nickname}, @#{third_user.nickname} how about beering together this weekend?" + }) + + %{"to" => to, "cc" => cc} = Utils.make_like_data(other_user, activity, nil) + assert Enum.sort(to) == expected_to + assert Enum.sort(cc) == expected_cc + end + + test "does not adress actor's follower address if the activity is not public", %{ + user: user, + other_user: other_user, + third_user: third_user + } do + expected_to = Enum.sort([user.ap_id]) + expected_cc = [third_user.ap_id] + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "@#{other_user.nickname} @#{third_user.nickname} bought a new swimsuit!", + visibility: "private" + }) + + %{"to" => to, "cc" => cc} = Utils.make_like_data(other_user, activity, nil) + assert Enum.sort(to) == expected_to + assert Enum.sort(cc) == expected_cc + end + end + + test "make_json_ld_header/0" do + assert Utils.make_json_ld_header() == %{ + "@context" => [ + "https://www.w3.org/ns/activitystreams", + "http://localhost:4001/schemas/litepub-0.1.jsonld", + %{ + "@language" => "und" + } + ] + } + end + + describe "get_existing_votes" do + test "fetches existing votes" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "How do I pronounce LaTeX?", + poll: %{ + options: ["laytekh", "lahtekh", "latex"], + expires_in: 20, + multiple: true + } + }) + + object = Object.normalize(activity) + {:ok, votes, object} = CommonAPI.vote(other_user, object, [0, 1]) + assert Enum.sort(Utils.get_existing_votes(other_user.ap_id, object)) == Enum.sort(votes) + end + + test "fetches only Create activities" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Are we living in a society?", + poll: %{ + options: ["yes", "no"], + expires_in: 20 + } + }) + + object = Object.normalize(activity) + {:ok, [vote], object} = CommonAPI.vote(other_user, object, [0]) + {:ok, _activity} = CommonAPI.favorite(user, activity.id) + [fetched_vote] = Utils.get_existing_votes(other_user.ap_id, object) + assert fetched_vote.id == vote.id + end + end + + describe "update_follow_state_for_all/2" do + test "updates the state of all Follow activities with the same actor and object" do + user = insert(:user, locked: true) + follower = insert(:user) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) + + data = + follow_activity_two.data + |> Map.put("state", "accept") + + cng = Ecto.Changeset.change(follow_activity_two, data: data) + + {:ok, follow_activity_two} = Repo.update(cng) + + {:ok, follow_activity_two} = + Utils.update_follow_state_for_all(follow_activity_two, "accept") + + assert refresh_record(follow_activity).data["state"] == "accept" + assert refresh_record(follow_activity_two).data["state"] == "accept" + end + end + + describe "update_follow_state/2" do + test "updates the state of the given follow activity" do + user = insert(:user, locked: true) + follower = insert(:user) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) + + data = + follow_activity_two.data + |> Map.put("state", "accept") + + cng = Ecto.Changeset.change(follow_activity_two, data: data) + + {:ok, follow_activity_two} = Repo.update(cng) + + {:ok, follow_activity_two} = Utils.update_follow_state(follow_activity_two, "reject") + + assert refresh_record(follow_activity).data["state"] == "pending" + assert refresh_record(follow_activity_two).data["state"] == "reject" + end + end + + describe "update_element_in_object/3" do + test "updates likes" do + user = insert(:user) + activity = insert(:note_activity) + object = Object.normalize(activity) + + assert {:ok, updated_object} = + Utils.update_element_in_object( + "like", + [user.ap_id], + object + ) + + assert updated_object.data["likes"] == [user.ap_id] + assert updated_object.data["like_count"] == 1 + end + end + + describe "add_like_to_object/2" do + test "add actor to likes" do + user = insert(:user) + user2 = insert(:user) + object = insert(:note) + + assert {:ok, updated_object} = + Utils.add_like_to_object( + %Activity{data: %{"actor" => user.ap_id}}, + object + ) + + assert updated_object.data["likes"] == [user.ap_id] + assert updated_object.data["like_count"] == 1 + + assert {:ok, updated_object2} = + Utils.add_like_to_object( + %Activity{data: %{"actor" => user2.ap_id}}, + updated_object + ) + + assert updated_object2.data["likes"] == [user2.ap_id, user.ap_id] + assert updated_object2.data["like_count"] == 2 + end + end + + describe "remove_like_from_object/2" do + test "removes ap_id from likes" do + user = insert(:user) + user2 = insert(:user) + object = insert(:note, data: %{"likes" => [user.ap_id, user2.ap_id], "like_count" => 2}) + + assert {:ok, updated_object} = + Utils.remove_like_from_object( + %Activity{data: %{"actor" => user.ap_id}}, + object + ) + + assert updated_object.data["likes"] == [user2.ap_id] + assert updated_object.data["like_count"] == 1 + end + end + + describe "get_existing_like/2" do + test "fetches existing like" do + note_activity = insert(:note_activity) + assert object = Object.normalize(note_activity) + + user = insert(:user) + refute Utils.get_existing_like(user.ap_id, object) + {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id) + + assert ^like_activity = Utils.get_existing_like(user.ap_id, object) + end + end + + describe "get_get_existing_announce/2" do + test "returns nil if announce not found" do + actor = insert(:user) + refute Utils.get_existing_announce(actor.ap_id, %{data: %{"id" => "test"}}) + end + + test "fetches existing announce" do + note_activity = insert(:note_activity) + assert object = Object.normalize(note_activity) + actor = insert(:user) + + {:ok, announce} = CommonAPI.repeat(note_activity.id, actor) + assert Utils.get_existing_announce(actor.ap_id, object) == announce + end + end + + describe "fetch_latest_block/2" do + test "fetches last block activities" do + user1 = insert(:user) + user2 = insert(:user) + + assert {:ok, %Activity{} = _} = CommonAPI.block(user1, user2) + assert {:ok, %Activity{} = _} = CommonAPI.block(user1, user2) + assert {:ok, %Activity{} = activity} = CommonAPI.block(user1, user2) + + assert Utils.fetch_latest_block(user1, user2) == activity + end + end + + describe "recipient_in_message/3" do + test "returns true when recipient in `to`" do + recipient = insert(:user) + actor = insert(:user) + assert Utils.recipient_in_message(recipient, actor, %{"to" => recipient.ap_id}) + + assert Utils.recipient_in_message( + recipient, + actor, + %{"to" => [recipient.ap_id], "cc" => ""} + ) + end + + test "returns true when recipient in `cc`" do + recipient = insert(:user) + actor = insert(:user) + assert Utils.recipient_in_message(recipient, actor, %{"cc" => recipient.ap_id}) + + assert Utils.recipient_in_message( + recipient, + actor, + %{"cc" => [recipient.ap_id], "to" => ""} + ) + end + + test "returns true when recipient in `bto`" do + recipient = insert(:user) + actor = insert(:user) + assert Utils.recipient_in_message(recipient, actor, %{"bto" => recipient.ap_id}) + + assert Utils.recipient_in_message( + recipient, + actor, + %{"bcc" => "", "bto" => [recipient.ap_id]} + ) + end + + test "returns true when recipient in `bcc`" do + recipient = insert(:user) + actor = insert(:user) + assert Utils.recipient_in_message(recipient, actor, %{"bcc" => recipient.ap_id}) + + assert Utils.recipient_in_message( + recipient, + actor, + %{"bto" => "", "bcc" => [recipient.ap_id]} + ) + end + + test "returns true when message without addresses fields" do + recipient = insert(:user) + actor = insert(:user) + assert Utils.recipient_in_message(recipient, actor, %{"bccc" => recipient.ap_id}) + + assert Utils.recipient_in_message( + recipient, + actor, + %{"btod" => "", "bccc" => [recipient.ap_id]} + ) + end + + test "returns false" do + recipient = insert(:user) + actor = insert(:user) + refute Utils.recipient_in_message(recipient, actor, %{"to" => "ap_id"}) + end + end + + describe "lazy_put_activity_defaults/2" do + test "returns map with id and published data" do + note_activity = insert(:note_activity) + object = Object.normalize(note_activity) + 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 + + 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) + end + + test "returns activity data with object" do + note_activity = insert(:note_activity) + object = Object.normalize(note_activity) + + res = + Utils.lazy_put_activity_defaults(%{ + "context" => object.data["id"], + "object" => %{} + }) + + 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 + + describe "make_flag_data" do + test "returns empty map when params is invalid" do + assert Utils.make_flag_data(%{}, %{}) == %{} + end + + test "returns map with Flag object" do + reporter = insert(:user) + target_account = insert(:user) + {:ok, activity} = CommonAPI.post(target_account, %{status: "foobar"}) + context = Utils.generate_context_id() + content = "foobar" + + target_ap_id = target_account.ap_id + activity_ap_id = activity.data["id"] + + res = + Utils.make_flag_data( + %{ + actor: reporter, + context: context, + account: target_account, + statuses: [%{"id" => activity.data["id"]}], + content: content + }, + %{} + ) + + note_obj = %{ + "type" => "Note", + "id" => activity_ap_id, + "content" => content, + "published" => activity.object.data["published"], + "actor" => + AccountView.render("show.json", %{user: target_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 + test "adds actor to announcement" do + user = insert(:user) + object = insert(:note) + + activity = + insert(:note_activity, + data: %{ + "actor" => user.ap_id, + "cc" => [Pleroma.Constants.as_public()] + } + ) + + assert {:ok, updated_object} = Utils.add_announce_to_object(activity, object) + assert updated_object.data["announcements"] == [user.ap_id] + assert updated_object.data["announcement_count"] == 1 + end + end + + describe "remove_announce_from_object/2" do + test "removes actor from announcements" do + user = insert(:user) + user2 = insert(:user) + + object = + insert(:note, + data: %{"announcements" => [user.ap_id, user2.ap_id], "announcement_count" => 2} + ) + + activity = insert(:note_activity, data: %{"actor" => user.ap_id}) + + assert {:ok, updated_object} = Utils.remove_announce_from_object(activity, object) + assert updated_object.data["announcements"] == [user2.ap_id] + assert updated_object.data["announcement_count"] == 1 + end + end + + describe "get_cached_emoji_reactions/1" do + test "returns the data or an emtpy 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"]]] + + object = insert(:note, data: %{"reactions" => %{}}) + assert Utils.get_cached_emoji_reactions(object) == [] + 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 @@ -0,0 +1,84 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectViewTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ObjectView + alias Pleroma.Web.CommonAPI + + test "renders a note object" do + note = insert(:note) + + result = ObjectView.render("object.json", %{object: note}) + + assert result["id"] == note.data["id"] + assert result["to"] == note.data["to"] + assert result["content"] == note.data["content"] + assert result["type"] == "Note" + assert result["@context"] + end + + test "renders a note activity" do + note = insert(:note_activity) + object = Object.normalize(note) + + result = ObjectView.render("object.json", %{object: note}) + + assert result["id"] == note.data["id"] + assert result["to"] == note.data["to"] + assert result["object"]["type"] == "Note" + assert result["object"]["content"] == object.data["content"] + assert result["type"] == "Create" + assert result["@context"] + end + + describe "note activity's `replies` collection rendering" do + setup do: clear_config([:activitypub, :note_replies_output_limit], 5) + + test "renders `replies` collection for a note activity" do + user = insert(:user) + activity = insert(:note_activity, user: user) + + {:ok, self_reply1} = + CommonAPI.post(user, %{status: "self-reply 1", in_reply_to_status_id: activity.id}) + + replies_uris = [self_reply1.object.data["id"]] + result = ObjectView.render("object.json", %{object: refresh_record(activity)}) + + assert %{"type" => "Collection", "items" => ^replies_uris} = + get_in(result, ["object", "replies"]) + end + end + + test "renders a like activity" do + note = insert(:note_activity) + object = Object.normalize(note) + user = insert(:user) + + {:ok, like_activity} = CommonAPI.favorite(user, note.id) + + result = ObjectView.render("object.json", %{object: like_activity}) + + assert result["id"] == like_activity.data["id"] + assert result["object"] == object.data["id"] + assert result["type"] == "Like" + end + + test "renders an announce activity" do + note = insert(:note_activity) + object = Object.normalize(note) + user = insert(:user) + + {:ok, announce_activity} = CommonAPI.repeat(note.id, user) + + result = ObjectView.render("object.json", %{object: announce_activity}) + + assert result["id"] == announce_activity.data["id"] + assert result["object"] == object.data["id"] + assert result["type"] == "Announce" + 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 @@ -0,0 +1,180 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.UserViewTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.User + alias Pleroma.Web.ActivityPub.UserView + alias Pleroma.Web.CommonAPI + + 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}) + + assert result["id"] == user.ap_id + assert result["preferredUsername"] == user.nickname + + assert String.contains?(result["publicKey"]["publicKeyPem"], "BEGIN PUBLIC KEY") + end + + test "Renders profile fields" do + fields = [ + %{"name" => "foo", "value" => "bar"} + ] + + {:ok, user} = + insert(:user) + |> User.update_changeset(%{fields: fields}) + |> User.update_and_set_cache() + + assert %{ + "attachment" => [%{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}] + } = UserView.render("user.json", %{user: user}) + end + + test "Renders with emoji tags" do + user = insert(:user, emoji: %{"bib" => "/test"}) + + assert %{ + "tag" => [ + %{ + "icon" => %{"type" => "Image", "url" => "/test"}, + "id" => "/test", + "name" => ":bib:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + } + ] + } = UserView.render("user.json", %{user: user}) + end + + 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"] + refute result["image"] + + user = + insert(:user, + avatar: %{"url" => [%{"href" => "https://someurl"}]}, + 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" + end + + test "renders an invisible user with the invisible property set to true" do + user = insert(:user, invisible: true) + + assert %{"invisible" => true} = UserView.render("service.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}) + + assert result["id"] == user.ap_id + + %{ + "sharedInbox" => _, + "oauthAuthorizationEndpoint" => _, + "oauthRegistrationEndpoint" => _, + "oauthTokenEndpoint" => _ + } = result["endpoints"] + end + + 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}) + + assert result["id"] == user.ap_id + assert result["endpoints"] == %{} + end + + 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}) + + refute result["endpoints"]["oauthAuthorizationEndpoint"] + refute result["endpoints"]["oauthRegistrationEndpoint"] + refute result["endpoints"]["oauthTokenEndpoint"] + end + end + + describe "followers" do + test "sets totalItems to zero when followers are hidden" do + user = insert(:user) + other_user = insert(:user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user}) + user = Map.merge(user, %{hide_followers_count: true, hide_followers: true}) + refute UserView.render("followers.json", %{user: user}) |> Map.has_key?("totalItems") + end + + test "sets correct totalItems when followers are hidden but the follower counter is not" do + user = insert(:user) + other_user = insert(:user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user}) + user = Map.merge(user, %{hide_followers_count: false, hide_followers: true}) + assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user}) + end + end + + describe "following" do + test "sets totalItems to zero when follows are hidden" do + user = insert(:user) + other_user = insert(:user) + {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) + assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) + user = Map.merge(user, %{hide_follows_count: true, hide_follows: true}) + assert %{"totalItems" => 0} = UserView.render("following.json", %{user: user}) + end + + test "sets correct totalItems when follows are hidden but the follow counter is not" do + user = insert(:user) + other_user = insert(:user) + {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) + assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) + user = Map.merge(user, %{hide_follows_count: false, hide_follows: true}) + assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) + end + end + + describe "acceptsChatMessages" do + test "it returns this value if it is set" do + true_user = insert(:user, accepts_chat_messages: true) + false_user = insert(:user, accepts_chat_messages: false) + nil_user = insert(:user, accepts_chat_messages: nil) + + assert %{"capabilities" => %{"acceptsChatMessages" => true}} = + UserView.render("user.json", user: true_user) + + assert %{"capabilities" => %{"acceptsChatMessages" => false}} = + UserView.render("user.json", user: false_user) + + refute Map.has_key?( + UserView.render("user.json", user: nil_user)["capabilities"], + "acceptsChatMessages" + ) + end + end +end diff --git a/test/pleroma/web/activity_pub/visibility_test.exs b/test/pleroma/web/activity_pub/visibility_test.exs @@ -0,0 +1,230 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.VisibilityTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.CommonAPI + import Pleroma.Factory + + setup do + user = insert(:user) + mentioned = insert(:user) + following = insert(:user) + unrelated = insert(:user) + {:ok, following} = Pleroma.User.follow(following, user) + {:ok, list} = Pleroma.List.create("foo", user) + + Pleroma.List.follow(list, unrelated) + + {:ok, public} = + CommonAPI.post(user, %{status: "@#{mentioned.nickname}", visibility: "public"}) + + {:ok, private} = + CommonAPI.post(user, %{status: "@#{mentioned.nickname}", visibility: "private"}) + + {:ok, direct} = + CommonAPI.post(user, %{status: "@#{mentioned.nickname}", visibility: "direct"}) + + {:ok, unlisted} = + CommonAPI.post(user, %{status: "@#{mentioned.nickname}", visibility: "unlisted"}) + + {:ok, list} = + CommonAPI.post(user, %{ + status: "@#{mentioned.nickname}", + visibility: "list:#{list.id}" + }) + + %{ + public: public, + private: private, + direct: direct, + unlisted: unlisted, + user: user, + mentioned: mentioned, + following: following, + unrelated: unrelated, + list: list + } + end + + test "is_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) + end + + test "is_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) + end + + test "is_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) + end + + test "is_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) + end + + test "visible_for_user?", %{ + public: public, + private: private, + direct: direct, + unlisted: unlisted, + user: user, + mentioned: mentioned, + following: following, + unrelated: unrelated, + list: list + } do + # All visible to author + + assert Visibility.visible_for_user?(public, user) + assert Visibility.visible_for_user?(private, user) + assert Visibility.visible_for_user?(unlisted, user) + assert Visibility.visible_for_user?(direct, user) + assert Visibility.visible_for_user?(list, user) + + # All visible to a mentioned user + + assert Visibility.visible_for_user?(public, mentioned) + assert Visibility.visible_for_user?(private, mentioned) + assert Visibility.visible_for_user?(unlisted, mentioned) + assert Visibility.visible_for_user?(direct, mentioned) + assert Visibility.visible_for_user?(list, mentioned) + + # DM not visible for just follower + + assert Visibility.visible_for_user?(public, following) + assert Visibility.visible_for_user?(private, following) + assert Visibility.visible_for_user?(unlisted, following) + refute Visibility.visible_for_user?(direct, following) + refute Visibility.visible_for_user?(list, following) + + # Public and unlisted visible for unrelated user + + assert Visibility.visible_for_user?(public, unrelated) + assert Visibility.visible_for_user?(unlisted, unrelated) + refute Visibility.visible_for_user?(private, unrelated) + refute Visibility.visible_for_user?(direct, unrelated) + + # Visible for a list member + assert Visibility.visible_for_user?(list, unrelated) + end + + test "doesn't die when the user doesn't exist", + %{ + direct: direct, + user: user + } do + Repo.delete(user) + Cachex.clear(:user_cache) + refute Visibility.is_private?(direct) + end + + test "get_visibility", %{ + public: public, + private: private, + direct: direct, + unlisted: unlisted, + list: list + } do + assert Visibility.get_visibility(public) == "public" + assert Visibility.get_visibility(private) == "private" + assert Visibility.get_visibility(direct) == "direct" + assert Visibility.get_visibility(unlisted) == "unlisted" + assert Visibility.get_visibility(list) == "list" + end + + test "get_visibility with directMessage flag" do + assert Visibility.get_visibility(%{data: %{"directMessage" => true}}) == "direct" + end + + test "get_visibility with listMessage flag" do + assert Visibility.get_visibility(%{data: %{"listMessage" => ""}}) == "list" + end + + describe "entire_thread_visible_for_user?/2" do + test "returns false if not found activity", %{user: user} do + refute Visibility.entire_thread_visible_for_user?(%Activity{}, user) + end + + test "returns true if activity hasn't 'Create' type", %{user: user} do + activity = insert(:like_activity) + assert Visibility.entire_thread_visible_for_user?(activity, user) + end + + test "returns false when invalid recipients", %{user: user} do + author = insert(:user) + + activity = + insert(:note_activity, + note: + insert(:note, + user: author, + data: %{"to" => ["test-user"]} + ) + ) + + refute Visibility.entire_thread_visible_for_user?(activity, user) + end + + test "returns true if user following to author" do + author = insert(:user) + user = insert(:user) + Pleroma.User.follow(user, author) + + activity = + insert(:note_activity, + note: + insert(:note, + user: author, + data: %{"to" => [user.ap_id]} + ) + ) + + assert Visibility.entire_thread_visible_for_user?(activity, user) + end + end +end 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 @@ -0,0 +1,2034 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do + use Pleroma.Web.ConnCase + use Oban.Testing, repo: Pleroma.Repo + + import ExUnit.CaptureLog + import Mock + import Pleroma.Factory + import Swoosh.TestAssertions + + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.HTML + alias Pleroma.MFA + alias Pleroma.ModerationLog + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web + alias Pleroma.Web.ActivityPub.Relay + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MediaProxy + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + + :ok + end + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + test "with valid `admin_token` query parameter, skips OAuth scopes check" do + clear_config([:admin_token], "password123") + + user = insert(:user) + + conn = get(build_conn(), "/api/pleroma/admin/users/#{user.nickname}?admin_token=password123") + + assert json_response(conn, 200) + end + + describe "with [:auth, :enforce_oauth_admin_scope_usage]," do + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], true) + + 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(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(conn, :forbidden) + end + end + end + + describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) + + test "GET /api/pleroma/admin/users/:nickname requires " <> + "read:accounts or 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"]) + good_token4 = insert(:oauth_token, user: admin, scopes: ["read:accounts"]) + good_token5 = insert(:oauth_token, user: admin, scopes: ["read"]) + + good_tokens = [good_token1, good_token2, good_token3, good_token4, good_token5] + + bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts:partial"]) + bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"]) + bad_token3 = nil + + for good_token <- good_tokens do + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, good_token) + |> get(url) + + assert json_response(conn, 200) + end + + for good_token <- good_tokens 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(conn, :forbidden) + end + end + end + + describe "DELETE /api/pleroma/admin/users" do + test "single user", %{admin: admin, conn: conn} do + clear_config([:instance, :federating], true) + + user = + insert(:user, + avatar: %{"url" => [%{"href" => "https://someurl"}]}, + banner: %{"url" => [%{"href" => "https://somebanner"}]}, + bio: "Hello world!", + name: "A guy" + ) + + # Create some activities to check they got deleted later + follower = insert(:user) + {:ok, _} = CommonAPI.post(user, %{status: "test"}) + {:ok, _, _, _} = CommonAPI.follow(user, follower) + {:ok, _, _, _} = CommonAPI.follow(follower, user) + user = Repo.get(User, user.id) + assert user.note_count == 1 + assert user.follower_count == 1 + assert user.following_count == 1 + refute user.deactivated + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end, + perform: fn _, _ -> nil end do + conn = + conn + |> put_req_header("accept", "application/json") + |> delete("/api/pleroma/admin/users?nickname=#{user.nickname}") + + ObanHelpers.perform_all() + + assert User.get_by_nickname(user.nickname).deactivated + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deleted users: @#{user.nickname}" + + assert json_response(conn, 200) == [user.nickname] + + user = Repo.get(User, user.id) + assert user.deactivated + + assert user.avatar == %{} + assert user.banner == %{} + assert user.note_count == 0 + assert user.follower_count == 0 + assert user.following_count == 0 + assert user.bio == "" + assert user.name == nil + + assert called(Pleroma.Web.Federator.publish(:_)) + end + end + + test "multiple users", %{admin: admin, conn: conn} do + user_one = insert(:user) + user_two = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/json") + |> delete("/api/pleroma/admin/users", %{ + nicknames: [user_one.nickname, user_two.nickname] + }) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deleted users: @#{user_one.nickname}, @#{user_two.nickname}" + + response = json_response(conn, 200) + assert response -- [user_one.nickname, user_two.nickname] == [] + end + end + + describe "/api/pleroma/admin/users" do + test "Create", %{conn: conn} do + conn = + conn + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users", %{ + "users" => [ + %{ + "nickname" => "lain", + "email" => "lain@example.org", + "password" => "test" + }, + %{ + "nickname" => "lain2", + "email" => "lain2@example.org", + "password" => "test" + } + ] + }) + + response = json_response(conn, 200) |> Enum.map(&Map.get(&1, "type")) + assert response == ["success", "success"] + + log_entry = Repo.one(ModerationLog) + + assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == [] + end + + test "Cannot create user with existing email", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users", %{ + "users" => [ + %{ + "nickname" => "lain", + "email" => user.email, + "password" => "test" + } + ] + }) + + assert json_response(conn, 409) == [ + %{ + "code" => 409, + "data" => %{ + "email" => user.email, + "nickname" => "lain" + }, + "error" => "email has already been taken", + "type" => "error" + } + ] + end + + test "Cannot create user with existing nickname", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users", %{ + "users" => [ + %{ + "nickname" => user.nickname, + "email" => "someuser@plerama.social", + "password" => "test" + } + ] + }) + + assert json_response(conn, 409) == [ + %{ + "code" => 409, + "data" => %{ + "email" => "someuser@plerama.social", + "nickname" => user.nickname + }, + "error" => "nickname has already been taken", + "type" => "error" + } + ] + end + + test "Multiple user creation works in transaction", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users", %{ + "users" => [ + %{ + "nickname" => "newuser", + "email" => "newuser@pleroma.social", + "password" => "test" + }, + %{ + "nickname" => "lain", + "email" => user.email, + "password" => "test" + } + ] + }) + + assert json_response(conn, 409) == [ + %{ + "code" => 409, + "data" => %{ + "email" => user.email, + "nickname" => "lain" + }, + "error" => "email has already been taken", + "type" => "error" + }, + %{ + "code" => 409, + "data" => %{ + "email" => "newuser@pleroma.social", + "nickname" => "newuser" + }, + "error" => "", + "type" => "error" + } + ] + + assert User.get_by_nickname("newuser") === nil + end + end + + describe "/api/pleroma/admin/users/:nickname" do + test "Show", %{conn: conn} do + user = insert(:user) + + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}") + + expected = %{ + "deactivated" => false, + "id" => to_string(user.id), + "local" => true, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + + assert expected == json_response(conn, 200) + end + + test "when the user doesn't exist", %{conn: conn} do + user = build(:user) + + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}") + + assert %{"error" => "Not found"} == json_response(conn, 404) + end + end + + describe "/api/pleroma/admin/users/follow" do + test "allows to force-follow another user", %{admin: admin, conn: conn} do + user = insert(:user) + follower = insert(:user) + + conn + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users/follow", %{ + "follower" => follower.nickname, + "followed" => user.nickname + }) + + user = User.get_cached_by_id(user.id) + follower = User.get_cached_by_id(follower.id) + + assert User.following?(follower, user) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} made @#{follower.nickname} follow @#{user.nickname}" + end + end + + describe "/api/pleroma/admin/users/unfollow" do + test "allows to force-unfollow another user", %{admin: admin, conn: conn} do + user = insert(:user) + follower = insert(:user) + + User.follow(follower, user) + + conn + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users/unfollow", %{ + "follower" => follower.nickname, + "followed" => user.nickname + }) + + user = User.get_cached_by_id(user.id) + follower = User.get_cached_by_id(follower.id) + + refute User.following?(follower, user) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} made @#{follower.nickname} unfollow @#{user.nickname}" + end + end + + describe "PUT /api/pleroma/admin/users/tag" do + setup %{conn: conn} do + 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 + + test "it appends specified tags to users with specified nicknames", %{ + conn: conn, + admin: admin, + user1: user1, + user2: user2 + } do + 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"] + + log_entry = Repo.one(ModerationLog) + + users = + [user1.nickname, user2.nickname] + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags = ["foo", "bar"] |> Enum.join(", ") + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} added tags: #{tags} to users: #{users}" + end + + test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do + assert empty_json_response(conn) + assert User.get_cached_by_id(user3.id).tags == ["unchanged"] + end + end + + describe "DELETE /api/pleroma/admin/users/tag" do + setup %{conn: conn} do + 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 + + test "it removes specified tags from users with specified nicknames", %{ + conn: conn, + admin: admin, + user1: user1, + user2: user2 + } do + assert empty_json_response(conn) + assert User.get_cached_by_id(user1.id).tags == [] + assert User.get_cached_by_id(user2.id).tags == ["y"] + + log_entry = Repo.one(ModerationLog) + + users = + [user1.nickname, user2.nickname] + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags = ["x", "z"] |> Enum.join(", ") + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} removed tags: #{tags} from users: #{users}" + end + + test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do + assert empty_json_response(conn) + assert User.get_cached_by_id(user3.id).tags == ["unchanged"] + end + end + + describe "/api/pleroma/admin/users/:nickname/permission_group" do + test "GET is giving user_info", %{admin: admin, conn: conn} do + conn = + conn + |> put_req_header("accept", "application/json") + |> get("/api/pleroma/admin/users/#{admin.nickname}/permission_group/") + + assert json_response(conn, 200) == %{ + "is_admin" => true, + "is_moderator" => false + } + end + + test "/:right POST, can add to a permission group", %{admin: admin, conn: conn} do + user = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin") + + assert json_response(conn, 200) == %{ + "is_admin" => true + } + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} made @#{user.nickname} admin" + end + + test "/:right POST, can add to a permission group (multiple)", %{admin: admin, conn: conn} do + user_one = insert(:user) + user_two = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users/permission_group/admin", %{ + nicknames: [user_one.nickname, user_two.nickname] + }) + + assert json_response(conn, 200) == %{"is_admin" => true} + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin" + end + + test "/:right DELETE, can remove from a permission group", %{admin: admin, conn: conn} do + user = insert(:user, is_admin: true) + + conn = + conn + |> put_req_header("accept", "application/json") + |> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin") + + assert json_response(conn, 200) == %{"is_admin" => false} + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} revoked admin role from @#{user.nickname}" + end + + test "/:right DELETE, can remove from a permission group (multiple)", %{ + admin: admin, + conn: conn + } do + user_one = insert(:user, is_admin: true) + user_two = insert(:user, is_admin: true) + + conn = + conn + |> put_req_header("accept", "application/json") + |> delete("/api/pleroma/admin/users/permission_group/admin", %{ + nicknames: [user_one.nickname, user_two.nickname] + }) + + assert json_response(conn, 200) == %{"is_admin" => false} + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} revoked admin role from @#{user_one.nickname}, @#{ + user_two.nickname + }" + end + end + + test "/api/pleroma/admin/users/:nickname/password_reset", %{conn: conn} do + 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) + + assert Regex.match?(~r/(http:\/\/|https:\/\/)/, resp["link"]) + end + + describe "GET /api/pleroma/admin/users" do + test "renders users array for the first page", %{conn: conn, admin: admin} do + user = insert(:user, local: false, tags: ["foo", "bar"]) + user2 = insert(:user, approval_pending: true, registration_reason: "I'm a chill dude") + + conn = get(conn, "/api/pleroma/admin/users?page=1") + + users = + [ + %{ + "deactivated" => admin.deactivated, + "id" => admin.id, + "nickname" => admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(admin) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(admin.name || admin.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => admin.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + }, + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => false, + "tags" => ["foo", "bar"], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + }, + %{ + "deactivated" => user2.deactivated, + "id" => user2.id, + "nickname" => user2.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user2) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user2.name || user2.nickname), + "confirmation_pending" => false, + "approval_pending" => true, + "url" => user2.ap_id, + "registration_reason" => "I'm a chill dude", + "actor_type" => "Person" + } + ] + |> Enum.sort_by(& &1["nickname"]) + + assert json_response(conn, 200) == %{ + "count" => 3, + "page_size" => 50, + "users" => users + } + end + + test "pagination works correctly with service users", %{conn: conn} do + service1 = User.get_or_create_service_actor_by_ap_id(Web.base_url() <> "/meido", "meido") + + insert_list(25, :user) + + assert %{"count" => 26, "page_size" => 10, "users" => users1} = + conn + |> get("/api/pleroma/admin/users?page=1&filters=", %{page_size: "10"}) + |> json_response(200) + + assert Enum.count(users1) == 10 + assert service1 not in users1 + + assert %{"count" => 26, "page_size" => 10, "users" => users2} = + conn + |> get("/api/pleroma/admin/users?page=2&filters=", %{page_size: "10"}) + |> json_response(200) + + assert Enum.count(users2) == 10 + assert service1 not in users2 + + assert %{"count" => 26, "page_size" => 10, "users" => users3} = + conn + |> get("/api/pleroma/admin/users?page=3&filters=", %{page_size: "10"}) + |> json_response(200) + + assert Enum.count(users3) == 6 + assert service1 not in users3 + end + + test "renders empty array for the second page", %{conn: conn} do + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?page=2") + + assert json_response(conn, 200) == %{ + "count" => 2, + "page_size" => 50, + "users" => [] + } + end + + test "regular search", %{conn: conn} do + user = insert(:user, nickname: "bob") + + conn = get(conn, "/api/pleroma/admin/users?query=bo") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "search by domain", %{conn: conn} do + user = insert(:user, nickname: "nickname@domain.com") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?query=domain.com") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "search by full nickname", %{conn: conn} do + user = insert(:user, nickname: "nickname@domain.com") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?query=nickname@domain.com") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "search by display name", %{conn: conn} do + user = insert(:user, name: "Display name") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?name=display") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "search by email", %{conn: conn} do + user = insert(:user, email: "email@example.com") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?email=email@example.com") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "regular search with page size", %{conn: conn} do + user = insert(:user, nickname: "aalice") + user2 = insert(:user, nickname: "alice") + + conn1 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=1") + + assert json_response(conn1, 200) == %{ + "count" => 2, + "page_size" => 1, + "users" => [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + + conn2 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=2") + + assert json_response(conn2, 200) == %{ + "count" => 2, + "page_size" => 1, + "users" => [ + %{ + "deactivated" => user2.deactivated, + "id" => user2.id, + "nickname" => user2.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user2) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user2.name || user2.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user2.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "only local users" do + admin = insert(:user, is_admin: true, nickname: "john") + token = insert(:oauth_admin_token, user: admin) + user = insert(:user, nickname: "bob") + + insert(:user, nickname: "bobb", local: false) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + |> get("/api/pleroma/admin/users?query=bo&filters=local") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "only local users with no query", %{conn: conn, admin: old_admin} do + admin = insert(:user, is_admin: true, nickname: "john") + user = insert(:user, nickname: "bob") + + insert(:user, nickname: "bobb", local: false) + + conn = get(conn, "/api/pleroma/admin/users?filters=local") + + users = + [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + }, + %{ + "deactivated" => admin.deactivated, + "id" => admin.id, + "nickname" => admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(admin) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(admin.name || admin.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => admin.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + }, + %{ + "deactivated" => false, + "id" => old_admin.id, + "local" => true, + "nickname" => old_admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "tags" => [], + "avatar" => User.avatar_url(old_admin) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => old_admin.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + |> Enum.sort_by(& &1["nickname"]) + + assert json_response(conn, 200) == %{ + "count" => 3, + "page_size" => 50, + "users" => users + } + end + + test "only unapproved users", %{conn: conn} do + user = + insert(:user, + nickname: "sadboy", + approval_pending: true, + registration_reason: "Plz let me in!" + ) + + insert(:user, nickname: "happyboy", approval_pending: false) + + conn = get(conn, "/api/pleroma/admin/users?filters=need_approval") + + users = + [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => true, + "url" => user.ap_id, + "registration_reason" => "Plz let me in!", + "actor_type" => "Person" + } + ] + |> Enum.sort_by(& &1["nickname"]) + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => users + } + end + + test "load only admins", %{conn: conn, admin: admin} do + second_admin = insert(:user, is_admin: true) + insert(:user) + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?filters=is_admin") + + users = + [ + %{ + "deactivated" => false, + "id" => admin.id, + "nickname" => admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => admin.local, + "tags" => [], + "avatar" => User.avatar_url(admin) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(admin.name || admin.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => admin.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + }, + %{ + "deactivated" => false, + "id" => second_admin.id, + "nickname" => second_admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => second_admin.local, + "tags" => [], + "avatar" => User.avatar_url(second_admin) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => second_admin.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + |> Enum.sort_by(& &1["nickname"]) + + assert json_response(conn, 200) == %{ + "count" => 2, + "page_size" => 50, + "users" => users + } + end + + test "load only moderators", %{conn: conn} do + moderator = insert(:user, is_moderator: true) + insert(:user) + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?filters=is_moderator") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => false, + "id" => moderator.id, + "nickname" => moderator.nickname, + "roles" => %{"admin" => false, "moderator" => true}, + "local" => moderator.local, + "tags" => [], + "avatar" => User.avatar_url(moderator) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(moderator.name || moderator.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => moderator.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "load users with tags list", %{conn: conn} do + user1 = insert(:user, tags: ["first"]) + user2 = insert(:user, tags: ["second"]) + insert(:user) + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?tags[]=first&tags[]=second") + + users = + [ + %{ + "deactivated" => false, + "id" => user1.id, + "nickname" => user1.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => user1.local, + "tags" => ["first"], + "avatar" => User.avatar_url(user1) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user1.name || user1.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user1.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + }, + %{ + "deactivated" => false, + "id" => user2.id, + "nickname" => user2.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => user2.local, + "tags" => ["second"], + "avatar" => User.avatar_url(user2) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user2.name || user2.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user2.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + |> Enum.sort_by(& &1["nickname"]) + + assert json_response(conn, 200) == %{ + "count" => 2, + "page_size" => 50, + "users" => users + } + end + + test "`active` filters out users pending approval", %{token: token} do + insert(:user, approval_pending: true) + %{id: user_id} = insert(:user, approval_pending: false) + %{id: admin_id} = token.user + + conn = + build_conn() + |> assign(:user, token.user) + |> assign(:token, token) + |> get("/api/pleroma/admin/users?filters=active") + + assert %{ + "count" => 2, + "page_size" => 50, + "users" => [ + %{"id" => ^admin_id}, + %{"id" => ^user_id} + ] + } = json_response(conn, 200) + end + + test "it works with multiple filters" do + admin = insert(:user, nickname: "john", is_admin: true) + token = insert(:oauth_admin_token, user: admin) + user = insert(:user, nickname: "bob", local: false, deactivated: true) + + insert(:user, nickname: "ken", local: true, deactivated: true) + insert(:user, nickname: "bobb", local: false, deactivated: false) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + |> get("/api/pleroma/admin/users?filters=deactivated,external") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => user.local, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "it omits relay user", %{admin: admin, conn: conn} do + assert %User{} = Relay.get_actor() + + conn = get(conn, "/api/pleroma/admin/users") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => admin.deactivated, + "id" => admin.id, + "nickname" => admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(admin) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(admin.name || admin.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => admin.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + end + + test "PATCH /api/pleroma/admin/users/activate", %{admin: admin, conn: conn} do + user_one = insert(:user, deactivated: true) + user_two = insert(:user, deactivated: true) + + conn = + patch( + conn, + "/api/pleroma/admin/users/activate", + %{nicknames: [user_one.nickname, user_two.nickname]} + ) + + response = json_response(conn, 200) + assert Enum.map(response["users"], & &1["deactivated"]) == [false, false] + + 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 + user_one = insert(:user, deactivated: false) + user_two = insert(:user, deactivated: false) + + conn = + patch( + conn, + "/api/pleroma/admin/users/deactivate", + %{nicknames: [user_one.nickname, user_two.nickname]} + ) + + response = json_response(conn, 200) + assert Enum.map(response["users"], & &1["deactivated"]) == [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}" + end + + test "PATCH /api/pleroma/admin/users/approve", %{admin: admin, conn: conn} do + user_one = insert(:user, approval_pending: true) + user_two = insert(:user, approval_pending: true) + + conn = + patch( + conn, + "/api/pleroma/admin/users/approve", + %{nicknames: [user_one.nickname, user_two.nickname]} + ) + + response = json_response(conn, 200) + assert Enum.map(response["users"], & &1["approval_pending"]) == [false, false] + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} approved users: @#{user_one.nickname}, @#{user_two.nickname}" + end + + test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do + user = insert(:user) + + conn = patch(conn, "/api/pleroma/admin/users/#{user.nickname}/toggle_activation") + + assert json_response(conn, 200) == + %{ + "deactivated" => !user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deactivated users: @#{user.nickname}" + end + + describe "PUT disable_mfa" do + test "returns 200 and disable 2fa", %{conn: conn} do + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: "otp_secret", confirmed: true} + } + ) + + response = + conn + |> put("/api/pleroma/admin/users/disable_mfa", %{nickname: user.nickname}) + |> json_response(200) + + assert response == user.nickname + mfa_settings = refresh_record(user).multi_factor_authentication_settings + + refute mfa_settings.enabled + refute mfa_settings.totp.confirmed + end + + test "returns 404 if user not found", %{conn: conn} do + response = + conn + |> put("/api/pleroma/admin/users/disable_mfa", %{nickname: "nickname"}) + |> json_response(404) + + assert response == %{"error" => "Not found"} + end + end + + describe "GET /api/pleroma/admin/restart" do + setup do: clear_config(:configurable_from_database, true) + + test "pleroma restarts", %{conn: conn} do + capture_log(fn -> + assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{} + end) =~ "pleroma restarted" + + refute Restarter.Pleroma.need_reboot?() + end + end + + test "need_reboot flag", %{conn: conn} do + assert conn + |> get("/api/pleroma/admin/need_reboot") + |> json_response(200) == %{"need_reboot" => false} + + Restarter.Pleroma.need_reboot() + + assert conn + |> get("/api/pleroma/admin/need_reboot") + |> json_response(200) == %{"need_reboot" => true} + + on_exit(fn -> Restarter.Pleroma.refresh() end) + end + + describe "GET /api/pleroma/admin/users/:nickname/statuses" do + setup do + user = insert(:user) + + date1 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!() + date2 = (DateTime.to_unix(DateTime.utc_now()) + 1000) |> DateTime.from_unix!() + date3 = (DateTime.to_unix(DateTime.utc_now()) + 3000) |> DateTime.from_unix!() + + insert(:note_activity, user: user, published: date1) + insert(:note_activity, user: user, published: date2) + insert(:note_activity, user: user, published: date3) + + %{user: user} + end + + test "renders user's statuses", %{conn: conn, user: user} do + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses") + + assert json_response(conn, 200) |> length() == 3 + end + + test "renders user's statuses with a limit", %{conn: conn, user: user} do + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?page_size=2") + + assert json_response(conn, 200) |> length() == 2 + end + + test "doesn't return private statuses by default", %{conn: conn, user: user} do + {:ok, _private_status} = CommonAPI.post(user, %{status: "private", visibility: "private"}) + + {:ok, _public_status} = CommonAPI.post(user, %{status: "public", visibility: "public"}) + + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses") + + assert json_response(conn, 200) |> length() == 4 + end + + test "returns private statuses with godmode on", %{conn: conn, user: user} do + {:ok, _private_status} = CommonAPI.post(user, %{status: "private", visibility: "private"}) + + {:ok, _public_status} = CommonAPI.post(user, %{status: "public", visibility: "public"}) + + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?godmode=true") + + assert json_response(conn, 200) |> length() == 5 + end + + test "excludes reblogs by default", %{conn: conn, user: user} do + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "."}) + {:ok, %Activity{}} = CommonAPI.repeat(activity.id, other_user) + + conn_res = get(conn, "/api/pleroma/admin/users/#{other_user.nickname}/statuses") + assert json_response(conn_res, 200) |> length() == 0 + + conn_res = + get(conn, "/api/pleroma/admin/users/#{other_user.nickname}/statuses?with_reblogs=true") + + assert json_response(conn_res, 200) |> length() == 1 + end + end + + describe "GET /api/pleroma/admin/users/:nickname/chats" do + setup do + user = insert(:user) + recipients = insert_list(3, :user) + + Enum.each(recipients, fn recipient -> + CommonAPI.post_chat_message(user, recipient, "yo") + end) + + %{user: user} + end + + test "renders user's chats", %{conn: conn, user: user} do + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/chats") + + assert json_response(conn, 200) |> length() == 3 + end + end + + describe "GET /api/pleroma/admin/users/:nickname/chats unauthorized" do + setup do + user = insert(:user) + recipient = insert(:user) + CommonAPI.post_chat_message(user, recipient, "yo") + %{conn: conn} = oauth_access(["read:chats"]) + %{conn: conn, user: user} + end + + test "returns 403", %{conn: conn, user: user} do + conn + |> get("/api/pleroma/admin/users/#{user.nickname}/chats") + |> json_response(403) + end + end + + describe "GET /api/pleroma/admin/users/:nickname/chats unauthenticated" do + setup do + user = insert(:user) + recipient = insert(:user) + CommonAPI.post_chat_message(user, recipient, "yo") + %{conn: build_conn(), user: user} + end + + test "returns 403", %{conn: conn, user: user} do + conn + |> get("/api/pleroma/admin/users/#{user.nickname}/chats") + |> json_response(403) + end + end + + describe "GET /api/pleroma/admin/moderation_log" do + setup do + moderator = insert(:user, is_moderator: true) + + %{moderator: moderator} + end + + test "returns the log", %{conn: conn, admin: admin} do + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_follow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second) + }) + + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_unfollow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second) + }) + + conn = get(conn, "/api/pleroma/admin/moderation_log") + + response = json_response(conn, 200) + [first_entry, second_entry] = response["items"] + + assert response["total"] == 2 + assert first_entry["data"]["action"] == "relay_unfollow" + + assert first_entry["message"] == + "@#{admin.nickname} unfollowed relay: https://example.org/relay" + + assert second_entry["data"]["action"] == "relay_follow" + + assert second_entry["message"] == + "@#{admin.nickname} followed relay: https://example.org/relay" + end + + test "returns the log with pagination", %{conn: conn, admin: admin} do + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_follow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second) + }) + + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_unfollow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second) + }) + + conn1 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=1") + + response1 = json_response(conn1, 200) + [first_entry] = response1["items"] + + assert response1["total"] == 2 + assert response1["items"] |> length() == 1 + assert first_entry["data"]["action"] == "relay_unfollow" + + assert first_entry["message"] == + "@#{admin.nickname} unfollowed relay: https://example.org/relay" + + conn2 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=2") + + response2 = json_response(conn2, 200) + [second_entry] = response2["items"] + + assert response2["total"] == 2 + assert response2["items"] |> length() == 1 + assert second_entry["data"]["action"] == "relay_follow" + + assert second_entry["message"] == + "@#{admin.nickname} followed relay: https://example.org/relay" + end + + test "filters log by date", %{conn: conn, admin: admin} do + first_date = "2017-08-15T15:47:06Z" + second_date = "2017-08-20T15:47:06Z" + + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_follow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.from_iso8601!(first_date) + }) + + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_unfollow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.from_iso8601!(second_date) + }) + + conn1 = + get( + conn, + "/api/pleroma/admin/moderation_log?start_date=#{second_date}" + ) + + response1 = json_response(conn1, 200) + [first_entry] = response1["items"] + + assert response1["total"] == 1 + assert first_entry["data"]["action"] == "relay_unfollow" + + assert first_entry["message"] == + "@#{admin.nickname} unfollowed relay: https://example.org/relay" + end + + test "returns log filtered by user", %{conn: conn, admin: admin, moderator: moderator} do + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_follow", + target: "https://example.org/relay" + } + }) + + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => moderator.id, + "nickname" => moderator.nickname, + "type" => "user" + }, + action: "relay_unfollow", + target: "https://example.org/relay" + } + }) + + conn1 = get(conn, "/api/pleroma/admin/moderation_log?user_id=#{moderator.id}") + + response1 = json_response(conn1, 200) + [first_entry] = response1["items"] + + assert response1["total"] == 1 + assert get_in(first_entry, ["data", "actor", "id"]) == moderator.id + end + + test "returns log filtered by search", %{conn: conn, moderator: moderator} do + ModerationLog.insert_log(%{ + actor: moderator, + action: "relay_follow", + target: "https://example.org/relay" + }) + + ModerationLog.insert_log(%{ + actor: moderator, + action: "relay_unfollow", + target: "https://example.org/relay" + }) + + conn1 = get(conn, "/api/pleroma/admin/moderation_log?search=unfo") + + response1 = json_response(conn1, 200) + [first_entry] = response1["items"] + + assert response1["total"] == 1 + + assert get_in(first_entry, ["data", "message"]) == + "@#{moderator.nickname} unfollowed relay: https://example.org/relay" + end + end + + test "gets a remote users when [:instance, :limit_to_local_content] is set to :unauthenticated", + %{conn: conn} do + clear_config(Pleroma.Config.get([:instance, :limit_to_local_content]), :unauthenticated) + user = insert(:user, %{local: false, nickname: "u@peer1.com"}) + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials") + + assert json_response(conn, 200) + end + + describe "GET /users/:nickname/credentials" do + test "gets the user credentials", %{conn: conn} do + user = insert(:user) + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials") + + response = assert json_response(conn, 200) + assert response["email"] == user.email + end + + test "returns 403 if requested by a non-admin" do + user = insert(:user) + + conn = + build_conn() + |> assign(:user, user) + |> get("/api/pleroma/admin/users/#{user.nickname}/credentials") + + assert json_response(conn, :forbidden) + end + end + + describe "PATCH /users/:nickname/credentials" do + setup do + user = insert(:user) + [user: user] + end + + test "changes password and email", %{conn: conn, admin: admin, user: user} do + assert user.password_reset_pending == false + + 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, 200) == %{"status" => "success"} + + ObanHelpers.perform_all() + + updated_user = User.get_by_id(user.id) + + assert updated_user.email == "new_email@example.com" + assert updated_user.name == "new_name" + assert updated_user.password_hash != user.password_hash + assert updated_user.password_reset_pending == true + + [log_entry2, log_entry1] = ModerationLog |> Repo.all() |> Enum.sort() + + assert ModerationLog.get_log_entry_message(log_entry1) == + "@#{admin.nickname} updated users: @#{user.nickname}" + + assert ModerationLog.get_log_entry_message(log_entry2) == + "@#{admin.nickname} forced password reset for users: @#{user.nickname}" + end + + test "returns 403 if requested by a non-admin", %{user: user} do + conn = + build_conn() + |> assign(:user, user) + |> patch("/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" + + assert patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{ + "actor_type" => "Service" + }) + |> json_response(200) == %{"status" => "success"} + + updated_user = User.get_by_id(user.id) + + assert updated_user.actor_type == "Service" + + assert patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{ + "actor_type" => "Application" + }) + |> json_response(400) == %{"errors" => %{"actor_type" => "is invalid"}} + end + + test "update non existing user", %{conn: conn} do + assert patch(conn, "/api/pleroma/admin/users/non-existing/credentials", %{ + "password" => "new_password" + }) + |> json_response(404) == %{"error" => "Not found"} + end + end + + describe "PATCH /users/:nickname/force_password_reset" do + test "sets password_reset_pending to true", %{conn: conn} do + user = insert(:user) + assert user.password_reset_pending == false + + conn = + patch(conn, "/api/pleroma/admin/users/force_password_reset", %{nicknames: [user.nickname]}) + + assert empty_json_response(conn) == "" + + ObanHelpers.perform_all() + + assert User.get_by_id(user.id).password_reset_pending == true + end + end + + describe "instances" do + test "GET /instances/:instance/statuses", %{conn: conn} do + user = insert(:user, local: false, nickname: "archaeme@archae.me") + user2 = insert(:user, local: false, nickname: "test@test.com") + insert_pair(:note_activity, user: user) + activity = insert(:note_activity, user: user2) + + ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses") + + response = json_response(ret_conn, 200) + + assert length(response) == 2 + + ret_conn = get(conn, "/api/pleroma/admin/instances/test.com/statuses") + + response = json_response(ret_conn, 200) + + assert length(response) == 1 + + ret_conn = get(conn, "/api/pleroma/admin/instances/nonexistent.com/statuses") + + response = json_response(ret_conn, 200) + + assert Enum.empty?(response) + + CommonAPI.repeat(activity.id, user) + + ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses") + response = json_response(ret_conn, 200) + assert length(response) == 2 + + ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses?with_reblogs=true") + response = json_response(ret_conn, 200) + assert length(response) == 3 + end + end + + describe "PATCH /confirm_email" do + test "it confirms emails of two users", %{conn: conn, admin: admin} do + [first_user, second_user] = insert_pair(:user, confirmation_pending: true) + + assert first_user.confirmation_pending == true + assert second_user.confirmation_pending == true + + ret_conn = + patch(conn, "/api/pleroma/admin/users/confirm_email", %{ + nicknames: [ + first_user.nickname, + second_user.nickname + ] + }) + + assert ret_conn.status == 200 + + assert first_user.confirmation_pending == true + assert second_user.confirmation_pending == true + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} confirmed email for users: @#{first_user.nickname}, @#{ + second_user.nickname + }" + end + end + + describe "PATCH /resend_confirmation_email" do + test "it resend emails for two users", %{conn: conn, admin: admin} do + [first_user, second_user] = insert_pair(:user, confirmation_pending: true) + + ret_conn = + patch(conn, "/api/pleroma/admin/users/resend_confirmation_email", %{ + nicknames: [ + first_user.nickname, + second_user.nickname + ] + }) + + assert ret_conn.status == 200 + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} re-sent confirmation email for users: @#{first_user.nickname}, @#{ + second_user.nickname + }" + + ObanHelpers.perform_all() + + Pleroma.Emails.UserEmail.account_confirmation_email(first_user) + # temporary hackney fix until hackney max_connections bug is fixed + # https://git.pleroma.social/pleroma/pleroma/-/issues/2101 + |> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]]) + |> assert_email_sent() + end + end + + describe "/api/pleroma/admin/stats" do + test "status visibility count", %{conn: conn} do + admin = insert(:user, is_admin: true) + user = insert(:user) + CommonAPI.post(user, %{visibility: "public", status: "hey"}) + CommonAPI.post(user, %{visibility: "unlisted", status: "hey"}) + CommonAPI.post(user, %{visibility: "unlisted", status: "hey"}) + + response = + conn + |> assign(:user, admin) + |> get("/api/pleroma/admin/stats") + |> json_response(200) + + assert %{"direct" => 0, "private" => 0, "public" => 1, "unlisted" => 2} = + response["status_visibility"] + end + + test "by instance", %{conn: conn} do + admin = insert(:user, is_admin: true) + user1 = insert(:user) + instance2 = "instance2.tld" + user2 = insert(:user, %{ap_id: "https://#{instance2}/@actor"}) + + CommonAPI.post(user1, %{visibility: "public", status: "hey"}) + CommonAPI.post(user2, %{visibility: "unlisted", status: "hey"}) + CommonAPI.post(user2, %{visibility: "private", status: "hey"}) + + response = + conn + |> assign(:user, admin) + |> get("/api/pleroma/admin/stats", instance: instance2) + |> json_response(200) + + assert %{"direct" => 0, "private" => 1, "public" => 0, "unlisted" => 1} = + response["status_visibility"] + end + end +end + +# Needed for testing +defmodule Pleroma.Web.Endpoint.NotReal do +end + +defmodule Pleroma.Captcha.NotReal do +end diff --git a/test/pleroma/web/admin_api/controllers/chat_controller_test.exs b/test/pleroma/web/admin_api/controllers/chat_controller_test.exs @@ -0,0 +1,219 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ChatControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Config + alias Pleroma.ModerationLog + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.Web.CommonAPI + + defp admin_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 "DELETE /api/pleroma/admin/chats/:id/messages/:message_id" do + setup do: admin_setup() + + test "it deletes a message from the chat", %{conn: conn, admin: admin} do + user = insert(:user) + recipient = insert(:user) + + {:ok, message} = + CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend") + + object = Object.normalize(message, false) + + chat = Chat.get(user.id, recipient.ap_id) + recipient_chat = Chat.get(recipient.id, user.ap_id) + + cm_ref = MessageReference.for_chat_and_object(chat, object) + recipient_cm_ref = MessageReference.for_chat_and_object(recipient_chat, object) + + result = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}") + |> json_response_and_validate_schema(200) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deleted chat message ##{cm_ref.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 + end + + describe "GET /api/pleroma/admin/chats/:id/messages" do + setup do: admin_setup() + + test "it paginates", %{conn: conn} do + user = insert(:user) + recipient = insert(:user) + + Enum.each(1..30, fn _ -> + {:ok, _} = CommonAPI.post_chat_message(user, recipient, "hey") + end) + + chat = Chat.get(user.id, recipient.ap_id) + + result = + conn + |> get("/api/pleroma/admin/chats/#{chat.id}/messages") + |> json_response_and_validate_schema(200) + + assert length(result) == 20 + + result = + conn + |> get("/api/pleroma/admin/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}") + |> json_response_and_validate_schema(200) + + assert length(result) == 10 + end + + test "it returns the messages for a given chat", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey") + {:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey") + {:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?") + {:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?") + + chat = Chat.get(user.id, other_user.ap_id) + + result = + conn + |> get("/api/pleroma/admin/chats/#{chat.id}/messages") + |> json_response_and_validate_schema(200) + + result + |> Enum.each(fn message -> + assert message["chat_id"] == chat.id |> to_string() + end) + + assert length(result) == 3 + end + end + + describe "GET /api/pleroma/admin/chats/:id" do + setup do: admin_setup() + + test "it returns a chat", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> get("/api/pleroma/admin/chats/#{chat.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] == to_string(chat.id) + assert %{} = result["sender"] + assert %{} = result["receiver"] + refute result["account"] + end + end + + describe "unauthorized chat moderation" do + setup do + user = insert(:user) + recipient = insert(:user) + + {:ok, message} = CommonAPI.post_chat_message(user, recipient, "Yo") + object = Object.normalize(message, false) + chat = Chat.get(user.id, recipient.ap_id) + cm_ref = MessageReference.for_chat_and_object(chat, object) + + %{conn: conn} = oauth_access(["read:chats", "write:chats"]) + %{conn: conn, chat: chat, cm_ref: cm_ref} + end + + test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{ + conn: conn, + chat: chat, + cm_ref: cm_ref + } do + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}") + |> json_response(403) + + assert MessageReference.get_by_id(cm_ref.id) == cm_ref + end + + test "GET /api/pleroma/admin/chats/:id/messages", %{conn: conn, chat: chat} do + conn + |> get("/api/pleroma/admin/chats/#{chat.id}/messages") + |> json_response(403) + end + + test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do + conn + |> get("/api/pleroma/admin/chats/#{chat.id}") + |> json_response(403) + end + end + + describe "unauthenticated chat moderation" do + setup do + user = insert(:user) + recipient = insert(:user) + + {:ok, message} = CommonAPI.post_chat_message(user, recipient, "Yo") + object = Object.normalize(message, false) + chat = Chat.get(user.id, recipient.ap_id) + cm_ref = MessageReference.for_chat_and_object(chat, object) + + %{conn: build_conn(), chat: chat, cm_ref: cm_ref} + end + + test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{ + conn: conn, + chat: chat, + cm_ref: cm_ref + } do + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}") + |> json_response(403) + + assert MessageReference.get_by_id(cm_ref.id) == cm_ref + end + + test "GET /api/pleroma/admin/chats/:id/messages", %{conn: conn, chat: chat} do + conn + |> get("/api/pleroma/admin/chats/#{chat.id}/messages") + |> json_response(403) + end + + test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do + conn + |> get("/api/pleroma/admin/chats/#{chat.id}") + |> json_response(403) + end + end +end diff --git a/test/pleroma/web/admin_api/controllers/config_controller_test.exs b/test/pleroma/web/admin_api/controllers/config_controller_test.exs @@ -0,0 +1,1465 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do + use Pleroma.Web.ConnCase, async: true + + import ExUnit.CaptureLog + import Pleroma.Factory + + alias Pleroma.Config + alias Pleroma.ConfigDB + + 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/config" do + setup do: clear_config(:configurable_from_database, true) + + test "when configuration from database is off", %{conn: conn} do + Config.put(:configurable_from_database, false) + conn = get(conn, "/api/pleroma/admin/config") + + assert json_response_and_validate_schema(conn, 400) == + %{ + "error" => "To use this endpoint you need to enable configuration from database." + } + end + + test "with settings only in db", %{conn: conn} do + config1 = insert(:config) + config2 = insert(:config) + + conn = get(conn, "/api/pleroma/admin/config?only_db=true") + + %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => key1, + "value" => _ + }, + %{ + "group" => ":pleroma", + "key" => key2, + "value" => _ + } + ] + } = json_response_and_validate_schema(conn, 200) + + assert key1 == inspect(config1.key) + assert key2 == inspect(config2.key) + end + + test "db is added to settings that are in db", %{conn: conn} do + _config = insert(:config, key: ":instance", value: [name: "Some name"]) + + %{"configs" => configs} = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + [instance_config] = + Enum.filter(configs, fn %{"group" => group, "key" => key} -> + group == ":pleroma" and key == ":instance" + end) + + assert instance_config["db"] == [":name"] + end + + test "merged default setting with db settings", %{conn: conn} do + config1 = insert(:config) + config2 = insert(:config) + + config3 = + insert(:config, + value: [k1: :v1, k2: :v2] + ) + + %{"configs" => configs} = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + assert length(configs) > 3 + + saved_configs = [config1, config2, config3] + keys = Enum.map(saved_configs, &inspect(&1.key)) + + received_configs = + Enum.filter(configs, fn %{"group" => group, "key" => key} -> + group == ":pleroma" and key in keys + end) + + assert length(received_configs) == 3 + + db_keys = + config3.value + |> Keyword.keys() + |> ConfigDB.to_json_types() + + keys = Enum.map(saved_configs -- [config3], &inspect(&1.key)) + + values = Enum.map(saved_configs, &ConfigDB.to_json_types(&1.value)) + + mapset_keys = MapSet.new(keys ++ db_keys) + + Enum.each(received_configs, fn %{"value" => value, "db" => db} -> + db = MapSet.new(db) + assert MapSet.subset?(db, mapset_keys) + + assert value in values + end) + end + + test "subkeys with full update right merge", %{conn: conn} do + insert(:config, + key: ":emoji", + value: [groups: [a: 1, b: 2], key: [a: 1]] + ) + + insert(:config, + key: ":assets", + value: [mascots: [a: 1, b: 2], key: [a: 1]] + ) + + %{"configs" => configs} = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + vals = + Enum.filter(configs, fn %{"group" => group, "key" => key} -> + group == ":pleroma" and key in [":emoji", ":assets"] + end) + + emoji = Enum.find(vals, fn %{"key" => key} -> key == ":emoji" end) + assets = Enum.find(vals, fn %{"key" => key} -> key == ":assets" end) + + emoji_val = ConfigDB.to_elixir_types(emoji["value"]) + assets_val = ConfigDB.to_elixir_types(assets["value"]) + + assert emoji_val[:groups] == [a: 1, b: 2] + assert assets_val[:mascots] == [a: 1, b: 2] + end + + test "with valid `admin_token` query parameter, skips OAuth scopes check" do + clear_config([:admin_token], "password123") + + build_conn() + |> get("/api/pleroma/admin/config?admin_token=password123") + |> json_response_and_validate_schema(200) + end + end + + test "POST /api/pleroma/admin/config error", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{"configs" => []}) + + assert json_response_and_validate_schema(conn, 400) == + %{"error" => "To use this endpoint you need to enable configuration from database."} + end + + describe "POST /api/pleroma/admin/config" do + setup do + http = Application.get_env(:pleroma, :http) + + on_exit(fn -> + Application.delete_env(:pleroma, :key1) + Application.delete_env(:pleroma, :key2) + Application.delete_env(:pleroma, :key3) + Application.delete_env(:pleroma, :key4) + Application.delete_env(:pleroma, :keyaa1) + Application.delete_env(:pleroma, :keyaa2) + Application.delete_env(:pleroma, Pleroma.Web.Endpoint.NotReal) + Application.delete_env(:pleroma, Pleroma.Captcha.NotReal) + Application.put_env(:pleroma, :http, http) + Application.put_env(:tesla, :adapter, Tesla.Mock) + Restarter.Pleroma.refresh() + end) + end + + setup do: clear_config(:configurable_from_database, true) + + @tag capture_log: true + test "create new config setting in db", %{conn: conn} do + ueberauth = Application.get_env(:ueberauth, Ueberauth) + on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":key1", value: "value1"}, + %{ + group: ":ueberauth", + key: "Ueberauth", + value: [%{"tuple" => [":consumer_secret", "aaaa"]}] + }, + %{ + group: ":pleroma", + key: ":key2", + value: %{ + ":nested_1" => "nested_value1", + ":nested_2" => [ + %{":nested_22" => "nested_value222"}, + %{":nested_33" => %{":nested_44" => "nested_444"}} + ] + } + }, + %{ + group: ":pleroma", + key: ":key3", + value: [ + %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, + %{"nested_4" => true} + ] + }, + %{ + group: ":pleroma", + key: ":key4", + value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"} + }, + %{ + group: ":idna", + key: ":key5", + value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]} + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => "value1", + "db" => [":key1"] + }, + %{ + "group" => ":ueberauth", + "key" => "Ueberauth", + "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}], + "db" => [":consumer_secret"] + }, + %{ + "group" => ":pleroma", + "key" => ":key2", + "value" => %{ + ":nested_1" => "nested_value1", + ":nested_2" => [ + %{":nested_22" => "nested_value222"}, + %{":nested_33" => %{":nested_44" => "nested_444"}} + ] + }, + "db" => [":key2"] + }, + %{ + "group" => ":pleroma", + "key" => ":key3", + "value" => [ + %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, + %{"nested_4" => true} + ], + "db" => [":key3"] + }, + %{ + "group" => ":pleroma", + "key" => ":key4", + "value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"}, + "db" => [":key4"] + }, + %{ + "group" => ":idna", + "key" => ":key5", + "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}, + "db" => [":key5"] + } + ], + "need_reboot" => false + } + + assert Application.get_env(:pleroma, :key1) == "value1" + + assert Application.get_env(:pleroma, :key2) == %{ + nested_1: "nested_value1", + nested_2: [ + %{nested_22: "nested_value222"}, + %{nested_33: %{nested_44: "nested_444"}} + ] + } + + assert Application.get_env(:pleroma, :key3) == [ + %{"nested_3" => :nested_3, "nested_33" => "nested_33"}, + %{"nested_4" => true} + ] + + assert Application.get_env(:pleroma, :key4) == %{ + "endpoint" => "https://example.com", + nested_5: :upload + } + + assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []} + end + + 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) + + on_exit(fn -> + Application.put_env(:quack, :level, level) + Application.put_env(:quack, :meta, meta) + Application.put_env(:quack, :webhook_url, webhook_url) + end) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":quack", + key: ":level", + value: ":info" + }, + %{ + group: ":quack", + key: ":meta", + value: [":none"] + }, + %{ + group: ":quack", + key: ":webhook_url", + value: "https://hooks.slack.com/services/KEY" + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":quack", + "key" => ":level", + "value" => ":info", + "db" => [":level"] + }, + %{ + "group" => ":quack", + "key" => ":meta", + "value" => [":none"], + "db" => [":meta"] + }, + %{ + "group" => ":quack", + "key" => ":webhook_url", + "value" => "https://hooks.slack.com/services/KEY", + "db" => [":webhook_url"] + } + ], + "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" + end + + test "saving config with partial update", %{conn: conn} do + insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2)) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]} + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key1", 1]}, + %{"tuple" => [":key2", 2]}, + %{"tuple" => [":key3", 3]} + ], + "db" => [":key1", ":key2", ":key3"] + } + ], + "need_reboot" => false + } + end + + test "saving config which need pleroma reboot", %{conn: conn} do + chat = Config.get(:chat) + on_exit(fn -> Config.put(:chat, chat) end) + + assert conn + |> put_req_header("content-type", "application/json") + |> post( + "/api/pleroma/admin/config", + %{ + configs: [ + %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]} + ] + } + ) + |> json_response_and_validate_schema(200) == %{ + "configs" => [ + %{ + "db" => [":enabled"], + "group" => ":pleroma", + "key" => ":chat", + "value" => [%{"tuple" => [":enabled", true]}] + } + ], + "need_reboot" => true + } + + configs = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + assert configs["need_reboot"] + + capture_log(fn -> + assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == + %{} + end) =~ "pleroma restarted" + + configs = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + assert configs["need_reboot"] == false + end + + test "update setting which need reboot, don't change reboot flag until reboot", %{conn: conn} do + chat = Config.get(:chat) + on_exit(fn -> Config.put(:chat, chat) end) + + assert conn + |> put_req_header("content-type", "application/json") + |> post( + "/api/pleroma/admin/config", + %{ + configs: [ + %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]} + ] + } + ) + |> json_response_and_validate_schema(200) == %{ + "configs" => [ + %{ + "db" => [":enabled"], + "group" => ":pleroma", + "key" => ":chat", + "value" => [%{"tuple" => [":enabled", true]}] + } + ], + "need_reboot" => true + } + + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]} + ] + }) + |> json_response_and_validate_schema(200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key3", 3]} + ], + "db" => [":key3"] + } + ], + "need_reboot" => true + } + + capture_log(fn -> + assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == + %{} + end) =~ "pleroma restarted" + + configs = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + assert configs["need_reboot"] == false + end + + test "saving config with nested merge", %{conn: conn} do + insert(:config, key: :key1, value: [key1: 1, key2: [k1: 1, k2: 2]]) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":key1", + value: [ + %{"tuple" => [":key3", 3]}, + %{ + "tuple" => [ + ":key2", + [ + %{"tuple" => [":k2", 1]}, + %{"tuple" => [":k3", 3]} + ] + ] + } + ] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key1", 1]}, + %{"tuple" => [":key3", 3]}, + %{ + "tuple" => [ + ":key2", + [ + %{"tuple" => [":k1", 1]}, + %{"tuple" => [":k2", 1]}, + %{"tuple" => [":k3", 3]} + ] + ] + } + ], + "db" => [":key1", ":key3", ":key2"] + } + ], + "need_reboot" => false + } + end + + test "saving special atoms", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{ + "tuple" => [ + ":ssl_options", + [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}] + ] + } + ] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{ + "tuple" => [ + ":ssl_options", + [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}] + ] + } + ], + "db" => [":ssl_options"] + } + ], + "need_reboot" => false + } + + assert Application.get_env(:pleroma, :key1) == [ + ssl_options: [versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]] + ] + end + + test "saving full setting if value is in full_key_update list", %{conn: conn} do + backends = Application.get_env(:logger, :backends) + on_exit(fn -> Application.put_env(:logger, :backends, backends) end) + + insert(:config, + group: :logger, + key: :backends, + value: [] + ) + + Pleroma.Config.TransferTask.load_and_update_env([], false) + + assert Application.get_env(:logger, :backends) == [] + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":logger", + key: ":backends", + value: [":console"] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":logger", + "key" => ":backends", + "value" => [ + ":console" + ], + "db" => [":backends"] + } + ], + "need_reboot" => false + } + + assert Application.get_env(:logger, :backends) == [ + :console + ] + end + + test "saving full setting if value is not keyword", %{conn: conn} do + insert(:config, + group: :tesla, + key: :adapter, + value: Tesla.Adapter.Hackey + ) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: ":tesla", key: ":adapter", value: "Tesla.Adapter.Httpc"} + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":tesla", + "key" => ":adapter", + "value" => "Tesla.Adapter.Httpc", + "db" => [":adapter"] + } + ], + "need_reboot" => false + } + end + + test "update config setting & delete with fallback to default value", %{ + conn: conn, + admin: admin, + token: token + } do + ueberauth = Application.get_env(:ueberauth, Ueberauth) + insert(:config, key: :keyaa1) + insert(:config, key: :keyaa2) + + config3 = + insert(:config, + group: :ueberauth, + key: Ueberauth + ) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":keyaa1", value: "another_value"}, + %{group: ":pleroma", key: ":keyaa2", value: "another_value"} + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":keyaa1", + "value" => "another_value", + "db" => [":keyaa1"] + }, + %{ + "group" => ":pleroma", + "key" => ":keyaa2", + "value" => "another_value", + "db" => [":keyaa2"] + } + ], + "need_reboot" => false + } + + assert Application.get_env(:pleroma, :keyaa1) == "another_value" + assert Application.get_env(:pleroma, :keyaa2) == "another_value" + assert Application.get_env(:ueberauth, Ueberauth) == config3.value + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":keyaa2", delete: true}, + %{ + group: ":ueberauth", + key: "Ueberauth", + delete: true + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [], + "need_reboot" => false + } + + assert Application.get_env(:ueberauth, Ueberauth) == ueberauth + refute Keyword.has_key?(Application.get_all_env(:pleroma), :keyaa2) + end + + test "common config example", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Captcha.NotReal", + "value" => [ + %{"tuple" => [":enabled", false]}, + %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]}, + %{"tuple" => [":seconds_valid", 60]}, + %{"tuple" => [":path", ""]}, + %{"tuple" => [":key1", nil]}, + %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}, + %{"tuple" => [":regex1", "~r/https:\/\/example.com/"]}, + %{"tuple" => [":regex2", "~r/https:\/\/example.com/u"]}, + %{"tuple" => [":regex3", "~r/https:\/\/example.com/i"]}, + %{"tuple" => [":regex4", "~r/https:\/\/example.com/s"]}, + %{"tuple" => [":name", "Pleroma"]} + ] + } + ] + }) + + assert Config.get([Pleroma.Captcha.NotReal, :name]) == "Pleroma" + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Captcha.NotReal", + "value" => [ + %{"tuple" => [":enabled", false]}, + %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]}, + %{"tuple" => [":seconds_valid", 60]}, + %{"tuple" => [":path", ""]}, + %{"tuple" => [":key1", nil]}, + %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}, + %{"tuple" => [":regex1", "~r/https:\\/\\/example.com/"]}, + %{"tuple" => [":regex2", "~r/https:\\/\\/example.com/u"]}, + %{"tuple" => [":regex3", "~r/https:\\/\\/example.com/i"]}, + %{"tuple" => [":regex4", "~r/https:\\/\\/example.com/s"]}, + %{"tuple" => [":name", "Pleroma"]} + ], + "db" => [ + ":enabled", + ":method", + ":seconds_valid", + ":path", + ":key1", + ":partial_chain", + ":regex1", + ":regex2", + ":regex3", + ":regex4", + ":name" + ] + } + ], + "need_reboot" => false + } + end + + test "tuples with more than two values", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Web.Endpoint.NotReal", + "value" => [ + %{ + "tuple" => [ + ":http", + [ + %{ + "tuple" => [ + ":key2", + [ + %{ + "tuple" => [ + ":_", + [ + %{ + "tuple" => [ + "/api/v1/streaming", + "Pleroma.Web.MastodonAPI.WebsocketHandler", + [] + ] + }, + %{ + "tuple" => [ + "/websocket", + "Phoenix.Endpoint.CowboyWebSocket", + %{ + "tuple" => [ + "Phoenix.Transports.WebSocket", + %{ + "tuple" => [ + "Pleroma.Web.Endpoint", + "Pleroma.Web.UserSocket", + [] + ] + } + ] + } + ] + }, + %{ + "tuple" => [ + ":_", + "Phoenix.Endpoint.Cowboy2Handler", + %{"tuple" => ["Pleroma.Web.Endpoint", []]} + ] + } + ] + ] + } + ] + ] + } + ] + ] + } + ] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Web.Endpoint.NotReal", + "value" => [ + %{ + "tuple" => [ + ":http", + [ + %{ + "tuple" => [ + ":key2", + [ + %{ + "tuple" => [ + ":_", + [ + %{ + "tuple" => [ + "/api/v1/streaming", + "Pleroma.Web.MastodonAPI.WebsocketHandler", + [] + ] + }, + %{ + "tuple" => [ + "/websocket", + "Phoenix.Endpoint.CowboyWebSocket", + %{ + "tuple" => [ + "Phoenix.Transports.WebSocket", + %{ + "tuple" => [ + "Pleroma.Web.Endpoint", + "Pleroma.Web.UserSocket", + [] + ] + } + ] + } + ] + }, + %{ + "tuple" => [ + ":_", + "Phoenix.Endpoint.Cowboy2Handler", + %{"tuple" => ["Pleroma.Web.Endpoint", []]} + ] + } + ] + ] + } + ] + ] + } + ] + ] + } + ], + "db" => [":http"] + } + ], + "need_reboot" => false + } + end + + test "settings with nesting map", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key2", "some_val"]}, + %{ + "tuple" => [ + ":key3", + %{ + ":max_options" => 20, + ":max_option_chars" => 200, + ":min_expiration" => 0, + ":max_expiration" => 31_536_000, + "nested" => %{ + ":max_options" => 20, + ":max_option_chars" => 200, + ":min_expiration" => 0, + ":max_expiration" => 31_536_000 + } + } + ] + } + ] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == + %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key2", "some_val"]}, + %{ + "tuple" => [ + ":key3", + %{ + ":max_expiration" => 31_536_000, + ":max_option_chars" => 200, + ":max_options" => 20, + ":min_expiration" => 0, + "nested" => %{ + ":max_expiration" => 31_536_000, + ":max_option_chars" => 200, + ":max_options" => 20, + ":min_expiration" => 0 + } + } + ] + } + ], + "db" => [":key2", ":key3"] + } + ], + "need_reboot" => false + } + end + + test "value as map", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => %{"key" => "some_val"} + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == + %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => %{"key" => "some_val"}, + "db" => [":key1"] + } + ], + "need_reboot" => false + } + end + + test "queues key as atom", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":oban", + "key" => ":queues", + "value" => [ + %{"tuple" => [":federator_incoming", 50]}, + %{"tuple" => [":federator_outgoing", 50]}, + %{"tuple" => [":web_push", 50]}, + %{"tuple" => [":mailer", 10]}, + %{"tuple" => [":transmogrifier", 20]}, + %{"tuple" => [":scheduled_activities", 10]}, + %{"tuple" => [":background", 5]} + ] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":oban", + "key" => ":queues", + "value" => [ + %{"tuple" => [":federator_incoming", 50]}, + %{"tuple" => [":federator_outgoing", 50]}, + %{"tuple" => [":web_push", 50]}, + %{"tuple" => [":mailer", 10]}, + %{"tuple" => [":transmogrifier", 20]}, + %{"tuple" => [":scheduled_activities", 10]}, + %{"tuple" => [":background", 5]} + ], + "db" => [ + ":federator_incoming", + ":federator_outgoing", + ":web_push", + ":mailer", + ":transmogrifier", + ":scheduled_activities", + ":background" + ] + } + ], + "need_reboot" => false + } + end + + test "delete part of settings by atom subkeys", %{conn: conn} do + insert(:config, + key: :keyaa1, + value: [subkey1: "val1", subkey2: "val2", subkey3: "val3"] + ) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":keyaa1", + subkeys: [":subkey1", ":subkey3"], + delete: true + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":keyaa1", + "value" => [%{"tuple" => [":subkey2", "val2"]}], + "db" => [":subkey2"] + } + ], + "need_reboot" => false + } + end + + test "proxy tuple localhost", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":http", + value: [ + %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]} + ] + } + ] + }) + + assert %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":http", + "value" => value, + "db" => db + } + ] + } = json_response_and_validate_schema(conn, 200) + + assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]} in value + assert ":proxy_url" in db + end + + test "proxy tuple domain", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":http", + value: [ + %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]} + ] + } + ] + }) + + assert %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":http", + "value" => value, + "db" => db + } + ] + } = json_response_and_validate_schema(conn, 200) + + assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]} in value + assert ":proxy_url" in db + end + + test "proxy tuple ip", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":http", + value: [ + %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]} + ] + } + ] + }) + + assert %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":http", + "value" => value, + "db" => db + } + ] + } = json_response_and_validate_schema(conn, 200) + + assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]} in value + assert ":proxy_url" in db + end + + @tag capture_log: true + test "doesn't set keys not in the whitelist", %{conn: conn} do + clear_config(:database_config_whitelist, [ + {:pleroma, :key1}, + {:pleroma, :key2}, + {:pleroma, Pleroma.Captcha.NotReal}, + {:not_real} + ]) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":key1", value: "value1"}, + %{group: ":pleroma", key: ":key2", value: "value2"}, + %{group: ":pleroma", key: ":key3", value: "value3"}, + %{group: ":pleroma", key: "Pleroma.Web.Endpoint.NotReal", value: "value4"}, + %{group: ":pleroma", key: "Pleroma.Captcha.NotReal", value: "value5"}, + %{group: ":not_real", key: ":anything", value: "value6"} + ] + }) + + assert Application.get_env(:pleroma, :key1) == "value1" + assert Application.get_env(:pleroma, :key2) == "value2" + assert Application.get_env(:pleroma, :key3) == nil + assert Application.get_env(:pleroma, Pleroma.Web.Endpoint.NotReal) == nil + assert Application.get_env(:pleroma, Pleroma.Captcha.NotReal) == "value5" + assert Application.get_env(:not_real, :anything) == "value6" + end + + test "args for Pleroma.Upload.Filter.Mogrify with custom tuples", %{conn: conn} do + clear_config(Pleroma.Upload.Filter.Mogrify) + + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: "Pleroma.Upload.Filter.Mogrify", + value: [ + %{"tuple" => [":args", ["auto-orient", "strip"]]} + ] + } + ] + }) + |> json_response_and_validate_schema(200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Upload.Filter.Mogrify", + "value" => [ + %{"tuple" => [":args", ["auto-orient", "strip"]]} + ], + "db" => [":args"] + } + ], + "need_reboot" => false + } + + assert Config.get(Pleroma.Upload.Filter.Mogrify) == [args: ["auto-orient", "strip"]] + + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: "Pleroma.Upload.Filter.Mogrify", + value: [ + %{ + "tuple" => [ + ":args", + [ + "auto-orient", + "strip", + "{\"implode\", \"1\"}", + "{\"resize\", \"3840x1080>\"}" + ] + ] + } + ] + } + ] + }) + |> json_response(200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Upload.Filter.Mogrify", + "value" => [ + %{ + "tuple" => [ + ":args", + [ + "auto-orient", + "strip", + "{\"implode\", \"1\"}", + "{\"resize\", \"3840x1080>\"}" + ] + ] + } + ], + "db" => [":args"] + } + ], + "need_reboot" => false + } + + assert Config.get(Pleroma.Upload.Filter.Mogrify) == [ + args: ["auto-orient", "strip", {"implode", "1"}, {"resize", "3840x1080>"}] + ] + end + + test "enables the welcome messages", %{conn: conn} do + clear_config([:welcome]) + + params = %{ + "group" => ":pleroma", + "key" => ":welcome", + "value" => [ + %{ + "tuple" => [ + ":direct_message", + [ + %{"tuple" => [":enabled", true]}, + %{"tuple" => [":message", "Welcome to Pleroma!"]}, + %{"tuple" => [":sender_nickname", "pleroma"]} + ] + ] + }, + %{ + "tuple" => [ + ":chat_message", + [ + %{"tuple" => [":enabled", true]}, + %{"tuple" => [":message", "Welcome to Pleroma!"]}, + %{"tuple" => [":sender_nickname", "pleroma"]} + ] + ] + }, + %{ + "tuple" => [ + ":email", + [ + %{"tuple" => [":enabled", true]}, + %{"tuple" => [":sender", %{"tuple" => ["pleroma@dev.dev", "Pleroma"]}]}, + %{"tuple" => [":subject", "Welcome to <%= instance_name %>!"]}, + %{"tuple" => [":html", "Welcome to <%= instance_name %>!"]}, + %{"tuple" => [":text", "Welcome to <%= instance_name %>!"]} + ] + ] + } + ] + } + + refute Pleroma.User.WelcomeEmail.enabled?() + refute Pleroma.User.WelcomeMessage.enabled?() + refute Pleroma.User.WelcomeChatMessage.enabled?() + + res = + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{"configs" => [params]}) + |> json_response_and_validate_schema(200) + + assert Pleroma.User.WelcomeEmail.enabled?() + assert Pleroma.User.WelcomeMessage.enabled?() + assert Pleroma.User.WelcomeChatMessage.enabled?() + + assert res == %{ + "configs" => [ + %{ + "db" => [":direct_message", ":chat_message", ":email"], + "group" => ":pleroma", + "key" => ":welcome", + "value" => params["value"] + } + ], + "need_reboot" => false + } + end + end + + describe "GET /api/pleroma/admin/config/descriptions" do + test "structure", %{conn: conn} do + admin = insert(:user, is_admin: true) + + conn = + assign(conn, :user, admin) + |> get("/api/pleroma/admin/config/descriptions") + + assert [child | _others] = json_response_and_validate_schema(conn, 200) + + assert child["children"] + assert child["key"] + assert String.starts_with?(child["group"], ":") + assert child["description"] + end + + test "filters by database configuration whitelist", %{conn: conn} do + clear_config(:database_config_whitelist, [ + {:pleroma, :instance}, + {:pleroma, :activitypub}, + {:pleroma, Pleroma.Upload}, + {:esshd} + ]) + + admin = insert(:user, is_admin: true) + + conn = + assign(conn, :user, admin) + |> get("/api/pleroma/admin/config/descriptions") + + children = json_response_and_validate_schema(conn, 200) + + assert length(children) == 4 + + assert Enum.count(children, fn c -> c["group"] == ":pleroma" end) == 3 + + instance = Enum.find(children, fn c -> c["key"] == ":instance" end) + assert instance["children"] + + activitypub = Enum.find(children, fn c -> c["key"] == ":activitypub" end) + assert activitypub["children"] + + 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/instance_document_controller_test.exs b/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs @@ -0,0 +1,106 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do + use Pleroma.Web.ConnCase, async: true + import Pleroma.Factory + alias Pleroma.Config + + @dir "test/tmp/instance_static" + @default_instance_panel ~s(<p>Welcome to <a href="https://pleroma.social" target="_blank">Pleroma!</a></p>) + + setup do + File.mkdir_p!(@dir) + on_exit(fn -> File.rm_rf(@dir) end) + end + + setup do: clear_config([:instance, :static_dir], @dir) + + 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/instance_document/:name" do + test "return the instance document url", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/instance_document/instance-panel") + + assert content = html_response(conn, 200) + assert String.contains?(content, @default_instance_panel) + end + + test "it returns 403 if requested by a non-admin" do + non_admin_user = insert(:user) + token = insert(:oauth_token, user: non_admin_user) + + conn = + build_conn() + |> assign(:user, non_admin_user) + |> assign(:token, token) + |> get("/api/pleroma/admin/instance_document/instance-panel") + + assert json_response(conn, :forbidden) + end + + test "it returns 404 if the instance document with the given name doesn't exist", %{ + conn: conn + } do + conn = get(conn, "/api/pleroma/admin/instance_document/1234") + + assert json_response_and_validate_schema(conn, 404) + end + end + + describe "PATCH /api/pleroma/admin/instance_document/:name" do + test "uploads the instance document", %{conn: conn} do + image = %Plug.Upload{ + content_type: "text/html", + path: Path.absname("test/fixtures/custom_instance_panel.html"), + filename: "custom_instance_panel.html" + } + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/admin/instance_document/instance-panel", %{ + "file" => image + }) + + assert %{"url" => url} = json_response_and_validate_schema(conn, 200) + index = get(build_conn(), url) + assert html_response(index, 200) == "<h2>Custom instance panel</h2>" + end + end + + describe "DELETE /api/pleroma/admin/instance_document/:name" do + test "deletes the instance document", %{conn: conn} do + File.mkdir!(@dir <> "/instance/") + File.write!(@dir <> "/instance/panel.html", "Custom instance panel") + + conn_resp = + conn + |> get("/api/pleroma/admin/instance_document/instance-panel") + + assert html_response(conn_resp, 200) == "Custom instance panel" + + conn + |> delete("/api/pleroma/admin/instance_document/instance-panel") + |> json_response_and_validate_schema(200) + + conn_resp = + conn + |> get("/api/pleroma/admin/instance_document/instance-panel") + + assert content = html_response(conn_resp, 200) + assert String.contains?(content, @default_instance_panel) + end + end +end diff --git a/test/pleroma/web/admin_api/controllers/invite_controller_test.exs b/test/pleroma/web/admin_api/controllers/invite_controller_test.exs @@ -0,0 +1,281 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.InviteControllerTest do + use Pleroma.Web.ConnCase, async: true + + import Pleroma.Factory + + alias Pleroma.Config + alias Pleroma.Repo + alias Pleroma.UserInviteToken + + 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 "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) + + test "sends invitation and returns 204", %{admin: admin, conn: conn} do + recipient_email = "foo@bar.com" + recipient_name = "J. D." + + conn = + conn + |> put_req_header("content-type", "application/json;charset=utf-8") + |> post("/api/pleroma/admin/users/email_invite", %{ + email: recipient_email, + name: recipient_name + }) + + assert json_response_and_validate_schema(conn, :no_content) + + token_record = List.last(Repo.all(Pleroma.UserInviteToken)) + assert token_record + refute token_record.used + + notify_email = Config.get([:instance, :notify_email]) + instance_name = Config.get([:instance, :name]) + + email = + Pleroma.Emails.UserEmail.user_invitation_email( + admin, + token_record, + recipient_email, + recipient_name + ) + + Swoosh.TestAssertions.assert_email_sent( + from: {instance_name, notify_email}, + to: {recipient_name, recipient_email}, + html_body: email.html_body + ) + end + + test "it returns 403 if requested by a non-admin" do + non_admin_user = insert(:user) + token = insert(:oauth_token, user: non_admin_user) + + conn = + build_conn() + |> assign(:user, non_admin_user) + |> assign(:token, token) + |> put_req_header("content-type", "application/json;charset=utf-8") + |> post("/api/pleroma/admin/users/email_invite", %{ + email: "foo@bar.com", + name: "JD" + }) + + assert json_response(conn, :forbidden) + end + + test "email with +", %{conn: conn, admin: admin} do + recipient_email = "foo+bar@baz.com" + + conn + |> put_req_header("content-type", "application/json;charset=utf-8") + |> post("/api/pleroma/admin/users/email_invite", %{email: recipient_email}) + |> json_response_and_validate_schema(:no_content) + + token_record = + Pleroma.UserInviteToken + |> Repo.all() + |> List.last() + + assert token_record + refute token_record.used + + notify_email = Config.get([:instance, :notify_email]) + instance_name = Config.get([:instance, :name]) + + email = + Pleroma.Emails.UserEmail.user_invitation_email( + admin, + token_record, + recipient_email + ) + + Swoosh.TestAssertions.assert_email_sent( + from: {instance_name, notify_email}, + to: recipient_email, + html_body: email.html_body + ) + end + 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]) + + test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn} do + Config.put([:instance, :registrations_open], false) + Config.put([:instance, :invites_enabled], false) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/email_invite", %{ + email: "foo@bar.com", + name: "JD" + }) + + assert json_response_and_validate_schema(conn, :bad_request) == + %{ + "error" => + "To send invites you need to set the `invites_enabled` option to true." + } + end + + test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do + Config.put([:instance, :registrations_open], true) + Config.put([:instance, :invites_enabled], true) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/email_invite", %{ + email: "foo@bar.com", + name: "JD" + }) + + assert json_response_and_validate_schema(conn, :bad_request) == + %{ + "error" => + "To send invites you need to set the `registrations_open` option to false." + } + end + end + + describe "POST /api/pleroma/admin/users/invite_token" do + test "without options", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/invite_token") + + invite_json = json_response_and_validate_schema(conn, 200) + invite = UserInviteToken.find_by_token!(invite_json["token"]) + refute invite.used + refute invite.expires_at + refute invite.max_use + assert invite.invite_type == "one_time" + end + + test "with expires_at", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/invite_token", %{ + "expires_at" => Date.to_string(Date.utc_today()) + }) + + invite_json = json_response_and_validate_schema(conn, 200) + invite = UserInviteToken.find_by_token!(invite_json["token"]) + + refute invite.used + assert invite.expires_at == Date.utc_today() + refute invite.max_use + assert invite.invite_type == "date_limited" + end + + test "with max_use", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/invite_token", %{"max_use" => 150}) + + invite_json = json_response_and_validate_schema(conn, 200) + invite = UserInviteToken.find_by_token!(invite_json["token"]) + refute invite.used + refute invite.expires_at + assert invite.max_use == 150 + assert invite.invite_type == "reusable" + end + + test "with max use and expires_at", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/invite_token", %{ + "max_use" => 150, + "expires_at" => Date.to_string(Date.utc_today()) + }) + + invite_json = json_response_and_validate_schema(conn, 200) + invite = UserInviteToken.find_by_token!(invite_json["token"]) + refute invite.used + assert invite.expires_at == Date.utc_today() + assert invite.max_use == 150 + assert invite.invite_type == "reusable_date_limited" + end + end + + describe "GET /api/pleroma/admin/users/invites" do + test "no invites", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/users/invites") + + assert json_response_and_validate_schema(conn, 200) == %{"invites" => []} + end + + test "with invite", %{conn: conn} do + {:ok, invite} = UserInviteToken.create_invite() + + conn = get(conn, "/api/pleroma/admin/users/invites") + + assert json_response_and_validate_schema(conn, 200) == %{ + "invites" => [ + %{ + "expires_at" => nil, + "id" => invite.id, + "invite_type" => "one_time", + "max_use" => nil, + "token" => invite.token, + "used" => false, + "uses" => 0 + } + ] + } + end + end + + describe "POST /api/pleroma/admin/users/revoke_invite" do + test "with token", %{conn: conn} do + {:ok, invite} = UserInviteToken.create_invite() + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token}) + + assert json_response_and_validate_schema(conn, 200) == %{ + "expires_at" => nil, + "id" => invite.id, + "invite_type" => "one_time", + "max_use" => nil, + "token" => invite.token, + "used" => true, + "uses" => 0 + } + end + + test "with invalid token", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"}) + + assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} + end + end +end 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 @@ -0,0 +1,167 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.MediaProxyCacheControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + import Mock + + alias Pleroma.Web.MediaProxy + + setup do: clear_config([:media_proxy]) + + setup do + on_exit(fn -> Cachex.clear(:banned_urls_cache) end) + end + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + Config.put([:media_proxy, :enabled], true) + Config.put([:media_proxy, :invalidation, :enabled], true) + Config.put([:media_proxy, :invalidation, :provider], MediaProxy.Invalidation.Script) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "GET /api/pleroma/admin/media_proxy_caches" do + test "shows banned MediaProxy URLs", %{conn: conn} do + MediaProxy.put_in_banned_urls([ + "http://localhost:4001/media/a688346.jpg", + "http://localhost:4001/media/fb1f4d.jpg" + ]) + + MediaProxy.put_in_banned_urls("http://localhost:4001/media/gb1f44.jpg") + MediaProxy.put_in_banned_urls("http://localhost:4001/media/tb13f47.jpg") + MediaProxy.put_in_banned_urls("http://localhost:4001/media/wb1f46.jpg") + + response = + conn + |> get("/api/pleroma/admin/media_proxy_caches?page_size=2") + |> json_response_and_validate_schema(200) + + assert response["page_size"] == 2 + assert response["count"] == 5 + + assert response["urls"] == [ + "http://localhost:4001/media/fb1f4d.jpg", + "http://localhost:4001/media/a688346.jpg" + ] + + response = + conn + |> get("/api/pleroma/admin/media_proxy_caches?page_size=2&page=2") + |> json_response_and_validate_schema(200) + + assert response["urls"] == [ + "http://localhost:4001/media/gb1f44.jpg", + "http://localhost:4001/media/tb13f47.jpg" + ] + + assert response["page_size"] == 2 + assert response["count"] == 5 + + response = + conn + |> get("/api/pleroma/admin/media_proxy_caches?page_size=2&page=3") + |> json_response_and_validate_schema(200) + + assert response["urls"] == ["http://localhost:4001/media/wb1f46.jpg"] + end + + test "search banned MediaProxy URLs", %{conn: conn} do + MediaProxy.put_in_banned_urls([ + "http://localhost:4001/media/a688346.jpg", + "http://localhost:4001/media/ff44b1f4d.jpg" + ]) + + MediaProxy.put_in_banned_urls("http://localhost:4001/media/gb1f44.jpg") + MediaProxy.put_in_banned_urls("http://localhost:4001/media/tb13f47.jpg") + MediaProxy.put_in_banned_urls("http://localhost:4001/media/wb1f46.jpg") + + response = + conn + |> get("/api/pleroma/admin/media_proxy_caches?page_size=2&query=F44") + |> json_response_and_validate_schema(200) + + assert response["urls"] == [ + "http://localhost:4001/media/gb1f44.jpg", + "http://localhost:4001/media/ff44b1f4d.jpg" + ] + + assert response["page_size"] == 2 + assert response["count"] == 2 + end + end + + describe "POST /api/pleroma/admin/media_proxy_caches/delete" do + test "deleted MediaProxy URLs from banned", %{conn: conn} do + MediaProxy.put_in_banned_urls([ + "http://localhost:4001/media/a688346.jpg", + "http://localhost:4001/media/fb1f4d.jpg" + ]) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/media_proxy_caches/delete", %{ + urls: ["http://localhost:4001/media/a688346.jpg"] + }) + |> json_response_and_validate_schema(200) + + refute MediaProxy.in_banned_urls("http://localhost:4001/media/a688346.jpg") + assert MediaProxy.in_banned_urls("http://localhost:4001/media/fb1f4d.jpg") + end + end + + describe "POST /api/pleroma/admin/media_proxy_caches/purge" do + test "perform invalidates cache of MediaProxy", %{conn: conn} do + urls = [ + "http://example.com/media/a688346.jpg", + "http://example.com/media/fb1f4d.jpg" + ] + + with_mocks [ + {MediaProxy.Invalidation.Script, [], + [ + purge: fn _, _ -> {"ok", 0} end + ]} + ] do + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/media_proxy_caches/purge", %{urls: urls, ban: false}) + |> json_response_and_validate_schema(200) + + refute MediaProxy.in_banned_urls("http://example.com/media/a688346.jpg") + refute MediaProxy.in_banned_urls("http://example.com/media/fb1f4d.jpg") + end + end + + test "perform invalidates cache of MediaProxy and adds url to banned", %{conn: conn} do + urls = [ + "http://example.com/media/a688346.jpg", + "http://example.com/media/fb1f4d.jpg" + ] + + with_mocks [{MediaProxy.Invalidation.Script, [], [purge: fn _, _ -> {"ok", 0} end]}] do + conn + |> put_req_header("content-type", "application/json") + |> post( + "/api/pleroma/admin/media_proxy_caches/purge", + %{urls: urls, ban: true} + ) + |> json_response_and_validate_schema(200) + + assert MediaProxy.in_banned_urls("http://example.com/media/a688346.jpg") + assert MediaProxy.in_banned_urls("http://example.com/media/fb1f4d.jpg") + end + end + end +end 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 @@ -0,0 +1,220 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.OAuthAppControllerTest do + use Pleroma.Web.ConnCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Config + alias Pleroma.Web + + 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 "POST /api/pleroma/admin/oauth_app" do + test "errors", %{conn: conn} do + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/oauth_app", %{}) + |> json_response_and_validate_schema(400) + + assert %{ + "error" => "Missing field: name. Missing field: redirect_uris." + } = response + end + + test "success", %{conn: conn} do + base_url = Web.base_url() + app_name = "Trusted app" + + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/oauth_app", %{ + name: app_name, + redirect_uris: base_url + }) + |> json_response_and_validate_schema(200) + + assert %{ + "client_id" => _, + "client_secret" => _, + "name" => ^app_name, + "redirect_uri" => ^base_url, + "trusted" => false + } = response + end + + test "with trusted", %{conn: conn} do + base_url = Web.base_url() + app_name = "Trusted app" + + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/oauth_app", %{ + name: app_name, + redirect_uris: base_url, + trusted: true + }) + |> json_response_and_validate_schema(200) + + assert %{ + "client_id" => _, + "client_secret" => _, + "name" => ^app_name, + "redirect_uri" => ^base_url, + "trusted" => true + } = response + end + end + + describe "GET /api/pleroma/admin/oauth_app" do + setup do + app = insert(:oauth_app) + {:ok, app: app} + end + + test "list", %{conn: conn} do + response = + conn + |> get("/api/pleroma/admin/oauth_app") + |> json_response_and_validate_schema(200) + + assert %{"apps" => apps, "count" => count, "page_size" => _} = response + + assert length(apps) == count + end + + test "with page size", %{conn: conn} do + insert(:oauth_app) + page_size = 1 + + response = + conn + |> get("/api/pleroma/admin/oauth_app?page_size=#{page_size}") + |> json_response_and_validate_schema(200) + + assert %{"apps" => apps, "count" => _, "page_size" => ^page_size} = response + + assert length(apps) == page_size + end + + test "search by client name", %{conn: conn, app: app} do + response = + conn + |> get("/api/pleroma/admin/oauth_app?name=#{app.client_name}") + |> json_response_and_validate_schema(200) + + assert %{"apps" => [returned], "count" => _, "page_size" => _} = response + + assert returned["client_id"] == app.client_id + assert returned["name"] == app.client_name + end + + test "search by client id", %{conn: conn, app: app} do + response = + conn + |> get("/api/pleroma/admin/oauth_app?client_id=#{app.client_id}") + |> json_response_and_validate_schema(200) + + assert %{"apps" => [returned], "count" => _, "page_size" => _} = response + + assert returned["client_id"] == app.client_id + assert returned["name"] == app.client_name + end + + test "only trusted", %{conn: conn} do + app = insert(:oauth_app, trusted: true) + + response = + conn + |> get("/api/pleroma/admin/oauth_app?trusted=true") + |> json_response_and_validate_schema(200) + + assert %{"apps" => [returned], "count" => _, "page_size" => _} = response + + assert returned["client_id"] == app.client_id + assert returned["name"] == app.client_name + end + end + + describe "DELETE /api/pleroma/admin/oauth_app/:id" do + test "with id", %{conn: conn} do + app = insert(:oauth_app) + + response = + conn + |> delete("/api/pleroma/admin/oauth_app/" <> to_string(app.id)) + |> json_response_and_validate_schema(:no_content) + + assert response == "" + end + + test "with non existance id", %{conn: conn} do + response = + conn + |> delete("/api/pleroma/admin/oauth_app/0") + |> json_response_and_validate_schema(:bad_request) + + assert response == "" + end + end + + describe "PATCH /api/pleroma/admin/oauth_app/:id" do + test "with id", %{conn: conn} do + app = insert(:oauth_app) + + name = "another name" + url = "https://example.com" + scopes = ["admin"] + id = app.id + website = "http://website.com" + + response = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/oauth_app/#{id}", %{ + name: name, + trusted: true, + redirect_uris: url, + scopes: scopes, + website: website + }) + |> json_response_and_validate_schema(200) + + assert %{ + "client_id" => _, + "client_secret" => _, + "id" => ^id, + "name" => ^name, + "redirect_uri" => ^url, + "trusted" => true, + "website" => ^website + } = response + end + + test "without id", %{conn: conn} do + response = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/oauth_app/0") + |> json_response_and_validate_schema(:bad_request) + + assert response == "" + end + end +end diff --git a/test/pleroma/web/admin_api/controllers/relay_controller_test.exs b/test/pleroma/web/admin_api/controllers/relay_controller_test.exs @@ -0,0 +1,99 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.RelayControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Config + alias Pleroma.ModerationLog + alias Pleroma.Repo + alias Pleroma.User + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + + :ok + end + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "relays" do + test "POST /relay", %{conn: conn, admin: admin} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/relay", %{ + relay_url: "http://mastodon.example.org/users/admin" + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "actor" => "http://mastodon.example.org/users/admin", + "followed_back" => false + } + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" + end + + test "GET /relay", %{conn: conn} do + relay_user = Pleroma.Web.ActivityPub.Relay.get_actor() + + ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"] + |> Enum.each(fn ap_id -> + {:ok, user} = User.get_or_fetch_by_ap_id(ap_id) + User.follow(relay_user, user) + end) + + conn = get(conn, "/api/pleroma/admin/relay") + + assert json_response_and_validate_schema(conn, 200)["relays"] == [ + %{ + "actor" => "http://mastodon.example.org/users/admin", + "followed_back" => true + }, + %{"actor" => "https://mstdn.io/users/mayuutann", "followed_back" => true} + ] + end + + test "DELETE /relay", %{conn: conn, admin: admin} do + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/relay", %{ + relay_url: "http://mastodon.example.org/users/admin" + }) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/admin/relay", %{ + relay_url: "http://mastodon.example.org/users/admin" + }) + + assert json_response_and_validate_schema(conn, 200) == + "http://mastodon.example.org/users/admin" + + [log_entry_one, log_entry_two] = Repo.all(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry_one) == + "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" + + assert ModerationLog.get_log_entry_message(log_entry_two) == + "@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin" + end + end +end diff --git a/test/pleroma/web/admin_api/controllers/report_controller_test.exs b/test/pleroma/web/admin_api/controllers/report_controller_test.exs @@ -0,0 +1,372 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ReportControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.ModerationLog + alias Pleroma.Repo + alias Pleroma.ReportNote + alias Pleroma.Web.CommonAPI + + 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/reports/:id" do + test "returns report by its id", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + response = + conn + |> get("/api/pleroma/admin/reports/#{report_id}") + |> json_response_and_validate_schema(:ok) + + assert response["id"] == report_id + end + + test "returns 404 when report id is invalid", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/reports/test") + + assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} + end + end + + describe "PATCH /api/pleroma/admin/reports" do + setup do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + {:ok, %{id: second_report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel very offended", + status_ids: [activity.id] + }) + + %{ + id: report_id, + second_report_id: second_report_id + } + 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"]) + + response = + conn + |> assign(:token, read_token) + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [%{"state" => "resolved", "id" => id}] + }) + |> json_response_and_validate_schema(403) + + assert response == %{ + "error" => "Insufficient permissions: admin:write:reports." + } + + conn + |> assign(:token, write_token) + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [%{"state" => "resolved", "id" => id}] + }) + |> json_response_and_validate_schema(:no_content) + end + + test "mark report as resolved", %{conn: conn, id: id, admin: admin} do + 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(id) + assert activity.data["state"] == "resolved" + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated report ##{id} with 'resolved' state" + end + + test "closes report", %{conn: conn, id: id, admin: admin} do + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [ + %{"state" => "closed", "id" => id} + ] + }) + |> json_response_and_validate_schema(:no_content) + + activity = Activity.get_by_id(id) + assert activity.data["state"] == "closed" + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated report ##{id} with 'closed' state" + end + + test "returns 400 when state is unknown", %{conn: conn, id: id} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [ + %{"state" => "test", "id" => id} + ] + }) + + assert "Unsupported state" = + hd(json_response_and_validate_schema(conn, :bad_request))["error"] + end + + test "returns 404 when report is not exist", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [ + %{"state" => "closed", "id" => "test"} + ] + }) + + assert hd(json_response_and_validate_schema(conn, :bad_request))["error"] == "not_found" + end + + test "updates state of multiple reports", %{ + conn: conn, + id: id, + admin: admin, + second_report_id: second_report_id + } do + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [ + %{"state" => "resolved", "id" => id}, + %{"state" => "closed", "id" => second_report_id} + ] + }) + |> json_response_and_validate_schema(:no_content) + + activity = Activity.get_by_id(id) + second_activity = Activity.get_by_id(second_report_id) + assert activity.data["state"] == "resolved" + assert second_activity.data["state"] == "closed" + + [first_log_entry, second_log_entry] = Repo.all(ModerationLog) + + assert ModerationLog.get_log_entry_message(first_log_entry) == + "@#{admin.nickname} updated report ##{id} with 'resolved' state" + + assert ModerationLog.get_log_entry_message(second_log_entry) == + "@#{admin.nickname} updated report ##{second_report_id} with 'closed' state" + end + end + + describe "GET /api/pleroma/admin/reports" do + test "returns empty response when no reports created", %{conn: conn} do + response = + conn + |> get(report_path(conn, :index)) + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response["reports"]) + assert response["total"] == 0 + end + + test "returns reports", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + response = + conn + |> get(report_path(conn, :index)) + |> json_response_and_validate_schema(:ok) + + [report] = response["reports"] + + assert length(response["reports"]) == 1 + assert report["id"] == report_id + + assert response["total"] == 1 + end + + test "returns reports with specified state", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: first_report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + {:ok, %{id: second_report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I don't like this user" + }) + + CommonAPI.update_report_state(second_report_id, "closed") + + response = + conn + |> get(report_path(conn, :index, %{state: "open"})) + |> json_response_and_validate_schema(:ok) + + assert [open_report] = response["reports"] + + assert length(response["reports"]) == 1 + assert open_report["id"] == first_report_id + + assert response["total"] == 1 + + response = + conn + |> get(report_path(conn, :index, %{state: "closed"})) + |> json_response_and_validate_schema(:ok) + + assert [closed_report] = response["reports"] + + assert length(response["reports"]) == 1 + assert closed_report["id"] == second_report_id + + assert response["total"] == 1 + + assert %{"total" => 0, "reports" => []} == + conn + |> get(report_path(conn, :index, %{state: "resolved"})) + |> json_response_and_validate_schema(:ok) + end + + test "returns 403 when requested by a non-admin" do + user = insert(:user) + token = insert(:oauth_token, user: user) + + conn = + build_conn() + |> assign(:user, user) + |> assign(:token, token) + |> get("/api/pleroma/admin/reports") + + assert json_response(conn, :forbidden) == + %{"error" => "User is not an admin."} + end + + test "returns 403 when requested by anonymous" do + conn = get(build_conn(), "/api/pleroma/admin/reports") + + assert json_response(conn, :forbidden) == %{ + "error" => "Invalid credentials." + } + end + end + + describe "POST /api/pleroma/admin/reports/:id/notes" do + setup %{conn: conn, admin: admin} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ + content: "this is disgusting!" + }) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ + content: "this is disgusting2!" + }) + + %{ + admin_id: admin.id, + report_id: report_id + } + end + + test "it creates report note", %{admin_id: admin_id, report_id: report_id} do + assert [note, _] = Repo.all(ReportNote) + + assert %{ + activity_id: ^report_id, + content: "this is disgusting!", + user_id: ^admin_id + } = note + end + + test "it returns reports with notes", %{conn: conn, admin: admin} do + conn = get(conn, "/api/pleroma/admin/reports") + + response = json_response_and_validate_schema(conn, 200) + notes = hd(response["reports"])["notes"] + [note, _] = notes + + assert note["user"]["nickname"] == admin.nickname + assert note["content"] == "this is disgusting!" + assert note["created_at"] + assert response["total"] == 1 + end + + test "it deletes the note", %{conn: conn, report_id: report_id} do + assert ReportNote |> Repo.all() |> length() == 2 + assert [note, _] = Repo.all(ReportNote) + + delete(conn, "/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}") + + assert ReportNote |> Repo.all() |> length() == 1 + 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 @@ -0,0 +1,202 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.StatusControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.ModerationLog + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + 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/statuses/:id" do + test "not found", %{conn: conn} do + assert conn + |> get("/api/pleroma/admin/statuses/not_found") + |> json_response_and_validate_schema(:not_found) + end + + test "shows activity", %{conn: conn} do + activity = insert(:note_activity) + + response = + conn + |> get("/api/pleroma/admin/statuses/#{activity.id}") + |> json_response_and_validate_schema(200) + + assert response["id"] == activity.id + + account = response["account"] + actor = User.get_by_ap_id(activity.actor) + + assert account["id"] == actor.id + assert account["nickname"] == actor.nickname + assert account["deactivated"] == actor.deactivated + assert account["confirmation_pending"] == actor.confirmation_pending + end + end + + describe "PUT /api/pleroma/admin/statuses/:id" do + setup do + activity = insert(:note_activity) + + %{id: activity.id} + end + + test "toggle sensitive flag", %{conn: conn, id: id, admin: admin} do + response = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"}) + |> json_response_and_validate_schema(:ok) + + assert response["sensitive"] + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated status ##{id}, set sensitive: 'true'" + + response = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"}) + |> json_response_and_validate_schema(:ok) + + refute response["sensitive"] + end + + test "change visibility flag", %{conn: conn, id: id, admin: admin} do + response = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "public"}) + |> json_response_and_validate_schema(:ok) + + assert response["visibility"] == "public" + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated status ##{id}, set visibility: 'public'" + + response = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "private"}) + |> json_response_and_validate_schema(:ok) + + assert response["visibility"] == "private" + + response = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "unlisted"}) + |> json_response_and_validate_schema(:ok) + + assert response["visibility"] == "unlisted" + end + + test "returns 400 when visibility is unknown", %{conn: conn, id: id} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "test"}) + + assert %{"error" => "test - Invalid value for enum."} = + json_response_and_validate_schema(conn, :bad_request) + end + end + + describe "DELETE /api/pleroma/admin/statuses/:id" do + setup do + activity = insert(:note_activity) + + %{id: activity.id} + end + + test "deletes status", %{conn: conn, id: id, admin: admin} do + conn + |> delete("/api/pleroma/admin/statuses/#{id}") + |> json_response_and_validate_schema(:ok) + + refute Activity.get_by_id(id) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deleted status ##{id}" + end + + test "returns 404 when the status does not exist", %{conn: conn} do + conn = delete(conn, "/api/pleroma/admin/statuses/test") + + assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} + end + end + + describe "GET /api/pleroma/admin/statuses" do + test "returns all public and unlisted statuses", %{conn: conn, admin: admin} do + blocked = insert(:user) + user = insert(:user) + User.block(admin, blocked) + + {:ok, _} = CommonAPI.post(user, %{status: "@#{admin.nickname}", visibility: "direct"}) + + {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "private"}) + {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + {:ok, _} = CommonAPI.post(blocked, %{status: ".", visibility: "public"}) + + response = + conn + |> get("/api/pleroma/admin/statuses") + |> json_response_and_validate_schema(200) + + refute "private" in Enum.map(response, & &1["visibility"]) + assert length(response) == 3 + end + + test "returns only local statuses with local_only on", %{conn: conn} do + user = insert(:user) + remote_user = insert(:user, local: false, nickname: "archaeme@archae.me") + insert(:note_activity, user: user, local: true) + insert(:note_activity, user: remote_user, local: false) + + response = + conn + |> get("/api/pleroma/admin/statuses?local_only=true") + |> json_response_and_validate_schema(200) + + assert length(response) == 1 + end + + test "returns private and direct statuses with godmode on", %{conn: conn, admin: admin} do + user = insert(:user) + + {:ok, _} = CommonAPI.post(user, %{status: "@#{admin.nickname}", visibility: "direct"}) + + {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "private"}) + {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + conn = get(conn, "/api/pleroma/admin/statuses?godmode=true") + assert json_response_and_validate_schema(conn, 200) |> length() == 3 + end + end +end diff --git a/test/pleroma/web/admin_api/search_test.exs b/test/pleroma/web/admin_api/search_test.exs @@ -0,0 +1,190 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.SearchTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.AdminAPI.Search + + import Pleroma.Factory + + describe "search for admin" do + test "it ignores case" do + insert(:user, nickname: "papercoach") + insert(:user, nickname: "CanadaPaperCoach") + + {:ok, _results, count} = + Search.user(%{ + query: "paper", + local: false, + page: 1, + page_size: 50 + }) + + assert count == 2 + end + + test "it returns local/external users" do + insert(:user, local: true) + insert(:user, local: false) + insert(:user, local: false) + + {:ok, _results, local_count} = + Search.user(%{ + query: "", + local: true + }) + + {:ok, _results, external_count} = + Search.user(%{ + query: "", + external: true + }) + + assert local_count == 1 + assert external_count == 2 + end + + test "it returns active/deactivated users" do + insert(:user, deactivated: true) + insert(:user, deactivated: true) + insert(:user, deactivated: false) + + {:ok, _results, active_count} = + Search.user(%{ + query: "", + active: true + }) + + {:ok, _results, deactivated_count} = + Search.user(%{ + query: "", + deactivated: true + }) + + assert active_count == 1 + assert deactivated_count == 2 + end + + test "it returns specific user" do + insert(:user) + insert(:user) + user = insert(:user, nickname: "bob", local: true, deactivated: false) + + {:ok, _results, total_count} = Search.user(%{query: ""}) + + {:ok, [^user], count} = + Search.user(%{ + query: "Bo", + active: true, + local: true + }) + + assert total_count == 3 + assert count == 1 + end + + test "it returns user by domain" do + insert(:user) + insert(:user) + user = insert(:user, nickname: "some@domain.com") + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{query: "domain.com"}) + assert total == 3 + assert count == 1 + end + + test "it return user by full nickname" do + insert(:user) + insert(:user) + user = insert(:user, nickname: "some@domain.com") + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{query: "some@domain.com"}) + assert total == 3 + assert count == 1 + end + + test "it returns admin user" do + admin = insert(:user, is_admin: true) + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^admin], count} = Search.user(%{is_admin: true}) + assert total == 3 + assert count == 1 + end + + test "it returns moderator user" do + moderator = insert(:user, is_moderator: true) + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^moderator], count} = Search.user(%{is_moderator: true}) + assert total == 3 + assert count == 1 + end + + test "it returns users with tags" do + user1 = insert(:user, tags: ["first"]) + user2 = insert(:user, tags: ["second"]) + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, users, count} = Search.user(%{tags: ["first", "second"]}) + assert total == 4 + assert count == 2 + assert user1 in users + assert user2 in users + end + + test "it returns user by display name" do + user = insert(:user, name: "Display name") + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{name: "display"}) + + assert total == 3 + assert count == 1 + end + + test "it returns user by email" do + user = insert(:user, email: "some@example.com") + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{email: "some@example.com"}) + + assert total == 3 + assert count == 1 + end + + test "it returns unapproved user" do + unapproved = insert(:user, approval_pending: true) + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^unapproved], count} = Search.user(%{need_approval: true}) + assert total == 3 + assert count == 1 + end + + test "it returns non-discoverable users" do + insert(:user) + insert(:user, discoverable: false) + + {:ok, _results, total} = Search.user() + + assert total == 2 + end + end +end diff --git a/test/pleroma/web/admin_api/views/report_view_test.exs b/test/pleroma/web/admin_api/views/report_view_test.exs @@ -0,0 +1,146 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ReportViewTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Web.AdminAPI + alias Pleroma.Web.AdminAPI.Report + alias Pleroma.Web.AdminAPI.ReportView + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI + alias Pleroma.Web.MastodonAPI.StatusView + + test "renders a report" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.report(user, %{account_id: other_user.id}) + + expected = %{ + content: nil, + actor: + Map.merge( + MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true}), + AdminAPI.AccountView.render("show.json", %{user: user}) + ), + account: + Map.merge( + MastodonAPI.AccountView.render("show.json", %{ + user: other_user, + skip_visibility_check: true + }), + AdminAPI.AccountView.render("show.json", %{user: other_user}) + ), + statuses: [], + notes: [], + state: "open", + id: activity.id + } + + result = + ReportView.render("show.json", Report.extract_report_info(activity)) + |> Map.delete(:created_at) + + assert result == expected + end + + test "includes reported statuses" do + user = insert(:user) + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(other_user, %{status: "toot"}) + + {:ok, report_activity} = + CommonAPI.report(user, %{account_id: other_user.id, status_ids: [activity.id]}) + + other_user = Pleroma.User.get_by_id(other_user.id) + + expected = %{ + content: nil, + actor: + Map.merge( + MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true}), + AdminAPI.AccountView.render("show.json", %{user: user}) + ), + account: + Map.merge( + MastodonAPI.AccountView.render("show.json", %{ + user: other_user, + skip_visibility_check: true + }), + AdminAPI.AccountView.render("show.json", %{user: other_user}) + ), + statuses: [StatusView.render("show.json", %{activity: activity})], + state: "open", + notes: [], + id: report_activity.id + } + + result = + ReportView.render("show.json", Report.extract_report_info(report_activity)) + |> Map.delete(:created_at) + + assert result == expected + end + + test "renders report's state" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.report(user, %{account_id: other_user.id}) + {:ok, activity} = CommonAPI.update_report_state(activity.id, "closed") + + assert %{state: "closed"} = + ReportView.render("show.json", Report.extract_report_info(activity)) + end + + test "renders report description" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.report(user, %{ + account_id: other_user.id, + comment: "posts are too good for this instance" + }) + + assert %{content: "posts are too good for this instance"} = + ReportView.render("show.json", Report.extract_report_info(activity)) + end + + test "sanitizes report description" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.report(user, %{ + account_id: other_user.id, + comment: "" + }) + + data = Map.put(activity.data, "content", "<script> alert('hecked :D:D:D:D:D:D:D') </script>") + activity = Map.put(activity, :data, data) + + refute "<script> alert('hecked :D:D:D:D:D:D:D') </script>" == + ReportView.render("show.json", Report.extract_report_info(activity))[:content] + end + + test "doesn't error out when the user doesn't exists" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.report(user, %{ + account_id: other_user.id, + comment: "" + }) + + Pleroma.User.delete(other_user) + Pleroma.User.invalidate_cache(other_user) + + assert %{} = ReportView.render("show.json", Report.extract_report_info(activity)) + end +end diff --git a/test/pleroma/web/api_spec/schema_examples_test.exs b/test/pleroma/web/api_spec/schema_examples_test.exs @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.SchemaExamplesTest do + use ExUnit.Case, async: true + import Pleroma.Tests.ApiSpecHelpers + + @content_type "application/json" + + for operation <- api_operations() do + describe operation.operationId <> " Request Body" do + if operation.requestBody do + @media_type operation.requestBody.content[@content_type] + @schema resolve_schema(@media_type.schema) + + if @media_type.example do + test "request body media type example matches schema" do + assert_schema(@media_type.example, @schema) + end + end + + if @schema.example do + test "request body schema example matches schema" do + assert_schema(@schema.example, @schema) + end + end + end + end + + for {status, response} <- operation.responses, is_map(response.content[@content_type]) do + describe "#{operation.operationId} - #{status} Response" do + @schema resolve_schema(response.content[@content_type].schema) + + if @schema.example do + test "example matches schema" do + assert_schema(@schema.example, @schema) + end + end + end + end + end +end diff --git a/test/pleroma/web/auth/auth_controller_test.exs b/test/pleroma/web/auth/auth_controller_test.exs @@ -0,0 +1,242 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Auth.AuthControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + describe "do_oauth_check" do + test "serves with proper OAuth token (fulfilling requested scopes)" do + %{conn: good_token_conn, user: user} = oauth_access(["read"]) + + assert %{"user_id" => user.id} == + good_token_conn + |> get("/test/authenticated_api/do_oauth_check") + |> json_response(200) + + # Unintended usage (:api) — use with :authenticated_api instead + assert %{"user_id" => user.id} == + good_token_conn + |> get("/test/api/do_oauth_check") + |> json_response(200) + end + + test "fails on no token / missing scope(s)" do + %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"]) + + bad_token_conn + |> get("/test/authenticated_api/do_oauth_check") + |> json_response(403) + + bad_token_conn + |> assign(:token, nil) + |> get("/test/api/do_oauth_check") + |> json_response(403) + end + end + + describe "fallback_oauth_check" do + test "serves with proper OAuth token (fulfilling requested scopes)" do + %{conn: good_token_conn, user: user} = oauth_access(["read"]) + + assert %{"user_id" => user.id} == + good_token_conn + |> get("/test/api/fallback_oauth_check") + |> json_response(200) + + # Unintended usage (:authenticated_api) — use with :api instead + assert %{"user_id" => user.id} == + good_token_conn + |> get("/test/authenticated_api/fallback_oauth_check") + |> json_response(200) + end + + test "for :api on public instance, drops :user and renders on no token / missing scope(s)" do + clear_config([:instance, :public], true) + + %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"]) + + assert %{"user_id" => nil} == + bad_token_conn + |> get("/test/api/fallback_oauth_check") + |> json_response(200) + + assert %{"user_id" => nil} == + bad_token_conn + |> assign(:token, nil) + |> get("/test/api/fallback_oauth_check") + |> json_response(200) + end + + test "for :api on private instance, fails on no token / missing scope(s)" do + clear_config([:instance, :public], false) + + %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"]) + + bad_token_conn + |> get("/test/api/fallback_oauth_check") + |> json_response(403) + + bad_token_conn + |> assign(:token, nil) + |> get("/test/api/fallback_oauth_check") + |> json_response(403) + end + end + + describe "skip_oauth_check" do + test "for :authenticated_api, serves if :user is set (regardless of token / token scopes)" do + user = insert(:user) + + assert %{"user_id" => user.id} == + build_conn() + |> assign(:user, user) + |> get("/test/authenticated_api/skip_oauth_check") + |> json_response(200) + + %{conn: bad_token_conn, user: user} = oauth_access(["irrelevant_scope"]) + + assert %{"user_id" => user.id} == + bad_token_conn + |> get("/test/authenticated_api/skip_oauth_check") + |> json_response(200) + end + + test "serves via :api on public instance if :user is not set" do + clear_config([:instance, :public], true) + + assert %{"user_id" => nil} == + build_conn() + |> get("/test/api/skip_oauth_check") + |> json_response(200) + + build_conn() + |> get("/test/authenticated_api/skip_oauth_check") + |> json_response(403) + end + + test "fails on private instance if :user is not set" do + clear_config([:instance, :public], false) + + build_conn() + |> get("/test/api/skip_oauth_check") + |> json_response(403) + + build_conn() + |> get("/test/authenticated_api/skip_oauth_check") + |> json_response(403) + end + end + + describe "fallback_oauth_skip_publicity_check" do + test "serves with proper OAuth token (fulfilling requested scopes)" do + %{conn: good_token_conn, user: user} = oauth_access(["read"]) + + assert %{"user_id" => user.id} == + good_token_conn + |> get("/test/api/fallback_oauth_skip_publicity_check") + |> json_response(200) + + # Unintended usage (:authenticated_api) + assert %{"user_id" => user.id} == + good_token_conn + |> get("/test/authenticated_api/fallback_oauth_skip_publicity_check") + |> json_response(200) + end + + test "for :api on private / public instance, drops :user and renders on token issue" do + %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"]) + + for is_public <- [true, false] do + clear_config([:instance, :public], is_public) + + assert %{"user_id" => nil} == + bad_token_conn + |> get("/test/api/fallback_oauth_skip_publicity_check") + |> json_response(200) + + assert %{"user_id" => nil} == + bad_token_conn + |> assign(:token, nil) + |> get("/test/api/fallback_oauth_skip_publicity_check") + |> json_response(200) + end + end + end + + describe "skip_oauth_skip_publicity_check" do + test "for :authenticated_api, serves if :user is set (regardless of token / token scopes)" do + user = insert(:user) + + assert %{"user_id" => user.id} == + build_conn() + |> assign(:user, user) + |> get("/test/authenticated_api/skip_oauth_skip_publicity_check") + |> json_response(200) + + %{conn: bad_token_conn, user: user} = oauth_access(["irrelevant_scope"]) + + assert %{"user_id" => user.id} == + bad_token_conn + |> get("/test/authenticated_api/skip_oauth_skip_publicity_check") + |> json_response(200) + end + + test "for :api, serves on private and public instances regardless of whether :user is set" do + user = insert(:user) + + for is_public <- [true, false] do + clear_config([:instance, :public], is_public) + + assert %{"user_id" => nil} == + build_conn() + |> get("/test/api/skip_oauth_skip_publicity_check") + |> json_response(200) + + assert %{"user_id" => user.id} == + build_conn() + |> assign(:user, user) + |> get("/test/api/skip_oauth_skip_publicity_check") + |> json_response(200) + end + end + end + + describe "missing_oauth_check_definition" do + def test_missing_oauth_check_definition_failure(endpoint, expected_error) do + %{conn: conn} = oauth_access(["read", "write", "follow", "push", "admin"]) + + assert %{"error" => expected_error} == + conn + |> get(endpoint) + |> json_response(403) + end + + test "fails if served via :authenticated_api" do + test_missing_oauth_check_definition_failure( + "/test/authenticated_api/missing_oauth_check_definition", + "Security violation: OAuth scopes check was neither handled nor explicitly skipped." + ) + end + + test "fails if served via :api and the instance is private" do + clear_config([:instance, :public], false) + + test_missing_oauth_check_definition_failure( + "/test/api/missing_oauth_check_definition", + "This resource requires authentication." + ) + end + + test "succeeds with dropped :user if served via :api on public instance" do + %{conn: conn} = oauth_access(["read", "write", "follow", "push", "admin"]) + + assert %{"user_id" => nil} == + conn + |> get("/test/api/missing_oauth_check_definition") + |> json_response(200) + end + end +end diff --git a/test/pleroma/web/auth/authenticator_test.exs b/test/pleroma/web/auth/authenticator_test.exs @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Auth.AuthenticatorTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.Auth.Authenticator + import Pleroma.Factory + + describe "fetch_user/1" do + test "returns user by name" do + user = insert(:user) + assert Authenticator.fetch_user(user.nickname) == user + end + + test "returns user by email" do + user = insert(:user) + assert Authenticator.fetch_user(user.email) == user + end + + test "returns nil" do + assert Authenticator.fetch_user("email") == nil + end + end + + describe "fetch_credentials/1" do + test "returns name and password from authorization params" do + params = %{"authorization" => %{"name" => "test", "password" => "test-pass"}} + assert Authenticator.fetch_credentials(params) == {:ok, {"test", "test-pass"}} + end + + test "returns name and password with grant_type 'password'" do + params = %{"grant_type" => "password", "username" => "test", "password" => "test-pass"} + assert Authenticator.fetch_credentials(params) == {:ok, {"test", "test-pass"}} + end + + test "returns error" do + assert Authenticator.fetch_credentials(%{}) == {:error, :invalid_credentials} + end + end +end diff --git a/test/pleroma/web/auth/basic_auth_test.exs b/test/pleroma/web/auth/basic_auth_test.exs @@ -0,0 +1,46 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Auth.BasicAuthTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + test "with HTTP Basic Auth used, grants access to OAuth scope-restricted endpoints", %{ + conn: conn + } do + user = insert(:user) + assert Pbkdf2.verify_pass("test", user.password_hash) + + basic_auth_contents = + (URI.encode_www_form(user.nickname) <> ":" <> URI.encode_www_form("test")) + |> Base.encode64() + + # Succeeds with HTTP Basic Auth + response = + conn + |> put_req_header("authorization", "Basic " <> basic_auth_contents) + |> get("/api/v1/accounts/verify_credentials") + |> json_response(200) + + user_nickname = user.nickname + assert %{"username" => ^user_nickname} = response + + # Succeeds with a properly scoped OAuth token + valid_token = insert(:oauth_token, scopes: ["read:accounts"]) + + conn + |> put_req_header("authorization", "Bearer #{valid_token.token}") + |> get("/api/v1/accounts/verify_credentials") + |> json_response(200) + + # Fails with a wrong-scoped OAuth token (proof of restriction) + invalid_token = insert(:oauth_token, scopes: ["read:something"]) + + conn + |> put_req_header("authorization", "Bearer #{invalid_token.token}") + |> get("/api/v1/accounts/verify_credentials") + |> json_response(403) + end +end diff --git a/test/pleroma/web/auth/pleroma_authenticator_test.exs b/test/pleroma/web/auth/pleroma_authenticator_test.exs @@ -0,0 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Auth.PleromaAuthenticatorTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.Auth.PleromaAuthenticator + import Pleroma.Factory + + setup do + password = "testpassword" + name = "AgentSmith" + user = insert(:user, nickname: name, password_hash: Pbkdf2.hash_pwd_salt(password)) + {:ok, [user: user, name: name, password: password]} + end + + test "get_user/authorization", %{name: name, password: password} do + name = name <> "1" + user = insert(:user, nickname: name, password_hash: Bcrypt.hash_pwd_salt(password)) + + params = %{"authorization" => %{"name" => name, "password" => password}} + res = PleromaAuthenticator.get_user(%Plug.Conn{params: params}) + + assert {:ok, returned_user} = res + assert returned_user.id == user.id + assert "$pbkdf2" <> _ = returned_user.password_hash + end + + test "get_user/authorization with invalid password", %{name: name} do + params = %{"authorization" => %{"name" => name, "password" => "password"}} + res = PleromaAuthenticator.get_user(%Plug.Conn{params: params}) + + assert {:error, {:checkpw, false}} == res + end + + test "get_user/grant_type_password", %{user: user, name: name, password: password} do + params = %{"grant_type" => "password", "username" => name, "password" => password} + res = PleromaAuthenticator.get_user(%Plug.Conn{params: params}) + + assert {:ok, user} == res + end + + test "error credintails" do + res = PleromaAuthenticator.get_user(%Plug.Conn{params: %{}}) + assert {:error, :invalid_credentials} == res + end +end diff --git a/test/pleroma/web/auth/totp_authenticator_test.exs b/test/pleroma/web/auth/totp_authenticator_test.exs @@ -0,0 +1,51 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Auth.TOTPAuthenticatorTest do + use Pleroma.Web.ConnCase + + alias Pleroma.MFA + alias Pleroma.MFA.BackupCodes + alias Pleroma.MFA.TOTP + alias Pleroma.Web.Auth.TOTPAuthenticator + + import Pleroma.Factory + + test "verify token" do + otp_secret = TOTP.generate_secret() + otp_token = TOTP.generate_token(otp_secret) + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + assert TOTPAuthenticator.verify(otp_token, user) == {:ok, :pass} + assert TOTPAuthenticator.verify(nil, user) == {:error, :invalid_token} + assert TOTPAuthenticator.verify("", user) == {:error, :invalid_token} + end + + test "checks backup codes" do + [code | _] = backup_codes = BackupCodes.generate() + + hashed_codes = + backup_codes + |> Enum.map(&Pbkdf2.hash_pwd_salt(&1)) + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + backup_codes: hashed_codes, + totp: %MFA.Settings.TOTP{secret: "otp_secret", confirmed: true} + } + ) + + assert TOTPAuthenticator.verify_recovery_code(user, code) == {:ok, :pass} + refute TOTPAuthenticator.verify_recovery_code(code, refresh_record(user)) == {:ok, :pass} + end +end diff --git a/test/pleroma/web/chat_channel_test.exs b/test/pleroma/web/chat_channel_test.exs @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ChatChannelTest do + use Pleroma.Web.ChannelCase + alias Pleroma.Web.ChatChannel + alias Pleroma.Web.UserSocket + + import Pleroma.Factory + + setup do + user = insert(:user) + + {:ok, _, socket} = + socket(UserSocket, "", %{user_name: user.nickname}) + |> subscribe_and_join(ChatChannel, "chat:public") + + {:ok, socket: socket} + end + + test "it broadcasts a message", %{socket: socket} do + push(socket, "new_msg", %{"text" => "why is tenshi eating a corndog so cute?"}) + assert_broadcast("new_msg", %{text: "why is tenshi eating a corndog so cute?"}) + end + + describe "message lengths" do + setup do: clear_config([:instance, :chat_limit]) + + test "it ignores messages of length zero", %{socket: socket} do + push(socket, "new_msg", %{"text" => ""}) + refute_broadcast("new_msg", %{text: ""}) + end + + test "it ignores messages above a certain length", %{socket: socket} do + Pleroma.Config.put([:instance, :chat_limit], 2) + push(socket, "new_msg", %{"text" => "123"}) + refute_broadcast("new_msg", %{text: "123"}) + end + end +end diff --git a/test/pleroma/web/common_api/utils_test.exs b/test/pleroma/web/common_api/utils_test.exs @@ -0,0 +1,593 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.CommonAPI.UtilsTest do + alias Pleroma.Builders.UserBuilder + alias Pleroma.Object + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.Utils + use Pleroma.DataCase + + import ExUnit.CaptureLog + import Pleroma.Factory + + @public_address "https://www.w3.org/ns/activitystreams#Public" + + describe "add_attachments/2" do + setup do + name = + "Sakura Mana – Turned on by a Senior OL with a Temptating Tight Skirt-s Full Hipline and Panty Shot- Beautiful Thick Thighs- and Erotic Ass- -2015- -- Oppaitime 8-28-2017 6-50-33 PM.png" + + attachment = %{ + "url" => [%{"href" => URI.encode(name)}] + } + + %{name: name, attachment: attachment} + end + + test "it adds attachment links to a given text and attachment set", %{ + name: name, + attachment: attachment + } do + len = 10 + clear_config([Pleroma.Upload, :filename_display_max_length], len) + + expected = + "<br><a href=\"#{URI.encode(name)}\" class='attachment'>#{String.slice(name, 0..len)}…</a>" + + assert Utils.add_attachments("", [attachment]) == expected + end + + test "doesn't truncate file name if config for truncate is set to 0", %{ + name: name, + attachment: attachment + } do + clear_config([Pleroma.Upload, :filename_display_max_length], 0) + + expected = "<br><a href=\"#{URI.encode(name)}\" class='attachment'>#{name}</a>" + + assert Utils.add_attachments("", [attachment]) == expected + end + end + + describe "it confirms the password given is the current users password" do + test "incorrect password given" do + {:ok, user} = UserBuilder.insert() + + assert Utils.confirm_current_password(user, "") == {:error, "Invalid password."} + end + + test "correct password given" do + {:ok, user} = UserBuilder.insert() + assert Utils.confirm_current_password(user, "test") == {:ok, user} + end + end + + describe "format_input/3" do + test "works for bare text/plain" do + text = "hello world!" + expected = "hello world!" + + {output, [], []} = Utils.format_input(text, "text/plain") + + assert output == expected + + text = "hello world!\n\nsecond paragraph!" + expected = "hello world!<br><br>second paragraph!" + + {output, [], []} = Utils.format_input(text, "text/plain") + + assert output == expected + end + + test "works for bare text/html" do + text = "<p>hello world!</p>" + expected = "<p>hello world!</p>" + + {output, [], []} = Utils.format_input(text, "text/html") + + assert output == expected + + text = "<p>hello world!</p><br/>\n<p>second paragraph</p>" + expected = "<p>hello world!</p><br/>\n<p>second paragraph</p>" + + {output, [], []} = Utils.format_input(text, "text/html") + + assert output == expected + end + + test "works for bare text/markdown" do + text = "**hello world**" + expected = "<p><strong>hello world</strong></p>" + + {output, [], []} = Utils.format_input(text, "text/markdown") + + assert output == expected + + text = "**hello world**\n\n*another paragraph*" + expected = "<p><strong>hello world</strong></p><p><em>another paragraph</em></p>" + + {output, [], []} = Utils.format_input(text, "text/markdown") + + assert output == expected + + text = """ + > cool quote + + by someone + """ + + expected = "<blockquote><p>cool quote</p></blockquote><p>by someone</p>" + + {output, [], []} = Utils.format_input(text, "text/markdown") + + assert output == expected + end + + test "works for bare text/bbcode" do + text = "[b]hello world[/b]" + expected = "<strong>hello world</strong>" + + {output, [], []} = Utils.format_input(text, "text/bbcode") + + assert output == expected + + text = "[b]hello world![/b]\n\nsecond paragraph!" + expected = "<strong>hello world!</strong><br><br>second paragraph!" + + {output, [], []} = Utils.format_input(text, "text/bbcode") + + assert output == expected + + text = "[b]hello world![/b]\n\n<strong>second paragraph!</strong>" + + expected = + "<strong>hello world!</strong><br><br>&lt;strong&gt;second paragraph!&lt;/strong&gt;" + + {output, [], []} = Utils.format_input(text, "text/bbcode") + + assert output == expected + end + + test "works for text/markdown with mentions" do + {:ok, user} = + UserBuilder.insert(%{nickname: "user__test", ap_id: "http://foo.com/user__test"}) + + text = "**hello world**\n\n*another @user__test and @user__test google.com paragraph*" + + {output, _, _} = Utils.format_input(text, "text/markdown") + + assert output == + ~s(<p><strong>hello world</strong></p><p><em>another <span class="h-card"><a class="u-url mention" data-user="#{ + user.id + }" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> and <span class="h-card"><a class="u-url mention" data-user="#{ + user.id + }" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> <a href="http://google.com" rel="ugc">google.com</a> paragraph</em></p>) + 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() + + expected = + date + |> DateTime.from_iso8601() + |> elem(1) + |> Calendar.Strftime.strftime!("%a %b %d %H:%M:%S %z %Y") + + assert Utils.date_to_asctime(date) == expected + end + + test "when date is a binary in wrong format" do + date = DateTime.utc_now() + + expected = "" + + assert capture_log(fn -> + assert Utils.date_to_asctime(date) == expected + end) =~ "[warn] Date #{date} in wrong format, must be ISO 8601" + end + + test "when date is a Unix timestamp" do + date = DateTime.utc_now() |> DateTime.to_unix() + + expected = "" + + assert capture_log(fn -> + assert Utils.date_to_asctime(date) == expected + end) =~ "[warn] Date #{date} in wrong format, must be ISO 8601" + end + + test "when date is nil" do + expected = "" + + assert capture_log(fn -> + assert Utils.date_to_asctime(nil) == expected + end) =~ "[warn] Date in wrong format, must be ISO 8601" + end + + test "when date is a random string" do + assert capture_log(fn -> + assert Utils.date_to_asctime("foo") == "" + end) =~ "[warn] Date foo in wrong format, must be ISO 8601" + end + end + + describe "get_to_and_cc" do + test "for public posts, not a reply" do + user = insert(:user) + mentioned_user = insert(:user) + mentions = [mentioned_user.ap_id] + + {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "public", nil) + + assert length(to) == 2 + assert length(cc) == 1 + + assert @public_address in to + assert mentioned_user.ap_id in to + assert user.follower_address in cc + end + + test "for public posts, a reply" do + user = insert(:user) + mentioned_user = insert(:user) + third_user = insert(:user) + {:ok, activity} = CommonAPI.post(third_user, %{status: "uguu"}) + mentions = [mentioned_user.ap_id] + + {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "public", nil) + + assert length(to) == 3 + assert length(cc) == 1 + + assert @public_address in to + assert mentioned_user.ap_id in to + assert third_user.ap_id in to + assert user.follower_address in cc + end + + test "for unlisted posts, not a reply" do + user = insert(:user) + mentioned_user = insert(:user) + mentions = [mentioned_user.ap_id] + + {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "unlisted", nil) + + assert length(to) == 2 + assert length(cc) == 1 + + assert @public_address in cc + assert mentioned_user.ap_id in to + assert user.follower_address in to + end + + test "for unlisted posts, a reply" do + user = insert(:user) + mentioned_user = insert(:user) + third_user = insert(:user) + {:ok, activity} = CommonAPI.post(third_user, %{status: "uguu"}) + mentions = [mentioned_user.ap_id] + + {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "unlisted", nil) + + assert length(to) == 3 + assert length(cc) == 1 + + assert @public_address in cc + assert mentioned_user.ap_id in to + assert third_user.ap_id in to + assert user.follower_address in to + end + + test "for private posts, not a reply" do + user = insert(:user) + mentioned_user = insert(:user) + mentions = [mentioned_user.ap_id] + + {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private", nil) + assert length(to) == 2 + assert Enum.empty?(cc) + + assert mentioned_user.ap_id in to + assert user.follower_address in to + end + + test "for private posts, a reply" do + user = insert(:user) + mentioned_user = insert(:user) + third_user = insert(:user) + {:ok, activity} = CommonAPI.post(third_user, %{status: "uguu"}) + mentions = [mentioned_user.ap_id] + + {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private", nil) + + assert length(to) == 2 + assert Enum.empty?(cc) + + assert mentioned_user.ap_id in to + assert user.follower_address in to + end + + test "for direct posts, not a reply" do + user = insert(:user) + mentioned_user = insert(:user) + mentions = [mentioned_user.ap_id] + + {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct", nil) + + assert length(to) == 1 + assert Enum.empty?(cc) + + assert mentioned_user.ap_id in to + end + + test "for direct posts, a reply" do + user = insert(:user) + mentioned_user = insert(:user) + third_user = insert(:user) + {:ok, activity} = CommonAPI.post(third_user, %{status: "uguu"}) + mentions = [mentioned_user.ap_id] + + {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct", nil) + + assert length(to) == 1 + assert Enum.empty?(cc) + + assert mentioned_user.ap_id in to + + {:ok, direct_activity} = CommonAPI.post(third_user, %{status: "uguu", visibility: "direct"}) + + {to, cc} = Utils.get_to_and_cc(user, mentions, direct_activity, "direct", nil) + + assert length(to) == 2 + assert Enum.empty?(cc) + + assert mentioned_user.ap_id in to + assert third_user.ap_id in to + end + end + + describe "to_master_date/1" do + test "removes microseconds from date (NaiveDateTime)" do + assert Utils.to_masto_date(~N[2015-01-23 23:50:07.123]) == "2015-01-23T23:50:07.000Z" + end + + test "removes microseconds from date (String)" do + assert Utils.to_masto_date("2015-01-23T23:50:07.123Z") == "2015-01-23T23:50:07.000Z" + end + + test "returns empty string when date invalid" do + assert Utils.to_masto_date("2015-01?23T23:50:07.123Z") == "" + 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) + assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == ["test"] + end + + test "returns recipients from tag" do + user = insert(:user) + + object = + insert(:note, + user: user, + data: %{ + "tag" => [ + %{"type" => "Hashtag"}, + "", + %{"type" => "Mention", "href" => "https://testing.pleroma.lol/users/lain"}, + %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"}, + %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"} + ] + } + ) + + activity = insert(:note_activity, user: user, note: object) + + assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == [ + "test", + "https://testing.pleroma.lol/users/lain", + "https://shitposter.club/user/5381" + ] + end + + test "returns recipients when object is map" do + user = insert(:user) + object = insert(:note, user: user) + + activity = + insert(:note_activity, + user: user, + note: object, + data_attrs: %{ + "object" => %{ + "tag" => [ + %{"type" => "Hashtag"}, + "", + %{"type" => "Mention", "href" => "https://testing.pleroma.lol/users/lain"}, + %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"}, + %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"} + ] + } + } + ) + + Pleroma.Repo.delete(object) + + assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == [ + "test", + "https://testing.pleroma.lol/users/lain", + "https://shitposter.club/user/5381" + ] + end + + test "returns recipients when object not found" do + user = insert(:user) + object = insert(:note, user: user) + + activity = insert(:note_activity, user: user, note: object) + Pleroma.Repo.delete(object) + + obj_url = activity.data["object"] + + Tesla.Mock.mock(fn + %{method: :get, url: ^obj_url} -> + %Tesla.Env{status: 404, body: ""} + end) + + assert Utils.maybe_notify_mentioned_recipients(["test-test"], activity) == [ + "test-test" + ] + end + end + + describe "attachments_from_ids_descs/2" do + test "returns [] when attachment ids is empty" do + assert Utils.attachments_from_ids_descs([], "{}") == [] + end + + test "returns list attachments with desc" do + object = insert(:note) + desc = Jason.encode!(%{object.id => "test-desc"}) + + assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc) == [ + Map.merge(object.data, %{"name" => "test-desc"}) + ] + end + end + + describe "attachments_from_ids/1" do + test "returns attachments with descs" do + object = insert(:note) + desc = Jason.encode!(%{object.id => "test-desc"}) + + assert Utils.attachments_from_ids(%{ + media_ids: ["#{object.id}"], + descriptions: desc + }) == [ + 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] + end + + test "returns [] when not pass media_ids" do + assert Utils.attachments_from_ids(%{}) == [] + end + end + + describe "maybe_add_list_data/3" do + test "adds list params when found user list" do + user = insert(:user) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) + + assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) == + %{ + additional: %{"bcc" => [list.ap_id], "listMessage" => list.ap_id}, + object: %{"listMessage" => list.ap_id} + } + end + + test "returns original params when list not found" do + user = insert(:user) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", insert(:user)) + + assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) == + %{additional: %{}, object: %{}} + end + end + + describe "make_note_data/11" do + test "returns note data" do + user = insert(:user) + note = insert(:note) + user2 = insert(:user) + user3 = insert(:user) + + assert Utils.make_note_data( + user.ap_id, + [user2.ap_id], + "2hu", + "<h1>This is :moominmamma: note</h1>", + [], + note.id, + [name: "jimm"], + "test summary", + [user3.ap_id], + false, + %{"custom_tag" => "test"} + ) == %{ + "actor" => user.ap_id, + "attachment" => [], + "cc" => [user3.ap_id], + "content" => "<h1>This is :moominmamma: note</h1>", + "context" => "2hu", + "sensitive" => false, + "summary" => "test summary", + "tag" => ["jimm"], + "to" => [user2.ap_id], + "type" => "Note", + "custom_tag" => "test" + } + end + end + + describe "maybe_add_attachments/3" do + test "returns parsed results when attachment_links is false" do + assert Utils.maybe_add_attachments( + {"test", [], ["tags"]}, + [], + false + ) == {"test", [], ["tags"]} + end + + test "adds attachments to parsed results" do + attachment = %{"url" => [%{"href" => "SakuraPM.png"}]} + + assert Utils.maybe_add_attachments( + {"test", [], ["tags"]}, + [attachment], + true + ) == { + "test<br><a href=\"SakuraPM.png\" class='attachment'>SakuraPM.png</a>", + [], + ["tags"] + } + end + end +end diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs @@ -0,0 +1,1244 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.CommonAPITest do + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Activity + alias Pleroma.Chat + alias Pleroma.Conversation.Participation + alias Pleroma.Notification + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + import Mock + import Ecto.Query, only: [from: 2] + + require Pleroma.Constants + + setup do: clear_config([:instance, :safe_dm_mentions]) + setup do: clear_config([:instance, :limit]) + setup do: clear_config([:instance, :max_pinned_statuses]) + + describe "posting polls" do + test "it posts a poll" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "who is the best", + poll: %{expires_in: 600, options: ["reimu", "marisa"]} + }) + + object = Object.normalize(activity) + + assert object.data["type"] == "Question" + assert object.data["oneOf"] |> length() == 2 + end + end + + describe "blocking" do + setup do + blocker = insert(:user) + blocked = insert(:user) + User.follow(blocker, blocked) + User.follow(blocked, blocker) + %{blocker: blocker, blocked: blocked} + end + + test "it blocks and federates", %{blocker: blocker, blocked: blocked} do + clear_config([:instance, :federating], true) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + assert {:ok, block} = CommonAPI.block(blocker, blocked) + + assert block.local + assert User.blocks?(blocker, blocked) + refute User.following?(blocker, blocked) + refute User.following?(blocked, blocker) + + assert called(Pleroma.Web.Federator.publish(block)) + end + end + + test "it blocks and does not federate if outgoing blocks are disabled", %{ + blocker: blocker, + blocked: blocked + } do + clear_config([:instance, :federating], true) + clear_config([:activitypub, :outgoing_blocks], false) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + assert {:ok, block} = CommonAPI.block(blocker, blocked) + + assert block.local + assert User.blocks?(blocker, blocked) + refute User.following?(blocker, blocked) + refute User.following?(blocked, blocker) + + refute called(Pleroma.Web.Federator.publish(block)) + end + end + end + + describe "posting chat messages" do + setup do: clear_config([:instance, :chat_limit]) + + test "it posts a chat message without content but with an attachment" do + author = insert(:user) + recipient = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: author.ap_id) + + with_mocks([ + { + Pleroma.Web.Streamer, + [], + [ + stream: fn _, _ -> + nil + end + ] + }, + { + Pleroma.Web.Push, + [], + [ + send: fn _ -> nil end + ] + } + ]) do + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + nil, + media_id: upload.id + ) + + notification = + Notification.for_user_and_activity(recipient, activity) + |> Repo.preload(:activity) + + assert called(Pleroma.Web.Push.send(notification)) + assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) + assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) + + assert activity + end + end + + test "it adds html newlines" do + author = insert(:user) + recipient = insert(:user) + + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + "uguu\nuguuu" + ) + + assert other_user.ap_id not in activity.recipients + + object = Object.normalize(activity, false) + + assert object.data["content"] == "uguu<br/>uguuu" + end + + test "it linkifies" do + author = insert(:user) + recipient = insert(:user) + + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + "https://example.org is the site of @#{other_user.nickname} #2hu" + ) + + assert other_user.ap_id not in activity.recipients + + object = Object.normalize(activity, false) + + assert object.data["content"] == + "<a href=\"https://example.org\" rel=\"ugc\">https://example.org</a> is the site of <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{ + other_user.id + }\" href=\"#{other_user.ap_id}\" rel=\"ugc\">@<span>#{other_user.nickname}</span></a></span> <a class=\"hashtag\" data-tag=\"2hu\" href=\"http://localhost:4001/tag/2hu\">#2hu</a>" + end + + test "it posts a chat message" do + author = insert(:user) + recipient = insert(:user) + + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + "a test message <script>alert('uuu')</script> :firefox:" + ) + + assert activity.data["type"] == "Create" + assert activity.local + object = Object.normalize(activity) + + assert object.data["type"] == "ChatMessage" + assert object.data["to"] == [recipient.ap_id] + + assert object.data["content"] == + "a test message &lt;script&gt;alert(&#39;uuu&#39;)&lt;/script&gt; :firefox:" + + assert object.data["emoji"] == %{ + "firefox" => "http://localhost:4001/emoji/Firefox.gif" + } + + assert Chat.get(author.id, recipient.ap_id) + assert Chat.get(recipient.id, author.ap_id) + + assert :ok == Pleroma.Web.Federator.perform(:publish, activity) + end + + test "it reject messages over the local limit" do + Pleroma.Config.put([:instance, :chat_limit], 2) + + author = insert(:user) + recipient = insert(:user) + + {:error, message} = + CommonAPI.post_chat_message( + author, + recipient, + "123" + ) + + assert message == :content_too_long + end + + test "it reject messages via MRF" do + clear_config([:mrf_keyword, :reject], ["GNO"]) + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) + + author = insert(:user) + recipient = insert(:user) + + assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} == + CommonAPI.post_chat_message(author, recipient, "GNO/Linux") + end + end + + describe "unblocking" do + test "it works even without an existing block activity" do + blocked = insert(:user) + blocker = insert(:user) + User.block(blocker, blocked) + + assert User.blocks?(blocker, blocked) + assert {:ok, :no_activity} == CommonAPI.unblock(blocker, blocked) + refute User.blocks?(blocker, blocked) + end + end + + describe "deletion" do + test "it works with pruned objects" do + user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) + + clear_config([:instance, :federating], true) + + Object.normalize(post, false) + |> Object.prune() + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + assert {:ok, delete} = CommonAPI.delete(post.id, user) + assert delete.local + assert called(Pleroma.Web.Federator.publish(delete)) + end + + refute Activity.get_by_id(post.id) + end + + test "it allows users to delete their posts" do + user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) + + clear_config([:instance, :federating], true) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + assert {:ok, delete} = CommonAPI.delete(post.id, user) + assert delete.local + assert called(Pleroma.Web.Federator.publish(delete)) + end + + refute Activity.get_by_id(post.id) + end + + test "it does not allow a user to delete their posts" do + user = insert(:user) + other_user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) + + assert {:error, "Could not delete"} = CommonAPI.delete(post.id, other_user) + assert Activity.get_by_id(post.id) + end + + test "it allows moderators to delete other user's posts" do + user = insert(:user) + moderator = insert(:user, is_moderator: 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) + end + + test "it allows admins to delete other user's posts" do + user = insert(:user) + moderator = insert(:user, 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) + end + + test "superusers deleting non-local posts won't federate the delete" do + # This is the user of the ingested activity + _user = + insert(:user, + local: false, + ap_id: "http://mastodon.example.org/users/admin", + last_refreshed_at: NaiveDateTime.utc_now() + ) + + moderator = insert(:user, is_admin: true) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Jason.decode!() + + {:ok, post} = Transmogrifier.handle_incoming(data) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + assert {:ok, delete} = CommonAPI.delete(post.id, moderator) + assert delete.local + refute called(Pleroma.Web.Federator.publish(:_)) + end + + refute Activity.get_by_id(post.id) + end + end + + test "favoriting race condition" do + user = insert(:user) + users_serial = insert_list(10, :user) + users = insert_list(10, :user) + + {:ok, activity} = CommonAPI.post(user, %{status: "."}) + + users_serial + |> Enum.map(fn user -> + CommonAPI.favorite(user, activity.id) + end) + + object = Object.get_by_ap_id(activity.data["object"]) + assert object.data["like_count"] == 10 + + users + |> Enum.map(fn user -> + Task.async(fn -> + CommonAPI.favorite(user, activity.id) + end) + end) + |> Enum.map(&Task.await/1) + + object = Object.get_by_ap_id(activity.data["object"]) + assert object.data["like_count"] == 20 + end + + test "repeating race condition" do + user = insert(:user) + users_serial = insert_list(10, :user) + users = insert_list(10, :user) + + {:ok, activity} = CommonAPI.post(user, %{status: "."}) + + users_serial + |> Enum.map(fn user -> + CommonAPI.repeat(activity.id, user) + end) + + object = Object.get_by_ap_id(activity.data["object"]) + assert object.data["announcement_count"] == 10 + + users + |> Enum.map(fn user -> + Task.async(fn -> + CommonAPI.repeat(activity.id, user) + end) + end) + |> Enum.map(&Task.await/1) + + object = Object.get_by_ap_id(activity.data["object"]) + assert object.data["announcement_count"] == 20 + end + + test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + + [participation] = Participation.for_user(user) + + {:ok, convo_reply} = + CommonAPI.post(user, %{status: ".", in_reply_to_conversation_id: participation.id}) + + assert Visibility.is_direct?(convo_reply) + + assert activity.data["context"] == convo_reply.data["context"] + end + + test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do + har = insert(:user) + jafnhar = insert(:user) + tridi = insert(:user) + + {:ok, activity} = + CommonAPI.post(har, %{ + status: "@#{jafnhar.nickname} hey", + visibility: "direct" + }) + + assert har.ap_id in activity.recipients + assert jafnhar.ap_id in activity.recipients + + [participation] = Participation.for_user(har) + + {:ok, activity} = + CommonAPI.post(har, %{ + status: "I don't really like @#{tridi.nickname}", + visibility: "direct", + in_reply_to_status_id: activity.id, + in_reply_to_conversation_id: participation.id + }) + + assert har.ap_id in activity.recipients + assert jafnhar.ap_id in activity.recipients + refute tridi.ap_id in activity.recipients + end + + test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do + har = insert(:user) + jafnhar = insert(:user) + tridi = insert(:user) + + Pleroma.Config.put([:instance, :safe_dm_mentions], true) + + {:ok, activity} = + CommonAPI.post(har, %{ + status: "@#{jafnhar.nickname} hey, i never want to see @#{tridi.nickname} again", + visibility: "direct" + }) + + refute tridi.ap_id in activity.recipients + assert jafnhar.ap_id in activity.recipients + end + + test "it de-duplicates tags" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU"}) + + object = Object.normalize(activity) + + assert object.data["tag"] == ["2hu"] + end + + test "it adds emoji in the object" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: ":firefox:"}) + + assert Object.normalize(activity).data["emoji"]["firefox"] + end + + describe "posting" do + test "deactivated users can't post" do + user = insert(:user, deactivated: true) + assert {:error, _} = CommonAPI.post(user, %{status: "ye"}) + end + + test "it supports explicit addressing" do + user = insert(:user) + user_two = insert(:user) + user_three = insert(:user) + user_four = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: + "Hey, I think @#{user_three.nickname} is ugly. @#{user_four.nickname} is alright though.", + to: [user_two.nickname, user_four.nickname, "nonexistent"] + }) + + assert user.ap_id in activity.recipients + assert user_two.ap_id in activity.recipients + assert user_four.ap_id in activity.recipients + refute user_three.ap_id in activity.recipients + end + + test "it filters out obviously bad tags when accepting a post as HTML" do + user = insert(:user) + + post = "<p><b>2hu</b></p><script>alert('xss')</script>" + + {:ok, activity} = + CommonAPI.post(user, %{ + status: post, + content_type: "text/html" + }) + + object = Object.normalize(activity) + + assert object.data["content"] == "<p><b>2hu</b></p>alert(&#39;xss&#39;)" + assert object.data["source"] == post + end + + test "it filters out obviously bad tags when accepting a post as Markdown" do + user = insert(:user) + + post = "<p><b>2hu</b></p><script>alert('xss')</script>" + + {:ok, activity} = + CommonAPI.post(user, %{ + status: post, + content_type: "text/markdown" + }) + + object = Object.normalize(activity) + + assert object.data["content"] == "<p><b>2hu</b></p>alert(&#39;xss&#39;)" + assert object.data["source"] == post + end + + test "it does not allow replies to direct messages that are not direct messages themselves" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"}) + + assert {:ok, _} = + CommonAPI.post(user, %{ + status: "suya..", + visibility: "direct", + in_reply_to_status_id: activity.id + }) + + Enum.each(["public", "private", "unlisted"], fn visibility -> + assert {:error, "The message visibility must be direct"} = + CommonAPI.post(user, %{ + status: "suya..", + visibility: visibility, + in_reply_to_status_id: activity.id + }) + end) + end + + test "replying with a direct message will NOT auto-add the author of the reply to the recipient list" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "I'm stupid"}) + + {:ok, open_answer} = + CommonAPI.post(other_user, %{status: "No ur smart", in_reply_to_status_id: post.id}) + + # The OP is implicitly added + assert user.ap_id in open_answer.recipients + + {:ok, secret_answer} = + CommonAPI.post(other_user, %{ + status: "lol, that guy really is stupid, right, @#{third_user.nickname}?", + in_reply_to_status_id: post.id, + visibility: "direct" + }) + + assert third_user.ap_id in secret_answer.recipients + + # The OP is not added + refute user.ap_id in secret_answer.recipients + end + + test "it allows to address a list" do + user = insert(:user) + {:ok, list} = Pleroma.List.create("foo", user) + + {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) + + assert activity.data["bcc"] == [list.ap_id] + assert activity.recipients == [list.ap_id, user.ap_id] + assert activity.data["listMessage"] == list.ap_id + end + + test "it returns error when status is empty and no attachments" do + user = insert(:user) + + assert {:error, "Cannot post an empty status without attachments"} = + CommonAPI.post(user, %{status: ""}) + end + + test "it validates character limits are correctly enforced" do + Pleroma.Config.put([:instance, :limit], 5) + + user = insert(:user) + + assert {:error, "The status is over the character limit"} = + CommonAPI.post(user, %{status: "foobar"}) + + assert {:ok, activity} = CommonAPI.post(user, %{status: "12345"}) + end + + test "it can handle activities that expire" do + user = insert(:user) + + expires_at = DateTime.add(DateTime.utc_now(), 1_000_000) + + assert {:ok, activity} = CommonAPI.post(user, %{status: "chai", expires_in: 1_000_000}) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity.id}, + scheduled_at: expires_at + ) + end + end + + describe "reactions" do + test "reacting to a status with an emoji" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) + + {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍") + + assert reaction.data["actor"] == user.ap_id + assert reaction.data["content"] == "👍" + + {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) + + {:error, _} = CommonAPI.react_with_emoji(activity.id, user, ".") + end + + test "unreacting to a status with an emoji" do + user = insert(:user) + other_user = insert(:user) + + clear_config([:instance, :federating], true) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) + {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍") + + {:ok, unreaction} = CommonAPI.unreact_with_emoji(activity.id, user, "👍") + + assert unreaction.data["type"] == "Undo" + assert unreaction.data["object"] == reaction.data["id"] + assert unreaction.local + + # On federation, it contains the undone (and deleted) object + unreaction_with_object = %{ + unreaction + | data: Map.put(unreaction.data, "object", reaction.data) + } + + assert called(Pleroma.Web.Federator.publish(unreaction_with_object)) + end + end + + test "repeating a status" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) + + {:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, user) + assert Visibility.is_public?(announce_activity) + end + + test "can't repeat a repeat" do + user = insert(:user) + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) + + {:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, other_user) + + refute match?({:ok, %Activity{}}, CommonAPI.repeat(announce.id, user)) + end + + test "repeating a status privately" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) + + {:ok, %Activity{} = announce_activity} = + CommonAPI.repeat(activity.id, user, %{visibility: "private"}) + + assert Visibility.is_private?(announce_activity) + refute Visibility.visible_for_user?(announce_activity, nil) + end + + test "favoriting a status" do + user = insert(:user) + other_user = insert(:user) + + {:ok, post_activity} = CommonAPI.post(other_user, %{status: "cofe"}) + + {:ok, %Activity{data: data}} = CommonAPI.favorite(user, post_activity.id) + assert data["type"] == "Like" + assert data["actor"] == user.ap_id + assert data["object"] == post_activity.data["object"] + end + + test "retweeting a status twice returns the status" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) + {:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, user) + {:ok, ^announce} = CommonAPI.repeat(activity.id, user) + end + + test "favoriting a status twice returns ok, but without the like activity" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) + {:ok, %Activity{}} = CommonAPI.favorite(user, activity.id) + assert {:ok, :already_liked} = CommonAPI.favorite(user, activity.id) + end + end + + describe "pinned statuses" do + setup do + Pleroma.Config.put([:instance, :max_pinned_statuses], 1) + + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"}) + + [user: user, activity: activity] + end + + test "pin status", %{user: user, activity: activity} do + assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) + + id = activity.id + user = refresh_record(user) + + assert %User{pinned_activities: [^id]} = user + end + + test "pin poll", %{user: user} do + {:ok, activity} = + CommonAPI.post(user, %{ + status: "How is fediverse today?", + poll: %{options: ["Absolutely outstanding", "Not good"], expires_in: 20} + }) + + assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) + + id = activity.id + user = refresh_record(user) + + assert %User{pinned_activities: [^id]} = user + end + + test "unlisted statuses can be pinned", %{user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!", visibility: "unlisted"}) + assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) + end + + test "only self-authored can be pinned", %{activity: activity} do + user = insert(:user) + + assert {:error, "Could not pin"} = CommonAPI.pin(activity.id, user) + end + + test "max pinned statuses", %{user: user, activity: activity_one} do + {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"}) + + assert {:ok, ^activity_one} = CommonAPI.pin(activity_one.id, user) + + user = refresh_record(user) + + assert {:error, "You have already pinned the maximum number of statuses"} = + CommonAPI.pin(activity_two.id, user) + end + + test "unpin status", %{user: user, activity: activity} do + {:ok, activity} = CommonAPI.pin(activity.id, user) + + user = refresh_record(user) + + id = activity.id + + assert match?({:ok, %{id: ^id}}, CommonAPI.unpin(activity.id, user)) + + user = refresh_record(user) + + assert %User{pinned_activities: []} = user + end + + test "should unpin when deleting a status", %{user: user, activity: activity} do + {:ok, activity} = CommonAPI.pin(activity.id, user) + + user = refresh_record(user) + + assert {:ok, _} = CommonAPI.delete(activity.id, user) + + user = refresh_record(user) + + assert %User{pinned_activities: []} = user + end + end + + describe "mute tests" do + setup do + user = insert(:user) + + activity = insert(:note_activity) + + [user: user, activity: activity] + end + + test "marks notifications as read after mute" do + author = insert(:user) + activity = insert(:note_activity, user: author) + + friend1 = insert(:user) + friend2 = insert(:user) + + {:ok, reply_activity} = + CommonAPI.post( + friend2, + %{ + status: "@#{author.nickname} @#{friend1.nickname} test reply", + in_reply_to_status_id: activity.id + } + ) + + {:ok, favorite_activity} = CommonAPI.favorite(friend2, activity.id) + {:ok, repeat_activity} = CommonAPI.repeat(activity.id, friend1) + + assert Repo.aggregate( + from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id), + :count + ) == 1 + + unread_notifications = + Repo.all(from(n in Notification, where: n.seen == false, where: n.user_id == ^author.id)) + + assert Enum.any?(unread_notifications, fn n -> + n.type == "favourite" && n.activity_id == favorite_activity.id + end) + + assert Enum.any?(unread_notifications, fn n -> + n.type == "reblog" && n.activity_id == repeat_activity.id + end) + + assert Enum.any?(unread_notifications, fn n -> + n.type == "mention" && n.activity_id == reply_activity.id + end) + + {:ok, _} = CommonAPI.add_mute(author, activity) + assert CommonAPI.thread_muted?(author, activity) + + assert Repo.aggregate( + from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id), + :count + ) == 1 + + read_notifications = + Repo.all(from(n in Notification, where: n.seen == true, where: n.user_id == ^author.id)) + + assert Enum.any?(read_notifications, fn n -> + n.type == "favourite" && n.activity_id == favorite_activity.id + end) + + assert Enum.any?(read_notifications, fn n -> + n.type == "reblog" && n.activity_id == repeat_activity.id + end) + + assert Enum.any?(read_notifications, fn n -> + n.type == "mention" && n.activity_id == reply_activity.id + end) + end + + test "add mute", %{user: user, activity: activity} do + {:ok, _} = CommonAPI.add_mute(user, activity) + assert CommonAPI.thread_muted?(user, activity) + end + + test "remove mute", %{user: user, activity: activity} do + CommonAPI.add_mute(user, activity) + {:ok, _} = CommonAPI.remove_mute(user, activity) + refute CommonAPI.thread_muted?(user, activity) + end + + test "check that mutes can't be duplicate", %{user: user, activity: activity} do + CommonAPI.add_mute(user, activity) + {:error, _} = CommonAPI.add_mute(user, activity) + end + end + + describe "reports" do + test "creates a report" do + reporter = insert(:user) + target_user = insert(:user) + + {:ok, activity} = CommonAPI.post(target_user, %{status: "foobar"}) + + reporter_ap_id = reporter.ap_id + target_ap_id = target_user.ap_id + activity_ap_id = activity.data["id"] + comment = "foobar" + + report_data = %{ + account_id: target_user.id, + comment: comment, + status_ids: [activity.id] + } + + note_obj = %{ + "type" => "Note", + "id" => activity_ap_id, + "content" => "foobar", + "published" => activity.object.data["published"], + "actor" => AccountView.render("show.json", %{user: target_user}) + } + + assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data) + + assert %Activity{ + actor: ^reporter_ap_id, + data: %{ + "type" => "Flag", + "content" => ^comment, + "object" => [^target_ap_id, ^note_obj], + "state" => "open" + } + } = flag_activity + end + + test "updates report state" do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %Activity{id: report_id}} = + 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, activity_id] = report.data["object"] + + assert reported_user == target_user.ap_id + assert activity_id == activity.data["id"] + end + + test "does not update report state when state is unsupported" do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %Activity{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"} + end + + test "updates state of multiple reports" do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %Activity{id: first_report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + {:ok, %Activity{id: second_report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel very offended!", + status_ids: [activity.id] + }) + + {:ok, report_ids} = + CommonAPI.update_report_state([first_report_id, second_report_id], "resolved") + + first_report = Activity.get_by_id(first_report_id) + second_report = Activity.get_by_id(second_report_id) + + assert report_ids -- [first_report_id, second_report_id] == [] + assert first_report.data["state"] == "resolved" + assert second_report.data["state"] == "resolved" + end + end + + describe "reblog muting" do + setup do + muter = insert(:user) + + muted = insert(:user) + + [muter: muter, muted: muted] + end + + test "add a reblog mute", %{muter: muter, muted: muted} do + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted) + + assert User.showing_reblogs?(muter, muted) == false + end + + test "remove a reblog mute", %{muter: muter, muted: muted} do + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted) + {:ok, _reblog_mute} = CommonAPI.show_reblogs(muter, muted) + + assert User.showing_reblogs?(muter, muted) == true + end + end + + describe "follow/2" do + test "directly follows a non-locked local user" do + [follower, followed] = insert_pair(:user) + {:ok, follower, followed, _} = CommonAPI.follow(follower, followed) + + assert User.following?(follower, followed) + end + end + + describe "unfollow/2" do + test "also unsubscribes a user" do + [follower, followed] = insert_pair(:user) + {:ok, follower, followed, _} = CommonAPI.follow(follower, followed) + {:ok, _subscription} = User.subscribe(follower, followed) + + assert User.subscribed_to?(follower, followed) + + {:ok, follower} = CommonAPI.unfollow(follower, followed) + + refute User.subscribed_to?(follower, followed) + end + + test "cancels a pending follow for a local user" do + follower = insert(:user) + followed = insert(:user, locked: true) + + assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = + CommonAPI.follow(follower, followed) + + assert User.get_follow_state(follower, followed) == :follow_pending + assert {:ok, follower} = CommonAPI.unfollow(follower, followed) + assert User.get_follow_state(follower, followed) == nil + + assert %{id: ^activity_id, data: %{"state" => "cancelled"}} = + Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed) + + assert %{ + data: %{ + "type" => "Undo", + "object" => %{"type" => "Follow", "state" => "cancelled"} + } + } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower) + end + + test "cancels a pending follow for a remote user" do + follower = insert(:user) + followed = insert(:user, locked: true, local: false, ap_enabled: true) + + assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = + CommonAPI.follow(follower, followed) + + assert User.get_follow_state(follower, followed) == :follow_pending + assert {:ok, follower} = CommonAPI.unfollow(follower, followed) + assert User.get_follow_state(follower, followed) == nil + + assert %{id: ^activity_id, data: %{"state" => "cancelled"}} = + Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed) + + assert %{ + data: %{ + "type" => "Undo", + "object" => %{"type" => "Follow", "state" => "cancelled"} + } + } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower) + end + end + + describe "accept_follow_request/2" do + test "after acceptance, it sets all existing pending follow request states to 'accept'" do + user = insert(:user, locked: true) + follower = insert(:user) + follower_two = insert(:user) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user) + + assert follow_activity.data["state"] == "pending" + assert follow_activity_two.data["state"] == "pending" + assert follow_activity_three.data["state"] == "pending" + + {:ok, _follower} = CommonAPI.accept_follow_request(follower, user) + + assert Repo.get(Activity, follow_activity.id).data["state"] == "accept" + assert Repo.get(Activity, follow_activity_two.id).data["state"] == "accept" + assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending" + end + + test "after rejection, it sets all existing pending follow request states to 'reject'" do + user = insert(:user, locked: true) + follower = insert(:user) + follower_two = insert(:user) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user) + + assert follow_activity.data["state"] == "pending" + assert follow_activity_two.data["state"] == "pending" + assert follow_activity_three.data["state"] == "pending" + + {:ok, _follower} = CommonAPI.reject_follow_request(follower, user) + + assert Repo.get(Activity, follow_activity.id).data["state"] == "reject" + assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject" + assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending" + end + + test "doesn't create a following relationship if the corresponding follow request doesn't exist" do + user = insert(:user, locked: true) + not_follower = insert(:user) + CommonAPI.accept_follow_request(not_follower, user) + + assert Pleroma.FollowingRelationship.following?(not_follower, user) == false + end + end + + describe "vote/3" do + test "does not allow to vote twice" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Am I cute?", + poll: %{options: ["Yes", "No"], expires_in: 20} + }) + + object = Object.normalize(activity) + + {:ok, _, object} = CommonAPI.vote(other_user, object, [0]) + + assert {:error, "Already voted"} == CommonAPI.vote(other_user, object, [1]) + end + end + + describe "listen/2" do + test "returns a valid activity" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.listen(user, %{ + title: "lain radio episode 1", + album: "lain radio", + artist: "lain", + length: 180_000 + }) + + object = Object.normalize(activity) + + assert object.data["title"] == "lain radio episode 1" + + assert Visibility.get_visibility(activity) == "public" + end + + test "respects visibility=private" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.listen(user, %{ + title: "lain radio episode 1", + album: "lain radio", + artist: "lain", + length: 180_000, + visibility: "private" + }) + + object = Object.normalize(activity) + + assert object.data["title"] == "lain radio episode 1" + + assert Visibility.get_visibility(activity) == "private" + end + end + + describe "get_user/1" do + test "gets user by ap_id" do + user = insert(:user) + assert CommonAPI.get_user(user.ap_id) == user + end + + test "gets user by guessed nickname" do + user = insert(:user, ap_id: "", nickname: "mario@mushroom.kingdom") + assert CommonAPI.get_user("https://mushroom.kingdom/users/mario") == user + end + + test "fallback" do + assert %User{ + name: "", + ap_id: "", + nickname: "erroruser@example.com" + } = CommonAPI.get_user("") + end + end +end diff --git a/test/pleroma/web/fallback_test.exs b/test/pleroma/web/fallback_test.exs @@ -0,0 +1,80 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +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 + + describe "preloaded data and metadata attached to" do + test "GET /:maybe_nickname_or_id", %{conn: conn} do + 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" + end + + test "GET /*path", %{conn: conn} do + assert conn + |> get("/foo") + |> html_response(200) =~ "<!--server-generated-meta-->" + + refute conn + |> get("/foo/bar") + |> html_response(200) =~ "<!--server-generated-meta-->" + end + end + + describe "preloaded data is attached to" do + test "GET /main/public", %{conn: conn} do + public_page = get(conn, "/main/public") + + refute html_response(public_page, 200) =~ "<!--server-generated-meta-->" + assert html_response(public_page, 200) =~ "initial-results" + end + + test "GET /main/all", %{conn: conn} do + public_page = get(conn, "/main/all") + + refute html_response(public_page, 200) =~ "<!--server-generated-meta-->" + assert html_response(public_page, 200) =~ "initial-results" + end + end + + test "GET /api*path", %{conn: conn} do + assert conn + |> get("/api/foo") + |> json_response(404) == %{"error" => "Not implemented"} + end + + test "GET /pleroma/admin -> /pleroma/admin/", %{conn: conn} do + assert redirected_to(get(conn, "/pleroma/admin")) =~ "/pleroma/admin/" + end + + test "OPTIONS /*path", %{conn: conn} do + assert conn + |> options("/foo") + |> response(204) == "" + + assert conn + |> options("/foo/bar") + |> response(204) == "" + end +end diff --git a/test/pleroma/web/fed_sockets/fed_registry_test.exs b/test/pleroma/web/fed_sockets/fed_registry_test.exs @@ -0,0 +1,124 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets.FedRegistryTest do + use ExUnit.Case + + alias Pleroma.Web.FedSockets + alias Pleroma.Web.FedSockets.FedRegistry + alias Pleroma.Web.FedSockets.SocketInfo + + @good_domain "http://good.domain" + @good_domain_origin "good.domain:80" + + setup do + start_supervised({Pleroma.Web.FedSockets.Supervisor, []}) + build_test_socket(@good_domain) + Process.sleep(10) + + :ok + end + + describe "add_fed_socket/1 without conflicting sockets" do + test "can be added" do + Process.sleep(10) + assert {:ok, %SocketInfo{origin: origin}} = FedRegistry.get_fed_socket(@good_domain_origin) + assert origin == "good.domain:80" + end + + test "multiple origins can be added" do + build_test_socket("http://anothergood.domain") + Process.sleep(10) + + assert {:ok, %SocketInfo{origin: origin_1}} = + FedRegistry.get_fed_socket(@good_domain_origin) + + assert {:ok, %SocketInfo{origin: origin_2}} = + FedRegistry.get_fed_socket("anothergood.domain:80") + + assert origin_1 == "good.domain:80" + assert origin_2 == "anothergood.domain:80" + assert FedRegistry.list_all() |> Enum.count() == 2 + end + end + + describe "add_fed_socket/1 when duplicate sockets conflict" do + setup do + build_test_socket(@good_domain) + build_test_socket(@good_domain) + Process.sleep(10) + :ok + end + + test "will be ignored" do + assert {:ok, %SocketInfo{origin: origin, pid: pid_one}} = + FedRegistry.get_fed_socket(@good_domain_origin) + + assert origin == "good.domain:80" + + assert FedRegistry.list_all() |> Enum.count() == 1 + end + + test "the newer process will be closed" do + pid_two = build_test_socket(@good_domain) + + assert {:ok, %SocketInfo{origin: origin, pid: pid_one}} = + FedRegistry.get_fed_socket(@good_domain_origin) + + assert origin == "good.domain:80" + Process.sleep(10) + + refute Process.alive?(pid_two) + + assert FedRegistry.list_all() |> Enum.count() == 1 + end + end + + describe "get_fed_socket/1" do + test "returns missing for unknown hosts" do + assert {:error, :missing} = FedRegistry.get_fed_socket("not_a_dmoain") + end + + test "returns rejected for hosts previously rejected" do + "rejected.domain:80" + |> FedSockets.uri_for_origin() + |> FedRegistry.set_host_rejected() + + assert {:error, :rejected} = FedRegistry.get_fed_socket("rejected.domain:80") + end + + test "can retrieve a previously added SocketInfo" do + build_test_socket(@good_domain) + Process.sleep(10) + assert {:ok, %SocketInfo{origin: origin}} = FedRegistry.get_fed_socket(@good_domain_origin) + assert origin == "good.domain:80" + end + + test "removes references to SocketInfos when the process crashes" do + assert {:ok, %SocketInfo{origin: origin, pid: pid}} = + FedRegistry.get_fed_socket(@good_domain_origin) + + assert origin == "good.domain:80" + + Process.exit(pid, :testing) + Process.sleep(100) + assert {:error, :missing} = FedRegistry.get_fed_socket(@good_domain_origin) + end + end + + def build_test_socket(uri) do + Kernel.spawn(fn -> fed_socket_almost(uri) end) + end + + def fed_socket_almost(origin) do + FedRegistry.add_fed_socket(origin) + + receive do + :close -> + :ok + after + 5_000 -> :timeout + end + end +end diff --git a/test/pleroma/web/fed_sockets/fetch_registry_test.exs b/test/pleroma/web/fed_sockets/fetch_registry_test.exs @@ -0,0 +1,67 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets.FetchRegistryTest do + use ExUnit.Case + + alias Pleroma.Web.FedSockets.FetchRegistry + alias Pleroma.Web.FedSockets.FetchRegistry.FetchRegistryData + + @json_message "hello" + @json_reply "hello back" + + setup do + start_supervised( + {Pleroma.Web.FedSockets.Supervisor, + [ + ping_interval: 8, + connection_duration: 15, + rejection_duration: 5, + fed_socket_fetches: [default: 10, interval: 10] + ]} + ) + + :ok + end + + test "fetches can be stored" do + uuid = FetchRegistry.register_fetch(@json_message) + + assert {:error, :waiting} = FetchRegistry.check_fetch(uuid) + end + + test "fetches can return" do + uuid = FetchRegistry.register_fetch(@json_message) + task = Task.async(fn -> FetchRegistry.register_fetch_received(uuid, @json_reply) end) + + assert {:error, :waiting} = FetchRegistry.check_fetch(uuid) + Task.await(task) + + assert {:ok, %FetchRegistryData{received_json: received_json}} = + FetchRegistry.check_fetch(uuid) + + assert received_json == @json_reply + end + + test "fetches are deleted once popped from stack" do + uuid = FetchRegistry.register_fetch(@json_message) + task = Task.async(fn -> FetchRegistry.register_fetch_received(uuid, @json_reply) end) + Task.await(task) + + assert {:ok, %FetchRegistryData{received_json: received_json}} = + FetchRegistry.check_fetch(uuid) + + assert received_json == @json_reply + assert {:ok, @json_reply} = FetchRegistry.pop_fetch(uuid) + + assert {:error, :missing} = FetchRegistry.check_fetch(uuid) + end + + test "fetches can time out" do + uuid = FetchRegistry.register_fetch(@json_message) + assert {:error, :waiting} = FetchRegistry.check_fetch(uuid) + Process.sleep(500) + assert {:error, :missing} = FetchRegistry.check_fetch(uuid) + end +end diff --git a/test/pleroma/web/fed_sockets/socket_info_test.exs b/test/pleroma/web/fed_sockets/socket_info_test.exs @@ -0,0 +1,118 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets.SocketInfoTest do + use ExUnit.Case + + alias Pleroma.Web.FedSockets + alias Pleroma.Web.FedSockets.SocketInfo + + describe "uri_for_origin" do + test "provides the fed_socket URL given the origin information" do + endpoint = "example.com:4000" + assert FedSockets.uri_for_origin(endpoint) =~ "ws://" + assert FedSockets.uri_for_origin(endpoint) =~ endpoint + end + end + + describe "origin" do + test "will provide the origin field given a url" do + endpoint = "example.com:4000" + assert SocketInfo.origin("ws://#{endpoint}") == endpoint + assert SocketInfo.origin("http://#{endpoint}") == endpoint + assert SocketInfo.origin("https://#{endpoint}") == endpoint + end + + test "will proide the origin field given a uri" do + endpoint = "example.com:4000" + uri = URI.parse("http://#{endpoint}") + + assert SocketInfo.origin(uri) == endpoint + end + end + + describe "touch" do + test "will update the TTL" do + endpoint = "example.com:4000" + socket = SocketInfo.build("ws://#{endpoint}") + Process.sleep(2) + touched_socket = SocketInfo.touch(socket) + + assert socket.connected_until < touched_socket.connected_until + end + end + + describe "expired?" do + setup do + start_supervised( + {Pleroma.Web.FedSockets.Supervisor, + [ + ping_interval: 8, + connection_duration: 5, + rejection_duration: 5, + fed_socket_rejections: [lazy: true] + ]} + ) + + :ok + end + + test "tests if the TTL is exceeded" do + endpoint = "example.com:4000" + socket = SocketInfo.build("ws://#{endpoint}") + refute SocketInfo.expired?(socket) + Process.sleep(10) + + assert SocketInfo.expired?(socket) + end + end + + describe "creating outgoing connection records" do + test "can be passed a string" do + assert %{conn_pid: :pid, origin: _origin} = SocketInfo.build("example.com:4000", :pid) + end + + test "can be passed a URI" do + uri = URI.parse("http://example.com:4000") + assert %{conn_pid: :pid, origin: origin} = SocketInfo.build(uri, :pid) + assert origin =~ "example.com:4000" + end + + test "will include the port number" do + assert %{conn_pid: :pid, origin: origin} = SocketInfo.build("http://example.com:4000", :pid) + + assert origin =~ ":4000" + end + + test "will provide the port if missing" do + assert %{conn_pid: :pid, origin: "example.com:80"} = + SocketInfo.build("http://example.com", :pid) + + assert %{conn_pid: :pid, origin: "example.com:443"} = + SocketInfo.build("https://example.com", :pid) + end + end + + describe "creating incoming connection records" do + test "can be passed a string" do + assert %{pid: _, origin: _origin} = SocketInfo.build("example.com:4000") + end + + test "can be passed a URI" do + uri = URI.parse("example.com:4000") + assert %{pid: _, origin: _origin} = SocketInfo.build(uri) + end + + test "will include the port number" do + assert %{pid: _, origin: origin} = SocketInfo.build("http://example.com:4000") + + assert origin =~ ":4000" + end + + test "will provide the port if missing" do + assert %{pid: _, origin: "example.com:80"} = SocketInfo.build("http://example.com") + assert %{pid: _, origin: "example.com:443"} = SocketInfo.build("https://example.com") + end + end +end diff --git a/test/pleroma/web/federator_test.exs b/test/pleroma/web/federator_test.exs @@ -0,0 +1,173 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FederatorTest do + alias Pleroma.Instances + alias Pleroma.Tests.ObanHelpers + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Federator + alias Pleroma.Workers.PublisherWorker + + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + import Mock + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + + :ok + end + + setup_all do: clear_config([:instance, :federating], true) + setup do: clear_config([:instance, :allow_relay]) + setup do: clear_config([:mrf, :policies]) + setup do: clear_config([:mrf_keyword]) + + describe "Publish an activity" do + setup do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "HI"}) + + relay_mock = { + Pleroma.Web.ActivityPub.Relay, + [], + [publish: fn _activity -> send(self(), :relay_publish) end] + } + + %{activity: activity, relay_mock: relay_mock} + end + + test "with relays active, it publishes to the relay", %{ + activity: activity, + relay_mock: relay_mock + } do + with_mocks([relay_mock]) do + Federator.publish(activity) + ObanHelpers.perform(all_enqueued(worker: PublisherWorker)) + end + + assert_received :relay_publish + end + + test "with relays deactivated, it does not publish to the relay", %{ + activity: activity, + relay_mock: relay_mock + } do + Pleroma.Config.put([:instance, :allow_relay], false) + + with_mocks([relay_mock]) do + Federator.publish(activity) + ObanHelpers.perform(all_enqueued(worker: PublisherWorker)) + end + + refute_received :relay_publish + end + end + + describe "Targets reachability filtering in `publish`" do + test "it federates only to reachable instances via AP" do + user = insert(:user) + + {inbox1, inbox2} = + {"https://domain.com/users/nick1/inbox", "https://domain2.com/users/nick2/inbox"} + + insert(:user, %{ + local: false, + nickname: "nick1@domain.com", + ap_id: "https://domain.com/users/nick1", + inbox: inbox1, + ap_enabled: true + }) + + insert(:user, %{ + local: false, + nickname: "nick2@domain2.com", + ap_id: "https://domain2.com/users/nick2", + inbox: inbox2, + ap_enabled: true + }) + + dt = NaiveDateTime.utc_now() + Instances.set_unreachable(inbox1, dt) + + Instances.set_consistently_unreachable(URI.parse(inbox2).host) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "HI @nick1@domain.com, @nick2@domain2.com!"}) + + expected_dt = NaiveDateTime.to_iso8601(dt) + + ObanHelpers.perform(all_enqueued(worker: PublisherWorker)) + + assert ObanHelpers.member?( + %{ + "op" => "publish_one", + "params" => %{"inbox" => inbox1, "unreachable_since" => expected_dt} + }, + all_enqueued(worker: PublisherWorker) + ) + end + end + + describe "Receive an activity" do + test "successfully processes incoming AP docs with correct origin" do + params = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => "http://mastodon.example.org/users/admin", + "type" => "Create", + "id" => "http://mastodon.example.org/users/admin/activities/1", + "object" => %{ + "type" => "Note", + "content" => "hi world!", + "id" => "http://mastodon.example.org/users/admin/objects/1", + "attributedTo" => "http://mastodon.example.org/users/admin" + }, + "to" => ["https://www.w3.org/ns/activitystreams#Public"] + } + + assert {:ok, job} = Federator.incoming_ap_doc(params) + assert {:ok, _activity} = ObanHelpers.perform(job) + + assert {:ok, job} = Federator.incoming_ap_doc(params) + assert {:error, :already_present} = ObanHelpers.perform(job) + end + + test "rejects incoming AP docs with incorrect origin" do + params = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => "https://niu.moe/users/rye", + "type" => "Create", + "id" => "http://mastodon.example.org/users/admin/activities/1", + "object" => %{ + "type" => "Note", + "content" => "hi world!", + "id" => "http://mastodon.example.org/users/admin/objects/1", + "attributedTo" => "http://mastodon.example.org/users/admin" + }, + "to" => ["https://www.w3.org/ns/activitystreams#Public"] + } + + assert {:ok, job} = Federator.incoming_ap_doc(params) + assert {:error, :origin_containment_failed} = ObanHelpers.perform(job) + end + + test "it does not crash if MRF rejects the post" do + Pleroma.Config.put([:mrf_keyword, :reject], ["lain"]) + + Pleroma.Config.put( + [:mrf, :policies], + Pleroma.Web.ActivityPub.MRF.KeywordPolicy + ) + + params = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + assert {:ok, job} = Federator.incoming_ap_doc(params) + assert {:error, _} = 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 @@ -0,0 +1,197 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Feed.TagControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + import SweetXml + + alias Pleroma.Object + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Feed.FeedView + + setup do: clear_config([:feed]) + + test "gets a feed (ATOM)", %{conn: conn} do + Pleroma.Config.put( + [:feed, :post_title], + %{max_length: 25, omission: "..."} + ) + + user = insert(:user) + {:ok, activity1} = CommonAPI.post(user, %{status: "yeah #PleromaArt"}) + + object = Object.normalize(activity1) + + object_data = + Map.put(object.data, "attachment", [ + %{ + "url" => [ + %{ + "href" => + "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + } + ]) + + object + |> Ecto.Changeset.change(data: object_data) + |> Pleroma.Repo.update() + + {:ok, activity2} = CommonAPI.post(user, %{status: "42 This is :moominmamma #PleromaArt"}) + + {:ok, _activity3} = CommonAPI.post(user, %{status: "This is :moominmamma"}) + + response = + conn + |> put_req_header("accept", "application/atom+xml") + |> get(tag_feed_path(conn, :feed, "pleromaart.atom")) + |> response(200) + + xml = parse(response) + + assert xpath(xml, ~x"//feed/title/text()") == '#pleromaart' + + assert xpath(xml, ~x"//feed/entry/title/text()"l) == [ + '42 This is :moominmamm...', + 'yeah #PleromaArt' + ] + + 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 + |> put_req_header("accept", "application/atom+xml") + |> get("/tags/pleromaart.atom", %{"max_id" => activity2.id}) + + assert get_resp_header(conn, "content-type") == ["application/atom+xml; charset=utf-8"] + resp = response(conn, 200) + xml = parse(resp) + + assert xpath(xml, ~x"//feed/title/text()") == '#pleromaart' + + assert xpath(xml, ~x"//feed/entry/title/text()"l) == [ + 'yeah #PleromaArt' + ] + end + + test "gets a feed (RSS)", %{conn: conn} do + Pleroma.Config.put( + [:feed, :post_title], + %{max_length: 25, omission: "..."} + ) + + user = insert(:user) + {:ok, activity1} = CommonAPI.post(user, %{status: "yeah #PleromaArt"}) + + object = Object.normalize(activity1) + + object_data = + Map.put(object.data, "attachment", [ + %{ + "url" => [ + %{ + "href" => + "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + } + ]) + + object + |> Ecto.Changeset.change(data: object_data) + |> Pleroma.Repo.update() + + {:ok, activity2} = CommonAPI.post(user, %{status: "42 This is :moominmamma #PleromaArt"}) + + {:ok, _activity3} = CommonAPI.post(user, %{status: "This is :moominmamma"}) + + response = + conn + |> put_req_header("accept", "application/rss+xml") + |> get(tag_feed_path(conn, :feed, "pleromaart.rss")) + |> response(200) + + xml = parse(response) + assert xpath(xml, ~x"//channel/title/text()") == '#pleromaart' + + assert xpath(xml, ~x"//channel/description/text()"s) == + "These are public toots tagged with #pleromaart. You can interact with them if you have an account anywhere in the fediverse." + + assert xpath(xml, ~x"//channel/link/text()") == + '#{Pleroma.Web.base_url()}/tags/pleromaart.rss' + + assert xpath(xml, ~x"//channel/webfeeds:logo/text()") == + '#{Pleroma.Web.base_url()}/static/logo.png' + + assert xpath(xml, ~x"//channel/item/title/text()"l) == [ + '42 This is :moominmamm...', + 'yeah #PleromaArt' + ] + + assert xpath(xml, ~x"//channel/item/pubDate/text()"sl) == [ + FeedView.pub_date(activity2.data["published"]), + FeedView.pub_date(activity1.data["published"]) + ] + + assert xpath(xml, ~x"//channel/item/enclosure/@url"sl) == [ + "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4" + ] + + obj1 = Object.normalize(activity1) + obj2 = Object.normalize(activity2) + + assert xpath(xml, ~x"//channel/item/description/text()"sl) == [ + HtmlEntities.decode(FeedView.activity_content(obj2.data)), + HtmlEntities.decode(FeedView.activity_content(obj1.data)) + ] + + response = + conn + |> put_req_header("accept", "application/rss+xml") + |> get(tag_feed_path(conn, :feed, "pleromaart")) + |> response(200) + + xml = parse(response) + assert xpath(xml, ~x"//channel/title/text()") == '#pleromaart' + + assert xpath(xml, ~x"//channel/description/text()"s) == + "These are public toots tagged with #pleromaart. You can interact with them if you have an account anywhere in the fediverse." + + conn = + conn + |> put_req_header("accept", "application/rss+xml") + |> get("/tags/pleromaart.rss", %{"max_id" => activity2.id}) + + assert get_resp_header(conn, "content-type") == ["application/rss+xml; charset=utf-8"] + resp = response(conn, 200) + xml = parse(resp) + + assert xpath(xml, ~x"//channel/title/text()") == '#pleromaart' + + assert xpath(xml, ~x"//channel/item/title/text()"l) == [ + 'yeah #PleromaArt' + ] + end + + describe "private instance" do + setup do: clear_config([:instance, :public]) + + test "returns 404 for tags feed", %{conn: conn} do + Config.put([:instance, :public], false) + + conn + |> put_req_header("accept", "application/rss+xml") + |> get(tag_feed_path(conn, :feed, "pleromaart")) + |> response(404) + end + end +end diff --git a/test/pleroma/web/feed/user_controller_test.exs b/test/pleroma/web/feed/user_controller_test.exs @@ -0,0 +1,265 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Feed.UserControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + import SweetXml + + alias Pleroma.Config + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + setup do: clear_config([:instance, :federating], true) + + describe "feed" do + setup do: clear_config([:feed]) + + test "gets an atom feed", %{conn: conn} do + Config.put( + [:feed, :post_title], + %{max_length: 10, omission: "..."} + ) + + activity = insert(:note_activity) + + note = + insert(:note, + data: %{ + "content" => "This is :moominmamma: note ", + "attachment" => [ + %{ + "url" => [ + %{"mediaType" => "image/png", "href" => "https://pleroma.gov/image.png"} + ] + } + ], + "inReplyTo" => activity.data["id"] + } + ) + + note_activity = insert(:note_activity, note: note) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + note2 = + insert(:note, + user: user, + data: %{ + "content" => "42 This is :moominmamma: note ", + "inReplyTo" => activity.data["id"] + } + ) + + note_activity2 = insert(:note_activity, note: note2) + object = Object.normalize(note_activity) + + 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 == ['42 This...', 'This is...'] + assert resp =~ object.data["content"] + + resp = + conn + |> put_req_header("accept", "application/atom+xml") + |> get("/users/#{user.nickname}/feed", %{"max_id" => note_activity2.id}) + |> response(200) + + activity_titles = + resp + |> SweetXml.parse() + |> SweetXml.xpath(~x"//entry/title/text()"l) + + assert activity_titles == ['This is...'] + end + + test "gets a rss feed", %{conn: conn} do + Pleroma.Config.put( + [:feed, :post_title], + %{max_length: 10, omission: "..."} + ) + + activity = insert(:note_activity) + + note = + insert(:note, + data: %{ + "content" => "This is :moominmamma: note ", + "attachment" => [ + %{ + "url" => [ + %{"mediaType" => "image/png", "href" => "https://pleroma.gov/image.png"} + ] + } + ], + "inReplyTo" => activity.data["id"] + } + ) + + note_activity = insert(:note_activity, note: note) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + note2 = + insert(:note, + user: user, + data: %{ + "content" => "42 This is :moominmamma: note ", + "inReplyTo" => activity.data["id"] + } + ) + + note_activity2 = insert(:note_activity, note: note2) + object = Object.normalize(note_activity) + + resp = + conn + |> put_req_header("accept", "application/rss+xml") + |> get("/users/#{user.nickname}/feed.rss") + |> response(200) + + activity_titles = + resp + |> SweetXml.parse() + |> SweetXml.xpath(~x"//item/title/text()"l) + + assert activity_titles == ['42 This...', 'This is...'] + assert resp =~ object.data["content"] + + resp = + conn + |> put_req_header("accept", "application/rss+xml") + |> get("/users/#{user.nickname}/feed.rss", %{"max_id" => note_activity2.id}) + |> response(200) + + activity_titles = + resp + |> SweetXml.parse() + |> SweetXml.xpath(~x"//item/title/text()"l) + + assert activity_titles == ['This is...'] + end + + test "returns 404 for a missing feed", %{conn: conn} do + conn = + conn + |> put_req_header("accept", "application/atom+xml") + |> get(user_feed_path(conn, :feed, "nonexisting")) + + assert response(conn, 404) + end + + test "returns feed with public and unlisted activities", %{conn: conn} do + user = insert(:user) + + {:ok, _} = CommonAPI.post(user, %{status: "public", visibility: "public"}) + {:ok, _} = CommonAPI.post(user, %{status: "direct", visibility: "direct"}) + {:ok, _} = CommonAPI.post(user, %{status: "unlisted", visibility: "unlisted"}) + {:ok, _} = CommonAPI.post(user, %{status: "private", visibility: "private"}) + + 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) + |> Enum.sort() + + assert activity_titles == ['public', 'unlisted'] + end + + test "returns 404 when the user is remote", %{conn: conn} do + user = insert(:user, local: false) + + {:ok, _} = CommonAPI.post(user, %{status: "test"}) + + assert conn + |> put_req_header("accept", "application/atom+xml") + |> get(user_feed_path(conn, :feed, user.nickname)) + |> response(404) + end + end + + # Note: see ActivityPubControllerTest for JSON format tests + describe "feed_redirect" do + test "with html format, it redirects to user feed", %{conn: conn} do + note_activity = insert(:note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + response = + conn + |> get("/users/#{user.nickname}") + |> response(200) + + assert response == + Pleroma.Web.Fallback.RedirectController.redirector_with_meta( + conn, + %{user: user} + ).resp_body + end + + test "with html format, it returns error when user is not found", %{conn: conn} do + response = + conn + |> get("/users/jimm") + |> json_response(404) + + assert response == %{"error" => "Not found"} + end + + test "with non-html / non-json format, it redirects to user feed in atom format", %{ + conn: conn + } do + note_activity = insert(:note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + conn = + conn + |> put_req_header("accept", "application/xml") + |> get("/users/#{user.nickname}") + + assert conn.status == 302 + assert redirected_to(conn) == "#{Pleroma.Web.base_url()}/users/#{user.nickname}/feed.atom" + end + + test "with non-html / non-json format, it returns error when user is not found", %{conn: conn} do + response = + conn + |> put_req_header("accept", "application/xml") + |> get(user_feed_path(conn, :feed, "jimm")) + |> response(404) + + assert response == ~S({"error":"Not found"}) + end + end + + describe "private instance" do + setup do: clear_config([:instance, :public]) + + test "returns 404 for user feed", %{conn: conn} do + Config.put([:instance, :public], false) + user = insert(:user) + + {:ok, _} = CommonAPI.post(user, %{status: "test"}) + + assert conn + |> put_req_header("accept", "application/atom+xml") + |> get(user_feed_path(conn, :feed, user.nickname)) + |> response(404) + end + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -0,0 +1,1536 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.InternalFetchActor + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.OAuth.Token + + import Pleroma.Factory + + describe "account fetching" do + test "works by id" do + %User{id: user_id} = insert(:user) + + assert %{"id" => ^user_id} = + build_conn() + |> get("/api/v1/accounts/#{user_id}") + |> json_response_and_validate_schema(200) + + assert %{"error" => "Can't find user"} = + build_conn() + |> get("/api/v1/accounts/-1") + |> json_response_and_validate_schema(404) + end + + test "works by nickname" do + user = insert(:user) + + assert %{"id" => user_id} = + build_conn() + |> get("/api/v1/accounts/#{user.nickname}") + |> json_response_and_validate_schema(200) + end + + test "works by nickname for remote users" do + clear_config([:instance, :limit_to_local_content], false) + + user = insert(:user, nickname: "user@example.com", local: false) + + assert %{"id" => user_id} = + build_conn() + |> get("/api/v1/accounts/#{user.nickname}") + |> json_response_and_validate_schema(200) + end + + test "respects limit_to_local_content == :all for remote user nicknames" do + clear_config([:instance, :limit_to_local_content], :all) + + user = insert(:user, nickname: "user@example.com", local: false) + + assert build_conn() + |> get("/api/v1/accounts/#{user.nickname}") + |> json_response_and_validate_schema(404) + end + + test "respects limit_to_local_content == :unauthenticated for remote user nicknames" do + clear_config([:instance, :limit_to_local_content], :unauthenticated) + + user = insert(:user, nickname: "user@example.com", local: false) + reading_user = insert(:user) + + conn = + build_conn() + |> get("/api/v1/accounts/#{user.nickname}") + + assert json_response_and_validate_schema(conn, 404) + + conn = + build_conn() + |> assign(:user, reading_user) + |> assign(:token, insert(:oauth_token, user: reading_user, scopes: ["read:accounts"])) + |> get("/api/v1/accounts/#{user.nickname}") + + assert %{"id" => id} = json_response_and_validate_schema(conn, 200) + assert id == user.id + end + + test "accounts fetches correct account for nicknames beginning with numbers", %{conn: conn} do + # Need to set an old-style integer ID to reproduce the problem + # (these are no longer assigned to new accounts but were preserved + # for existing accounts during the migration to flakeIDs) + user_one = insert(:user, %{id: 1212}) + user_two = insert(:user, %{nickname: "#{user_one.id}garbage"}) + + acc_one = + conn + |> get("/api/v1/accounts/#{user_one.id}") + |> json_response_and_validate_schema(:ok) + + acc_two = + conn + |> get("/api/v1/accounts/#{user_two.nickname}") + |> json_response_and_validate_schema(:ok) + + acc_three = + conn + |> get("/api/v1/accounts/#{user_two.id}") + |> json_response_and_validate_schema(:ok) + + refute acc_one == acc_two + assert acc_two == acc_three + end + + test "returns 404 when user is invisible", %{conn: conn} do + user = insert(:user, %{invisible: true}) + + assert %{"error" => "Can't find user"} = + conn + |> get("/api/v1/accounts/#{user.nickname}") + |> json_response_and_validate_schema(404) + end + + test "returns 404 for internal.fetch actor", %{conn: conn} do + %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor() + + assert %{"error" => "Can't find user"} = + conn + |> get("/api/v1/accounts/internal.fetch") + |> json_response_and_validate_schema(404) + end + + test "returns 404 for deactivated user", %{conn: conn} do + user = insert(:user, deactivated: true) + + assert %{"error" => "Can't find user"} = + conn + |> get("/api/v1/accounts/#{user.id}") + |> json_response_and_validate_schema(:not_found) + end + end + + defp local_and_remote_users do + local = insert(:user) + remote = insert(:user, local: false) + {:ok, local: local, remote: remote} + end + + describe "user fetching with restrict unauthenticated profiles for local and remote" do + setup do: local_and_remote_users() + + setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) + + setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + assert %{"error" => "This API requires an authenticated user"} == + conn + |> get("/api/v1/accounts/#{local.id}") + |> json_response_and_validate_schema(:unauthorized) + + assert %{"error" => "This API requires an authenticated user"} == + conn + |> get("/api/v1/accounts/#{remote.id}") + |> json_response_and_validate_schema(:unauthorized) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + end + end + + describe "user fetching with restrict unauthenticated profiles for local" do + setup do: local_and_remote_users() + + setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/accounts/#{local.id}") + + assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ + "error" => "This API requires an authenticated user" + } + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + end + end + + describe "user fetching with restrict unauthenticated profiles for remote" do + setup do: local_and_remote_users() + + setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/accounts/#{local.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}") + + assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ + "error" => "This API requires an authenticated user" + } + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + end + end + + describe "user timelines" do + setup do: oauth_access(["read:statuses"]) + + test "works with announces that are just addressed to public", %{conn: conn} do + user = insert(:user, ap_id: "https://honktest/u/test", local: false) + other_user = insert(:user) + + {:ok, post} = CommonAPI.post(other_user, %{status: "bonkeronk"}) + + {:ok, announce, _} = + %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => "https://honktest/u/test", + "id" => "https://honktest/u/test/bonk/1793M7B9MQ48847vdx", + "object" => post.data["object"], + "published" => "2019-06-25T19:33:58Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "type" => "Announce" + } + |> ActivityPub.persist(local: false) + + assert resp = + conn + |> get("/api/v1/accounts/#{user.id}/statuses") + |> json_response_and_validate_schema(200) + + assert [%{"id" => id}] = resp + assert id == announce.id + end + + test "deactivated user", %{conn: conn} do + user = insert(:user, deactivated: true) + + assert %{"error" => "Can't find user"} == + conn + |> get("/api/v1/accounts/#{user.id}/statuses") + |> json_response_and_validate_schema(:not_found) + end + + test "returns 404 when user is invisible", %{conn: conn} do + user = insert(:user, %{invisible: true}) + + assert %{"error" => "Can't find user"} = + conn + |> get("/api/v1/accounts/#{user.id}") + |> json_response_and_validate_schema(404) + end + + test "respects blocks", %{user: user_one, conn: conn} do + user_two = insert(:user) + user_three = insert(:user) + + User.block(user_one, user_two) + + {:ok, activity} = CommonAPI.post(user_two, %{status: "User one sux0rz"}) + {:ok, repeat} = CommonAPI.repeat(activity.id, user_three) + + assert resp = + conn + |> get("/api/v1/accounts/#{user_two.id}/statuses") + |> json_response_and_validate_schema(200) + + assert [%{"id" => id}] = resp + assert id == activity.id + + # Even a blocked user will deliver the full user timeline, there would be + # no point in looking at a blocked users timeline otherwise + assert resp = + conn + |> get("/api/v1/accounts/#{user_two.id}/statuses") + |> json_response_and_validate_schema(200) + + assert [%{"id" => id}] = resp + assert id == activity.id + + # Third user's timeline includes the repeat when viewed by unauthenticated user + resp = + build_conn() + |> get("/api/v1/accounts/#{user_three.id}/statuses") + |> json_response_and_validate_schema(200) + + assert [%{"id" => id}] = resp + assert id == repeat.id + + # When viewing a third user's timeline, the blocked users' statuses will NOT be shown + resp = get(conn, "/api/v1/accounts/#{user_three.id}/statuses") + + assert [] == json_response_and_validate_schema(resp, 200) + end + + test "gets users statuses", %{conn: conn} do + user_one = insert(:user) + user_two = insert(:user) + user_three = insert(:user) + + {:ok, _user_three} = User.follow(user_three, user_one) + + {:ok, activity} = CommonAPI.post(user_one, %{status: "HI!!!"}) + + {:ok, direct_activity} = + CommonAPI.post(user_one, %{ + status: "Hi, @#{user_two.nickname}.", + visibility: "direct" + }) + + {:ok, private_activity} = + CommonAPI.post(user_one, %{status: "private", visibility: "private"}) + + # TODO!!! + 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) + + resp = + conn + |> assign(:user, user_two) + |> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"])) + |> get("/api/v1/accounts/#{user_one.id}/statuses") + |> json_response_and_validate_schema(200) + + assert [%{"id" => id_one}, %{"id" => id_two}] = resp + assert id_one == to_string(direct_activity.id) + assert id_two == to_string(activity.id) + + resp = + conn + |> assign(:user, user_three) + |> assign(:token, insert(:oauth_token, user: user_three, scopes: ["read:statuses"])) + |> get("/api/v1/accounts/#{user_one.id}/statuses") + |> json_response_and_validate_schema(200) + + assert [%{"id" => id_one}, %{"id" => id_two}] = resp + assert id_one == to_string(private_activity.id) + assert id_two == to_string(activity.id) + end + + test "unimplemented pinned statuses feature", %{conn: conn} do + note = insert(:note_activity) + user = User.get_cached_by_ap_id(note.data["actor"]) + + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?pinned=true") + + assert json_response_and_validate_schema(conn, 200) == [] + 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"]) + other_user = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, %{id: media_id}} = ActivityPub.upload(file, actor: user.ap_id) + + {:ok, %{id: image_post_id}} = CommonAPI.post(user, %{status: "cofe", media_ids: [media_id]}) + + {:ok, %{id: media_id}} = ActivityPub.upload(file, actor: other_user.ap_id) + + {:ok, %{id: other_image_post_id}} = + CommonAPI.post(other_user, %{status: "cofe2", media_ids: [media_id]}) + + {:ok, _announce} = CommonAPI.repeat(other_image_post_id, user) + + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?only_media=true") + + assert [%{"id" => ^image_post_id}] = json_response_and_validate_schema(conn, 200) + + conn = get(build_conn(), "/api/v1/accounts/#{user.id}/statuses?only_media=1") + + assert [%{"id" => ^image_post_id}] = json_response_and_validate_schema(conn, 200) + end + + test "gets a user's statuses without reblogs", %{user: user, conn: conn} do + {:ok, %{id: post_id}} = CommonAPI.post(user, %{status: "HI!!!"}) + {:ok, _} = CommonAPI.repeat(post_id, user) + + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_reblogs=true") + assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200) + + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_reblogs=1") + assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200) + end + + test "filters user's statuses by a hashtag", %{user: user, conn: conn} do + {:ok, %{id: post_id}} = CommonAPI.post(user, %{status: "#hashtag"}) + {:ok, _post} = CommonAPI.post(user, %{status: "hashtag"}) + + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?tagged=hashtag") + assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200) + end + + test "the user views their own timelines and excludes direct messages", %{ + user: user, + conn: conn + } do + {:ok, %{id: public_activity_id}} = + CommonAPI.post(user, %{status: ".", visibility: "public"}) + + {:ok, _direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_visibilities[]=direct") + assert [%{"id" => ^public_activity_id}] = json_response_and_validate_schema(conn, 200) + end + end + + defp local_and_remote_activities(%{local: local, remote: remote}) do + insert(:note_activity, user: local) + insert(:note_activity, user: remote, local: false) + + :ok + end + + describe "statuses with restrict unauthenticated profiles for local and remote" do + setup do: local_and_remote_users() + setup :local_and_remote_activities + + setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) + + setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + assert %{"error" => "This API requires an authenticated user"} == + conn + |> get("/api/v1/accounts/#{local.id}/statuses") + |> json_response_and_validate_schema(:unauthorized) + + assert %{"error" => "This API requires an authenticated user"} == + conn + |> get("/api/v1/accounts/#{remote.id}/statuses") + |> json_response_and_validate_schema(:unauthorized) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + end + end + + describe "statuses with restrict unauthenticated profiles for local" do + setup do: local_and_remote_users() + setup :local_and_remote_activities + + setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + assert %{"error" => "This API requires an authenticated user"} == + conn + |> get("/api/v1/accounts/#{local.id}/statuses") + |> json_response_and_validate_schema(:unauthorized) + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + end + end + + describe "statuses with restrict unauthenticated profiles for remote" do + setup do: local_and_remote_users() + setup :local_and_remote_activities + + setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + + assert %{"error" => "This API requires an authenticated user"} == + conn + |> get("/api/v1/accounts/#{remote.id}/statuses") + |> json_response_and_validate_schema(:unauthorized) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + end + end + + describe "followers" do + setup do: oauth_access(["read:accounts"]) + + test "getting followers", %{user: user, conn: conn} do + other_user = insert(:user) + {:ok, %{id: user_id}} = User.follow(user, other_user) + + conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers") + + assert [%{"id" => ^user_id}] = json_response_and_validate_schema(conn, 200) + end + + test "getting followers, hide_followers", %{user: user, conn: conn} do + other_user = insert(:user, hide_followers: true) + {:ok, _user} = User.follow(user, other_user) + + conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers") + + assert [] == json_response_and_validate_schema(conn, 200) + end + + test "getting followers, hide_followers, same user requesting" do + user = insert(:user) + other_user = insert(:user, hide_followers: true) + {:ok, _user} = User.follow(user, other_user) + + conn = + build_conn() + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) + |> get("/api/v1/accounts/#{other_user.id}/followers") + + refute [] == json_response_and_validate_schema(conn, 200) + end + + test "getting followers, pagination", %{user: user, conn: conn} do + {:ok, %User{id: follower1_id}} = :user |> insert() |> User.follow(user) + {:ok, %User{id: follower2_id}} = :user |> insert() |> User.follow(user) + {:ok, %User{id: follower3_id}} = :user |> insert() |> User.follow(user) + + assert [%{"id" => ^follower3_id}, %{"id" => ^follower2_id}] = + conn + |> get("/api/v1/accounts/#{user.id}/followers?since_id=#{follower1_id}") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^follower2_id}, %{"id" => ^follower1_id}] = + conn + |> get("/api/v1/accounts/#{user.id}/followers?max_id=#{follower3_id}") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^follower2_id}, %{"id" => ^follower1_id}] = + conn + |> get( + "/api/v1/accounts/#{user.id}/followers?id=#{user.id}&limit=20&max_id=#{ + follower3_id + }" + ) + |> json_response_and_validate_schema(200) + + res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?limit=1&max_id=#{follower3_id}") + + assert [%{"id" => ^follower2_id}] = json_response_and_validate_schema(res_conn, 200) + + assert [link_header] = get_resp_header(res_conn, "link") + assert link_header =~ ~r/min_id=#{follower2_id}/ + assert link_header =~ ~r/max_id=#{follower2_id}/ + end + end + + describe "following" do + setup do: oauth_access(["read:accounts"]) + + test "getting following", %{user: user, conn: conn} do + other_user = insert(:user) + {:ok, user} = User.follow(user, other_user) + + conn = get(conn, "/api/v1/accounts/#{user.id}/following") + + assert [%{"id" => id}] = json_response_and_validate_schema(conn, 200) + assert id == to_string(other_user.id) + end + + test "getting following, hide_follows, other user requesting" do + user = insert(:user, hide_follows: true) + other_user = insert(:user) + {:ok, user} = User.follow(user, other_user) + + conn = + build_conn() + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) + |> get("/api/v1/accounts/#{user.id}/following") + + assert [] == json_response_and_validate_schema(conn, 200) + end + + test "getting following, hide_follows, same user requesting" do + user = insert(:user, hide_follows: true) + other_user = insert(:user) + {:ok, user} = User.follow(user, other_user) + + conn = + build_conn() + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["read:accounts"])) + |> get("/api/v1/accounts/#{user.id}/following") + + refute [] == json_response_and_validate_schema(conn, 200) + end + + test "getting following, pagination", %{user: user, conn: conn} do + following1 = insert(:user) + following2 = insert(:user) + following3 = insert(:user) + {:ok, _} = User.follow(user, following1) + {:ok, _} = User.follow(user, following2) + {:ok, _} = User.follow(user, following3) + + res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?since_id=#{following1.id}") + + assert [%{"id" => id3}, %{"id" => id2}] = json_response_and_validate_schema(res_conn, 200) + assert id3 == following3.id + assert id2 == following2.id + + res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?max_id=#{following3.id}") + + assert [%{"id" => id2}, %{"id" => id1}] = json_response_and_validate_schema(res_conn, 200) + assert id2 == following2.id + assert id1 == following1.id + + res_conn = + get( + conn, + "/api/v1/accounts/#{user.id}/following?id=#{user.id}&limit=20&max_id=#{following3.id}" + ) + + assert [%{"id" => id2}, %{"id" => id1}] = json_response_and_validate_schema(res_conn, 200) + assert id2 == following2.id + assert id1 == following1.id + + res_conn = + get(conn, "/api/v1/accounts/#{user.id}/following?limit=1&max_id=#{following3.id}") + + assert [%{"id" => id2}] = json_response_and_validate_schema(res_conn, 200) + assert id2 == following2.id + + assert [link_header] = get_resp_header(res_conn, "link") + assert link_header =~ ~r/min_id=#{following2.id}/ + assert link_header =~ ~r/max_id=#{following2.id}/ + end + end + + describe "follow/unfollow" do + setup do: oauth_access(["follow"]) + + test "following / unfollowing a user", %{conn: conn} do + %{id: other_user_id, nickname: other_user_nickname} = insert(:user) + + assert %{"id" => _id, "following" => true} = + conn + |> post("/api/v1/accounts/#{other_user_id}/follow") + |> json_response_and_validate_schema(200) + + assert %{"id" => _id, "following" => false} = + conn + |> post("/api/v1/accounts/#{other_user_id}/unfollow") + |> json_response_and_validate_schema(200) + + assert %{"id" => ^other_user_id} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/follows", %{"uri" => other_user_nickname}) + |> json_response_and_validate_schema(200) + end + + test "cancelling follow request", %{conn: conn} do + %{id: other_user_id} = insert(:user, %{locked: true}) + + assert %{"id" => ^other_user_id, "following" => false, "requested" => true} = + conn + |> post("/api/v1/accounts/#{other_user_id}/follow") + |> json_response_and_validate_schema(:ok) + + assert %{"id" => ^other_user_id, "following" => false, "requested" => false} = + conn + |> post("/api/v1/accounts/#{other_user_id}/unfollow") + |> json_response_and_validate_schema(:ok) + end + + test "following without reblogs" do + %{conn: conn} = oauth_access(["follow", "read:statuses"]) + followed = insert(:user) + other_user = insert(:user) + + ret_conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: false}) + + assert %{"showing_reblogs" => false} = json_response_and_validate_schema(ret_conn, 200) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) + {:ok, %{id: reblog_id}} = CommonAPI.repeat(activity.id, followed) + + assert [] == + conn + |> get("/api/v1/timelines/home") + |> json_response(200) + + assert %{"showing_reblogs" => true} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: true}) + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^reblog_id}] = + conn + |> get("/api/v1/timelines/home") + |> json_response(200) + end + + test "following with reblogs" do + %{conn: conn} = oauth_access(["follow", "read:statuses"]) + followed = insert(:user) + other_user = insert(:user) + + ret_conn = post(conn, "/api/v1/accounts/#{followed.id}/follow") + + assert %{"showing_reblogs" => true} = json_response_and_validate_schema(ret_conn, 200) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) + {:ok, %{id: reblog_id}} = CommonAPI.repeat(activity.id, followed) + + assert [%{"id" => ^reblog_id}] = + conn + |> get("/api/v1/timelines/home") + |> json_response(200) + + assert %{"showing_reblogs" => false} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: false}) + |> json_response_and_validate_schema(200) + + assert [] == + conn + |> get("/api/v1/timelines/home") + |> json_response(200) + end + + test "following / unfollowing errors", %{user: user, conn: conn} do + # self follow + conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow") + + assert %{"error" => "Can not follow yourself"} = + json_response_and_validate_schema(conn_res, 400) + + # self unfollow + user = User.get_cached_by_id(user.id) + conn_res = post(conn, "/api/v1/accounts/#{user.id}/unfollow") + + assert %{"error" => "Can not unfollow yourself"} = + json_response_and_validate_schema(conn_res, 400) + + # self follow via uri + user = User.get_cached_by_id(user.id) + + assert %{"error" => "Can not follow yourself"} = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/follows", %{"uri" => user.nickname}) + |> json_response_and_validate_schema(400) + + # follow non existing user + conn_res = post(conn, "/api/v1/accounts/doesntexist/follow") + assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404) + + # follow non existing user via uri + conn_res = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/follows", %{"uri" => "doesntexist"}) + + assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404) + + # unfollow non existing user + conn_res = post(conn, "/api/v1/accounts/doesntexist/unfollow") + assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404) + end + end + + describe "mute/unmute" do + setup do: oauth_access(["write:mutes"]) + + test "with notifications", %{conn: conn} do + other_user = insert(:user) + + assert %{"id" => _id, "muting" => true, "muting_notifications" => true} = + conn + |> post("/api/v1/accounts/#{other_user.id}/mute") + |> json_response_and_validate_schema(200) + + conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute") + + assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = + json_response_and_validate_schema(conn, 200) + end + + test "without notifications", %{conn: conn} do + other_user = insert(:user) + + ret_conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"}) + + assert %{"id" => _id, "muting" => true, "muting_notifications" => false} = + json_response_and_validate_schema(ret_conn, 200) + + conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute") + + assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = + json_response_and_validate_schema(conn, 200) + end + end + + describe "pinned statuses" do + setup do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"}) + %{conn: conn} = oauth_access(["read:statuses"], user: user) + + [conn: conn, user: user, activity: activity] + end + + test "returns pinned statuses", %{conn: conn, user: user, activity: %{id: activity_id}} do + {:ok, _} = CommonAPI.pin(activity_id, user) + + assert [%{"id" => ^activity_id, "pinned" => true}] = + conn + |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") + |> json_response_and_validate_schema(200) + end + end + + test "blocking / unblocking a user" do + %{conn: conn} = oauth_access(["follow"]) + other_user = insert(:user) + + ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/block") + + assert %{"id" => _id, "blocking" => true} = json_response_and_validate_schema(ret_conn, 200) + + conn = post(conn, "/api/v1/accounts/#{other_user.id}/unblock") + + assert %{"id" => _id, "blocking" => false} = json_response_and_validate_schema(conn, 200) + end + + describe "create account by app" do + setup do + valid_params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + agreement: true + } + + [valid_params: valid_params] + end + + test "registers and logs in without :account_activation_required / :account_approval_required", + %{conn: conn} do + clear_config([:instance, :account_activation_required], false) + clear_config([:instance, :account_approval_required], false) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/apps", %{ + client_name: "client_name", + redirect_uris: "urn:ietf:wg:oauth:2.0:oob", + scopes: "read, write, follow" + }) + + assert %{ + "client_id" => client_id, + "client_secret" => client_secret, + "id" => _, + "name" => "client_name", + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "vapid_key" => _, + "website" => nil + } = json_response_and_validate_schema(conn, 200) + + conn = + post(conn, "/oauth/token", %{ + grant_type: "client_credentials", + client_id: client_id, + client_secret: client_secret + }) + + assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = + json_response(conn, 200) + + assert token + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + assert refresh + assert scope == "read write follow" + + clear_config([User, :email_blacklist], ["example.org"]) + + params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + bio: "Test Bio", + agreement: true + } + + conn = + build_conn() + |> put_req_header("content-type", "multipart/form-data") + |> put_req_header("authorization", "Bearer " <> token) + |> post("/api/v1/accounts", params) + + assert %{"error" => "{\"email\":[\"Invalid email\"]}"} = + json_response_and_validate_schema(conn, 400) + + Pleroma.Config.put([User, :email_blacklist], []) + + conn = + build_conn() + |> put_req_header("content-type", "multipart/form-data") + |> put_req_header("authorization", "Bearer " <> token) + |> post("/api/v1/accounts", params) + + %{ + "access_token" => token, + "created_at" => _created_at, + "scope" => ^scope, + "token_type" => "Bearer" + } = json_response_and_validate_schema(conn, 200) + + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + user = Repo.preload(token_from_db, :user).user + + assert user + refute user.confirmation_pending + refute user.approval_pending + end + + test "registers but does not log in with :account_activation_required", %{conn: conn} do + clear_config([:instance, :account_activation_required], true) + clear_config([:instance, :account_approval_required], false) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/apps", %{ + client_name: "client_name", + redirect_uris: "urn:ietf:wg:oauth:2.0:oob", + scopes: "read, write, follow" + }) + + assert %{ + "client_id" => client_id, + "client_secret" => client_secret, + "id" => _, + "name" => "client_name", + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "vapid_key" => _, + "website" => nil + } = json_response_and_validate_schema(conn, 200) + + conn = + post(conn, "/oauth/token", %{ + grant_type: "client_credentials", + client_id: client_id, + client_secret: client_secret + }) + + assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = + json_response(conn, 200) + + assert token + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + assert refresh + assert scope == "read write follow" + + conn = + build_conn() + |> put_req_header("content-type", "multipart/form-data") + |> put_req_header("authorization", "Bearer " <> token) + |> post("/api/v1/accounts", %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + bio: "Test Bio", + agreement: true + }) + + response = json_response_and_validate_schema(conn, 200) + assert %{"identifier" => "missing_confirmed_email"} = response + refute response["access_token"] + refute response["token_type"] + + user = Repo.get_by(User, email: "lain@example.org") + assert user.confirmation_pending + end + + test "registers but does not log in with :account_approval_required", %{conn: conn} do + clear_config([:instance, :account_approval_required], true) + clear_config([:instance, :account_activation_required], false) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/apps", %{ + client_name: "client_name", + redirect_uris: "urn:ietf:wg:oauth:2.0:oob", + scopes: "read, write, follow" + }) + + assert %{ + "client_id" => client_id, + "client_secret" => client_secret, + "id" => _, + "name" => "client_name", + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "vapid_key" => _, + "website" => nil + } = json_response_and_validate_schema(conn, 200) + + conn = + post(conn, "/oauth/token", %{ + grant_type: "client_credentials", + client_id: client_id, + client_secret: client_secret + }) + + assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = + json_response(conn, 200) + + assert token + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + assert refresh + assert scope == "read write follow" + + conn = + build_conn() + |> put_req_header("content-type", "multipart/form-data") + |> put_req_header("authorization", "Bearer " <> token) + |> post("/api/v1/accounts", %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + bio: "Test Bio", + agreement: true, + reason: "I'm a cool dude, bro" + }) + + response = json_response_and_validate_schema(conn, 200) + assert %{"identifier" => "awaiting_approval"} = response + refute response["access_token"] + refute response["token_type"] + + user = Repo.get_by(User, email: "lain@example.org") + + assert user.approval_pending + 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 + _user = insert(:user, email: "lain@example.org") + app_token = insert(:oauth_token, user: nil) + + res = + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts", valid_params) + + assert json_response_and_validate_schema(res, 400) == %{ + "error" => "{\"email\":[\"has already been taken\"]}" + } + end + + test "returns bad_request if missing required params", %{ + conn: conn, + valid_params: valid_params + } do + app_token = insert(:oauth_token, user: nil) + + conn = + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) + |> put_req_header("content-type", "application/json") + + res = post(conn, "/api/v1/accounts", valid_params) + assert json_response_and_validate_schema(res, 200) + + [{127, 0, 0, 1}, {127, 0, 0, 2}, {127, 0, 0, 3}, {127, 0, 0, 4}] + |> Stream.zip(Map.delete(valid_params, :email)) + |> Enum.each(fn {ip, {attr, _}} -> + res = + conn + |> Map.put(:remote_ip, ip) + |> post("/api/v1/accounts", Map.delete(valid_params, attr)) + |> json_response_and_validate_schema(400) + + assert res == %{ + "error" => "Missing field: #{attr}.", + "errors" => [ + %{ + "message" => "Missing field: #{attr}", + "source" => %{"pointer" => "/#{attr}"}, + "title" => "Invalid value" + } + ] + } + end) + end + + test "returns bad_request if missing email params when :account_activation_required is enabled", + %{conn: conn, valid_params: valid_params} do + clear_config([:instance, :account_activation_required], true) + + app_token = insert(:oauth_token, user: nil) + + conn = + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) + |> put_req_header("content-type", "application/json") + + res = + conn + |> Map.put(:remote_ip, {127, 0, 0, 5}) + |> post("/api/v1/accounts", Map.delete(valid_params, :email)) + + assert json_response_and_validate_schema(res, 400) == + %{"error" => "Missing parameter: email"} + + res = + conn + |> Map.put(:remote_ip, {127, 0, 0, 6}) + |> post("/api/v1/accounts", Map.put(valid_params, :email, "")) + + assert json_response_and_validate_schema(res, 400) == %{ + "error" => "{\"email\":[\"can't be blank\"]}" + } + end + + test "allow registration without an email", %{conn: conn, valid_params: valid_params} do + app_token = insert(:oauth_token, user: nil) + conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token) + + res = + conn + |> put_req_header("content-type", "application/json") + |> Map.put(:remote_ip, {127, 0, 0, 7}) + |> post("/api/v1/accounts", Map.delete(valid_params, :email)) + + assert json_response_and_validate_schema(res, 200) + end + + test "allow registration with an empty email", %{conn: conn, valid_params: valid_params} do + app_token = insert(:oauth_token, user: nil) + conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token) + + res = + conn + |> put_req_header("content-type", "application/json") + |> Map.put(:remote_ip, {127, 0, 0, 8}) + |> post("/api/v1/accounts", Map.put(valid_params, :email, "")) + + assert json_response_and_validate_schema(res, 200) + end + + test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_params} do + res = + conn + |> put_req_header("authorization", "Bearer " <> "invalid-token") + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/accounts", valid_params) + + assert json_response_and_validate_schema(res, 403) == %{"error" => "Invalid credentials"} + end + + test "registration from trusted app" do + clear_config([Pleroma.Captcha, :enabled], true) + app = insert(:oauth_app, trusted: true, scopes: ["read", "write", "follow", "push"]) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "client_credentials", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token, "token_type" => "Bearer"} = json_response(conn, 200) + + response = + build_conn() + |> Plug.Conn.put_req_header("authorization", "Bearer " <> token) + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/accounts", %{ + nickname: "nickanme", + agreement: true, + email: "email@example.com", + fullname: "Lain", + username: "Lain", + password: "some_password", + confirm: "some_password" + }) + |> json_response_and_validate_schema(200) + + assert %{ + "access_token" => access_token, + "created_at" => _, + "scope" => "read write follow push", + "token_type" => "Bearer" + } = response + + response = + build_conn() + |> Plug.Conn.put_req_header("authorization", "Bearer " <> access_token) + |> get("/api/v1/accounts/verify_credentials") + |> json_response_and_validate_schema(200) + + assert %{ + "acct" => "Lain", + "bot" => false, + "display_name" => "Lain", + "follow_requests_count" => 0, + "followers_count" => 0, + "following_count" => 0, + "locked" => false, + "note" => "", + "source" => %{ + "fields" => [], + "note" => "", + "pleroma" => %{ + "actor_type" => "Person", + "discoverable" => false, + "no_rich_text" => false, + "show_role" => true + }, + "privacy" => "public", + "sensitive" => false + }, + "statuses_count" => 0, + "username" => "Lain" + } = response + end + end + + describe "create account by app / rate limit" do + setup do: clear_config([:rate_limit, :app_account_creation], {10_000, 2}) + + test "respects rate limit setting", %{conn: conn} do + app_token = insert(:oauth_token, user: nil) + + conn = + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) + |> Map.put(:remote_ip, {15, 15, 15, 15}) + |> put_req_header("content-type", "multipart/form-data") + + for i <- 1..2 do + conn = + conn + |> post("/api/v1/accounts", %{ + username: "#{i}lain", + email: "#{i}lain@example.org", + password: "PlzDontHackLain", + agreement: true + }) + + %{ + "access_token" => token, + "created_at" => _created_at, + "scope" => _scope, + "token_type" => "Bearer" + } = json_response_and_validate_schema(conn, 200) + + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + token_from_db = Repo.preload(token_from_db, :user) + assert token_from_db.user + end + + conn = + post(conn, "/api/v1/accounts", %{ + username: "6lain", + email: "6lain@example.org", + password: "PlzDontHackLain", + agreement: true + }) + + assert json_response_and_validate_schema(conn, :too_many_requests) == %{ + "error" => "Throttled" + } + end + end + + describe "create account with enabled captcha" do + setup %{conn: conn} do + app_token = insert(:oauth_token, user: nil) + + conn = + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) + |> put_req_header("content-type", "multipart/form-data") + + [conn: conn] + end + + setup do: clear_config([Pleroma.Captcha, :enabled], true) + + test "creates an account and returns 200 if captcha is valid", %{conn: conn} do + %{token: token, answer_data: answer_data} = Pleroma.Captcha.new() + + params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + agreement: true, + captcha_solution: Pleroma.Captcha.Mock.solution(), + captcha_token: token, + captcha_answer_data: answer_data + } + + assert %{ + "access_token" => access_token, + "created_at" => _, + "scope" => "read", + "token_type" => "Bearer" + } = + conn + |> post("/api/v1/accounts", params) + |> json_response_and_validate_schema(:ok) + + assert Token |> Repo.get_by(token: access_token) |> Repo.preload(:user) |> Map.get(:user) + + Cachex.del(:used_captcha_cache, token) + end + + test "returns 400 if any captcha field is not provided", %{conn: conn} do + captcha_fields = [:captcha_solution, :captcha_token, :captcha_answer_data] + + valid_params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + agreement: true, + captcha_solution: "xx", + captcha_token: "xx", + captcha_answer_data: "xx" + } + + for field <- captcha_fields do + expected = %{ + "error" => "{\"captcha\":[\"Invalid CAPTCHA (Missing parameter: #{field})\"]}" + } + + assert expected == + conn + |> post("/api/v1/accounts", Map.delete(valid_params, field)) + |> json_response_and_validate_schema(:bad_request) + end + end + + test "returns an error if captcha is invalid", %{conn: conn} do + params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + agreement: true, + captcha_solution: "cofe", + captcha_token: "cofe", + captcha_answer_data: "cofe" + } + + assert %{"error" => "{\"captcha\":[\"Invalid answer data\"]}"} == + conn + |> post("/api/v1/accounts", params) + |> json_response_and_validate_schema(:bad_request) + end + end + + describe "GET /api/v1/accounts/:id/lists - account_lists" do + test "returns lists to which the account belongs" do + %{user: user, conn: conn} = oauth_access(["read:lists"]) + other_user = insert(:user) + assert {:ok, %Pleroma.List{id: list_id} = list} = Pleroma.List.create("Test List", user) + {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user) + + assert [%{"id" => list_id, "title" => "Test List"}] = + conn + |> get("/api/v1/accounts/#{other_user.id}/lists") + |> json_response_and_validate_schema(200) + end + end + + describe "verify_credentials" do + test "verify_credentials" do + %{user: user, conn: conn} = oauth_access(["read:accounts"]) + + [notification | _] = + insert_list(7, :notification, user: user, activity: insert(:note_activity)) + + Pleroma.Notification.set_read_up_to(user, notification.id) + conn = get(conn, "/api/v1/accounts/verify_credentials") + + response = json_response_and_validate_schema(conn, 200) + + assert %{"id" => id, "source" => %{"privacy" => "public"}} = response + assert response["pleroma"]["chat_token"] + assert response["pleroma"]["unread_notifications_count"] == 6 + assert id == to_string(user.id) + end + + test "verify_credentials default scope unlisted" do + user = insert(:user, default_scope: "unlisted") + %{conn: conn} = oauth_access(["read:accounts"], user: user) + + conn = get(conn, "/api/v1/accounts/verify_credentials") + + assert %{"id" => id, "source" => %{"privacy" => "unlisted"}} = + json_response_and_validate_schema(conn, 200) + + assert id == to_string(user.id) + end + + test "locked accounts" do + user = insert(:user, default_scope: "private") + %{conn: conn} = oauth_access(["read:accounts"], user: user) + + conn = get(conn, "/api/v1/accounts/verify_credentials") + + assert %{"id" => id, "source" => %{"privacy" => "private"}} = + json_response_and_validate_schema(conn, 200) + + assert id == to_string(user.id) + end + end + + describe "user relationships" do + setup do: oauth_access(["read:follows"]) + + test "returns the relationships for the current user", %{user: user, conn: conn} do + %{id: other_user_id} = other_user = insert(:user) + {:ok, _user} = User.follow(user, other_user) + + assert [%{"id" => ^other_user_id}] = + conn + |> get("/api/v1/accounts/relationships?id=#{other_user.id}") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^other_user_id}] = + conn + |> get("/api/v1/accounts/relationships?id[]=#{other_user.id}") + |> json_response_and_validate_schema(200) + end + + test "returns an empty list on a bad request", %{conn: conn} do + conn = get(conn, "/api/v1/accounts/relationships", %{}) + + assert [] = json_response_and_validate_schema(conn, 200) + end + end + + test "getting a list of mutes" do + %{user: user, conn: conn} = oauth_access(["read:mutes"]) + other_user = insert(:user) + + {:ok, _user_relationships} = User.mute(user, other_user) + + conn = get(conn, "/api/v1/mutes") + + other_user_id = to_string(other_user.id) + assert [%{"id" => ^other_user_id}] = json_response_and_validate_schema(conn, 200) + end + + test "getting a list of blocks" do + %{user: user, conn: conn} = oauth_access(["read:blocks"]) + other_user = insert(:user) + + {:ok, _user_relationship} = User.block(user, other_user) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/blocks") + + other_user_id = to_string(other_user.id) + assert [%{"id" => ^other_user_id}] = json_response_and_validate_schema(conn, 200) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/app_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/app_controller_test.exs @@ -0,0 +1,60 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.AppControllerTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Repo + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.Push + + import Pleroma.Factory + + test "apps/verify_credentials", %{conn: conn} do + token = insert(:oauth_token) + + conn = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/v1/apps/verify_credentials") + + app = Repo.preload(token, :app).app + + expected = %{ + "name" => app.client_name, + "website" => app.website, + "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key) + } + + assert expected == json_response_and_validate_schema(conn, 200) + end + + test "creates an oauth app", %{conn: conn} do + user = insert(:user) + app_attrs = build(:oauth_app) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> assign(:user, user) + |> post("/api/v1/apps", %{ + client_name: app_attrs.client_name, + redirect_uris: app_attrs.redirect_uris + }) + + [app] = Repo.all(App) + + expected = %{ + "name" => app.client_name, + "website" => app.website, + "client_id" => app.client_id, + "client_secret" => app.client_secret, + "id" => app.id |> to_string(), + "redirect_uri" => app.redirect_uris, + "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key) + } + + assert expected == json_response_and_validate_schema(conn, 200) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/auth_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/auth_controller_test.exs @@ -0,0 +1,159 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.AuthControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Config + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + + import Pleroma.Factory + import Swoosh.TestAssertions + + describe "GET /web/login" do + setup %{conn: conn} do + session_opts = [ + store: :cookie, + key: "_test", + signing_salt: "cooldude" + ] + + conn = + conn + |> Plug.Session.call(Plug.Session.init(session_opts)) + |> fetch_session() + + test_path = "/web/statuses/test" + %{conn: conn, path: test_path} + end + + test "redirects to the saved path after log in", %{conn: conn, path: path} do + app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".") + auth = insert(:oauth_authorization, app: app) + + conn = + conn + |> put_session(:return_to, path) + |> get("/web/login", %{code: auth.token}) + + assert conn.status == 302 + assert redirected_to(conn) == path + end + + test "redirects to the getting-started page when referer is not present", %{conn: conn} do + app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".") + auth = insert(:oauth_authorization, app: app) + + conn = get(conn, "/web/login", %{code: auth.token}) + + assert conn.status == 302 + assert redirected_to(conn) == "/web/getting-started" + end + end + + describe "POST /auth/password, with valid parameters" do + setup %{conn: conn} do + user = insert(:user) + conn = post(conn, "/auth/password?email=#{user.email}") + %{conn: conn, user: user} + end + + test "it returns 204", %{conn: conn} do + assert empty_json_response(conn) + end + + test "it creates a PasswordResetToken record for user", %{user: user} do + token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id) + assert token_record + end + + test "it sends an email to user", %{user: user} do + ObanHelpers.perform_all() + token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id) + + email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token) + notify_email = Config.get([:instance, :notify_email]) + instance_name = Config.get([:instance, :name]) + + assert_email_sent( + from: {instance_name, notify_email}, + to: {user.name, user.email}, + html_body: email.html_body + ) + end + end + + describe "POST /auth/password, with nickname" do + test "it returns 204", %{conn: conn} do + user = insert(:user) + + assert conn + |> post("/auth/password?nickname=#{user.nickname}") + |> empty_json_response() + + ObanHelpers.perform_all() + token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id) + + email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token) + notify_email = Config.get([:instance, :notify_email]) + instance_name = Config.get([:instance, :name]) + + assert_email_sent( + from: {instance_name, notify_email}, + to: {user.name, user.email}, + html_body: email.html_body + ) + end + + test "it doesn't fail when a user has no email", %{conn: conn} do + user = insert(:user, %{email: nil}) + + assert conn + |> post("/auth/password?nickname=#{user.nickname}") + |> empty_json_response() + end + end + + describe "POST /auth/password, with invalid parameters" do + setup do + user = insert(:user) + {:ok, user: user} + end + + test "it returns 204 when user is not found", %{conn: conn, user: user} do + conn = post(conn, "/auth/password?email=nonexisting_#{user.email}") + + assert empty_json_response(conn) + end + + test "it returns 204 when user is not local", %{conn: conn, user: user} do + {:ok, user} = Repo.update(Ecto.Changeset.change(user, local: false)) + conn = post(conn, "/auth/password?email=#{user.email}") + + assert empty_json_response(conn) + end + + test "it returns 204 when user is deactivated", %{conn: conn, user: user} do + {:ok, user} = Repo.update(Ecto.Changeset.change(user, deactivated: true, local: true)) + conn = post(conn, "/auth/password?email=#{user.email}") + + assert empty_json_response(conn) + end + end + + describe "DELETE /auth/sign_out" do + test "redirect to root page", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> delete("/auth/sign_out") + + assert conn.status == 302 + assert redirected_to(conn) == "/" + end + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/conversation_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/conversation_controller_test.exs @@ -0,0 +1,209 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + setup do: oauth_access(["read:statuses"]) + + describe "returns a list of conversations" do + setup(%{user: user_one, conn: conn}) do + user_two = insert(:user) + user_three = insert(:user) + + {:ok, user_two} = User.follow(user_two, user_one) + + {:ok, %{user: user_one, user_two: user_two, user_three: user_three, conn: conn}} + end + + test "returns correct conversations", %{ + user: user_one, + user_two: user_two, + user_three: user_three, + conn: conn + } do + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 + {:ok, direct} = create_direct_message(user_one, [user_two, user_three]) + + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1 + + {:ok, _follower_only} = + CommonAPI.post(user_one, %{ + status: "Hi @#{user_two.nickname}!", + visibility: "private" + }) + + res_conn = get(conn, "/api/v1/conversations") + + assert response = json_response_and_validate_schema(res_conn, 200) + + assert [ + %{ + "id" => res_id, + "accounts" => res_accounts, + "last_status" => res_last_status, + "unread" => unread + } + ] = response + + account_ids = Enum.map(res_accounts, & &1["id"]) + assert length(res_accounts) == 2 + assert user_two.id in account_ids + assert user_three.id in account_ids + assert is_binary(res_id) + assert unread == false + assert res_last_status["id"] == direct.id + assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 + end + + test "observes limit params", %{ + user: user_one, + user_two: user_two, + user_three: user_three, + conn: conn + } do + {:ok, _} = create_direct_message(user_one, [user_two, user_three]) + {:ok, _} = create_direct_message(user_two, [user_one, user_three]) + {:ok, _} = create_direct_message(user_three, [user_two, user_one]) + + res_conn = get(conn, "/api/v1/conversations?limit=1") + + assert response = json_response_and_validate_schema(res_conn, 200) + + assert Enum.count(response) == 1 + + res_conn = get(conn, "/api/v1/conversations?limit=2") + + assert response = json_response_and_validate_schema(res_conn, 200) + + assert Enum.count(response) == 2 + end + end + + test "filters conversations by recipients", %{user: user_one, conn: conn} do + user_two = insert(:user) + user_three = insert(:user) + {:ok, direct1} = create_direct_message(user_one, [user_two]) + {:ok, _direct2} = create_direct_message(user_one, [user_three]) + {:ok, direct3} = create_direct_message(user_one, [user_two, user_three]) + {:ok, _direct4} = create_direct_message(user_two, [user_three]) + {:ok, direct5} = create_direct_message(user_two, [user_one]) + + assert [conversation1, conversation2] = + conn + |> get("/api/v1/conversations?recipients[]=#{user_two.id}") + |> json_response_and_validate_schema(200) + + assert conversation1["last_status"]["id"] == direct5.id + assert conversation2["last_status"]["id"] == direct1.id + + [conversation1] = + conn + |> get("/api/v1/conversations?recipients[]=#{user_two.id}&recipients[]=#{user_three.id}") + |> json_response_and_validate_schema(200) + + assert conversation1["last_status"]["id"] == direct3.id + end + + test "updates the last_status on reply", %{user: user_one, conn: conn} do + user_two = insert(:user) + {:ok, direct} = create_direct_message(user_one, [user_two]) + + {:ok, direct_reply} = + CommonAPI.post(user_two, %{ + status: "reply", + visibility: "direct", + in_reply_to_status_id: direct.id + }) + + [%{"last_status" => res_last_status}] = + conn + |> get("/api/v1/conversations") + |> json_response_and_validate_schema(200) + + assert res_last_status["id"] == direct_reply.id + end + + test "the user marks a conversation as read", %{user: user_one, conn: conn} do + user_two = insert(:user) + {:ok, direct} = create_direct_message(user_one, [user_two]) + + assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1 + + user_two_conn = + build_conn() + |> assign(:user, user_two) + |> assign( + :token, + insert(:oauth_token, user: user_two, scopes: ["read:statuses", "write:conversations"]) + ) + + [%{"id" => direct_conversation_id, "unread" => true}] = + user_two_conn + |> get("/api/v1/conversations") + |> json_response_and_validate_schema(200) + + %{"unread" => false} = + user_two_conn + |> post("/api/v1/conversations/#{direct_conversation_id}/read") + |> json_response_and_validate_schema(200) + + assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 + + # The conversation is marked as unread on reply + {:ok, _} = + CommonAPI.post(user_two, %{ + status: "reply", + visibility: "direct", + in_reply_to_status_id: direct.id + }) + + [%{"unread" => true}] = + conn + |> get("/api/v1/conversations") + |> json_response_and_validate_schema(200) + + assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1 + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 + + # A reply doesn't increment the user's unread_conversation_count if the conversation is unread + {:ok, _} = + CommonAPI.post(user_two, %{ + status: "reply", + visibility: "direct", + in_reply_to_status_id: direct.id + }) + + assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1 + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 + end + + test "(vanilla) Mastodon frontend behaviour", %{user: user_one, conn: conn} do + user_two = insert(:user) + {:ok, direct} = create_direct_message(user_one, [user_two]) + + res_conn = get(conn, "/api/v1/statuses/#{direct.id}/context") + + assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200) + end + + defp create_direct_message(sender, recips) do + hellos = + recips + |> Enum.map(fn s -> "@#{s.nickname}" end) + |> Enum.join(", ") + + CommonAPI.post(sender, %{ + status: "Hi #{hellos}!", + visibility: "direct" + }) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/custom_emoji_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/custom_emoji_controller_test.exs @@ -0,0 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.CustomEmojiControllerTest do + use Pleroma.Web.ConnCase, async: true + + test "with tags", %{conn: conn} do + assert resp = + conn + |> get("/api/v1/custom_emojis") + |> json_response_and_validate_schema(200) + + assert [emoji | _body] = resp + assert Map.has_key?(emoji, "shortcode") + assert Map.has_key?(emoji, "static_url") + assert Map.has_key?(emoji, "tags") + assert is_list(emoji["tags"]) + assert Map.has_key?(emoji, "category") + assert Map.has_key?(emoji, "url") + assert Map.has_key?(emoji, "visible_in_picker") + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/domain_block_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/domain_block_controller_test.exs @@ -0,0 +1,79 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.User + + import Pleroma.Factory + + test "blocking / unblocking a domain" do + %{user: user, conn: conn} = oauth_access(["write:blocks"]) + other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"}) + + ret_conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"}) + + assert %{} == json_response_and_validate_schema(ret_conn, 200) + user = User.get_cached_by_ap_id(user.ap_id) + assert User.blocks?(user, other_user) + + ret_conn = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"}) + + assert %{} == json_response_and_validate_schema(ret_conn, 200) + user = User.get_cached_by_ap_id(user.ap_id) + refute User.blocks?(user, other_user) + end + + test "blocking a domain via query params" do + %{user: user, conn: conn} = oauth_access(["write:blocks"]) + other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"}) + + ret_conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/domain_blocks?domain=dogwhistle.zone") + + assert %{} == json_response_and_validate_schema(ret_conn, 200) + user = User.get_cached_by_ap_id(user.ap_id) + assert User.blocks?(user, other_user) + end + + test "unblocking a domain via query params" do + %{user: user, conn: conn} = oauth_access(["write:blocks"]) + other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"}) + + User.block_domain(user, "dogwhistle.zone") + user = refresh_record(user) + assert User.blocks?(user, other_user) + + ret_conn = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/domain_blocks?domain=dogwhistle.zone") + + assert %{} == json_response_and_validate_schema(ret_conn, 200) + user = User.get_cached_by_ap_id(user.ap_id) + refute User.blocks?(user, other_user) + end + + test "getting a list of domain blocks" do + %{user: user, conn: conn} = oauth_access(["read:blocks"]) + + {:ok, user} = User.block_domain(user, "bad.site") + {:ok, user} = User.block_domain(user, "even.worse.site") + + assert ["even.worse.site", "bad.site"] == + conn + |> assign(:user, user) + |> get("/api/v1/domain_blocks") + |> json_response_and_validate_schema(200) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/filter_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/filter_controller_test.exs @@ -0,0 +1,152 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.MastodonAPI.FilterView + + test "creating a filter" do + %{conn: conn} = oauth_access(["write:filters"]) + + filter = %Pleroma.Filter{ + phrase: "knights", + context: ["home"] + } + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context}) + + assert response = json_response_and_validate_schema(conn, 200) + assert response["phrase"] == filter.phrase + assert response["context"] == filter.context + assert response["irreversible"] == false + assert response["id"] != nil + assert response["id"] != "" + end + + test "fetching a list of filters" do + %{user: user, conn: conn} = oauth_access(["read:filters"]) + + query_one = %Pleroma.Filter{ + user_id: user.id, + filter_id: 1, + phrase: "knights", + context: ["home"] + } + + query_two = %Pleroma.Filter{ + user_id: user.id, + filter_id: 2, + phrase: "who", + context: ["home"] + } + + {:ok, filter_one} = Pleroma.Filter.create(query_one) + {:ok, filter_two} = Pleroma.Filter.create(query_two) + + response = + conn + |> get("/api/v1/filters") + |> json_response_and_validate_schema(200) + + assert response == + render_json( + FilterView, + "index.json", + filters: [filter_two, filter_one] + ) + end + + test "get a filter" do + %{user: user, conn: conn} = oauth_access(["read:filters"]) + + # check whole_word false + query = %Pleroma.Filter{ + user_id: user.id, + filter_id: 2, + phrase: "knight", + context: ["home"], + whole_word: false + } + + {:ok, filter} = Pleroma.Filter.create(query) + + conn = get(conn, "/api/v1/filters/#{filter.filter_id}") + + assert response = json_response_and_validate_schema(conn, 200) + assert response["whole_word"] == false + + # check whole_word true + %{user: user, conn: conn} = oauth_access(["read:filters"]) + + query = %Pleroma.Filter{ + user_id: user.id, + filter_id: 3, + phrase: "knight", + context: ["home"], + whole_word: true + } + + {:ok, filter} = Pleroma.Filter.create(query) + + conn = get(conn, "/api/v1/filters/#{filter.filter_id}") + + assert response = json_response_and_validate_schema(conn, 200) + assert response["whole_word"] == true + end + + test "update a filter" do + %{user: user, conn: conn} = oauth_access(["write:filters"]) + + query = %Pleroma.Filter{ + user_id: user.id, + filter_id: 2, + phrase: "knight", + context: ["home"], + hide: true, + whole_word: true + } + + {:ok, _filter} = Pleroma.Filter.create(query) + + new = %Pleroma.Filter{ + phrase: "nii", + context: ["home"] + } + + conn = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/filters/#{query.filter_id}", %{ + phrase: new.phrase, + context: new.context + }) + + assert response = json_response_and_validate_schema(conn, 200) + assert response["phrase"] == new.phrase + assert response["context"] == new.context + assert response["irreversible"] == true + assert response["whole_word"] == true + end + + test "delete a filter" do + %{user: user, conn: conn} = oauth_access(["write:filters"]) + + query = %Pleroma.Filter{ + user_id: user.id, + filter_id: 2, + phrase: "knight", + context: ["home"] + } + + {:ok, filter} = Pleroma.Filter.create(query) + + conn = delete(conn, "/api/v1/filters/#{filter.filter_id}") + + assert json_response_and_validate_schema(conn, 200) == %{} + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/follow_request_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/follow_request_controller_test.exs @@ -0,0 +1,74 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "locked accounts" do + setup do + user = insert(:user, locked: true) + %{conn: conn} = oauth_access(["follow"], user: user) + %{user: user, conn: conn} + end + + test "/api/v1/follow_requests works", %{user: user, conn: conn} do + other_user = insert(:user) + + {:ok, _, _, _activity} = CommonAPI.follow(other_user, user) + {:ok, other_user} = User.follow(other_user, user, :follow_pending) + + assert User.following?(other_user, user) == false + + conn = get(conn, "/api/v1/follow_requests") + + assert [relationship] = json_response_and_validate_schema(conn, 200) + assert to_string(other_user.id) == relationship["id"] + end + + test "/api/v1/follow_requests/:id/authorize works", %{user: user, conn: conn} do + other_user = insert(:user) + + {:ok, _, _, _activity} = CommonAPI.follow(other_user, user) + {:ok, other_user} = User.follow(other_user, user, :follow_pending) + + user = User.get_cached_by_id(user.id) + other_user = User.get_cached_by_id(other_user.id) + + assert User.following?(other_user, user) == false + + conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/authorize") + + assert relationship = json_response_and_validate_schema(conn, 200) + assert to_string(other_user.id) == relationship["id"] + + user = User.get_cached_by_id(user.id) + other_user = User.get_cached_by_id(other_user.id) + + assert User.following?(other_user, user) == true + end + + test "/api/v1/follow_requests/:id/reject works", %{user: user, conn: conn} do + other_user = insert(:user) + + {:ok, _, _, _activity} = CommonAPI.follow(other_user, user) + + user = User.get_cached_by_id(user.id) + + conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/reject") + + assert relationship = json_response_and_validate_schema(conn, 200) + assert to_string(other_user.id) == relationship["id"] + + user = User.get_cached_by_id(user.id) + other_user = User.get_cached_by_id(other_user.id) + + assert User.following?(other_user, user) == false + end + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs @@ -0,0 +1,87 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.User + import Pleroma.Factory + + test "get instance information", %{conn: conn} do + conn = get(conn, "/api/v1/instance") + assert result = json_response_and_validate_schema(conn, 200) + + email = Pleroma.Config.get([:instance, :email]) + # Note: not checking for "max_toot_chars" since it's optional + assert %{ + "uri" => _, + "title" => _, + "description" => _, + "version" => _, + "email" => from_config_email, + "urls" => %{ + "streaming_api" => _ + }, + "stats" => _, + "thumbnail" => _, + "languages" => _, + "registrations" => _, + "approval_required" => _, + "poll_limits" => _, + "upload_limit" => _, + "avatar_upload_limit" => _, + "background_upload_limit" => _, + "banner_upload_limit" => _, + "background_image" => _, + "chat_limit" => _, + "description_limit" => _ + } = result + + assert result["pleroma"]["metadata"]["account_activation_required"] != nil + assert result["pleroma"]["metadata"]["features"] + assert result["pleroma"]["metadata"]["federation"] + assert result["pleroma"]["metadata"]["fields_limits"] + assert result["pleroma"]["vapid_public_key"] + + assert email == from_config_email + end + + test "get instance stats", %{conn: conn} do + user = insert(:user, %{local: true}) + + user2 = insert(:user, %{local: true}) + {:ok, _user2} = User.deactivate(user2, !user2.deactivated) + + insert(:user, %{local: false, nickname: "u@peer1.com"}) + insert(:user, %{local: false, nickname: "u@peer2.com"}) + + {:ok, _} = Pleroma.Web.CommonAPI.post(user, %{status: "cofe"}) + + Pleroma.Stats.force_update() + + conn = get(conn, "/api/v1/instance") + + assert result = json_response_and_validate_schema(conn, 200) + + stats = result["stats"] + + assert stats + assert stats["user_count"] == 1 + assert stats["status_count"] == 1 + assert stats["domain_count"] == 2 + end + + test "get peers", %{conn: conn} do + insert(:user, %{local: false, nickname: "u@peer1.com"}) + insert(:user, %{local: false, nickname: "u@peer2.com"}) + + Pleroma.Stats.force_update() + + conn = get(conn, "/api/v1/instance/peers") + + assert result = json_response_and_validate_schema(conn, 200) + + assert ["peer1.com", "peer2.com"] == Enum.sort(result) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs @@ -0,0 +1,176 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ListControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Repo + + import Pleroma.Factory + + test "creating a list" do + %{conn: conn} = oauth_access(["write:lists"]) + + assert %{"title" => "cuties"} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists", %{"title" => "cuties"}) + |> json_response_and_validate_schema(:ok) + end + + test "renders error for invalid params" do + %{conn: conn} = oauth_access(["write:lists"]) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists", %{"title" => nil}) + + assert %{"error" => "title - null value where string expected."} = + json_response_and_validate_schema(conn, 400) + end + + test "listing a user's lists" do + %{conn: conn} = oauth_access(["read:lists", "write:lists"]) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists", %{"title" => "cuties"}) + |> json_response_and_validate_schema(:ok) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists", %{"title" => "cofe"}) + |> json_response_and_validate_schema(:ok) + + conn = get(conn, "/api/v1/lists") + + assert [ + %{"id" => _, "title" => "cofe"}, + %{"id" => _, "title" => "cuties"} + ] = json_response_and_validate_schema(conn, :ok) + end + + test "adding users to a list" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) + other_user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + + assert %{} == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + |> json_response_and_validate_schema(:ok) + + %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) + assert following == [other_user.follower_address] + end + + test "removing users from a list, body params" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) + other_user = insert(:user) + third_user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + {:ok, list} = Pleroma.List.follow(list, third_user) + + assert %{} == + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + |> json_response_and_validate_schema(:ok) + + %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) + assert following == [third_user.follower_address] + end + + test "removing users from a list, query params" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) + other_user = insert(:user) + third_user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + {:ok, list} = Pleroma.List.follow(list, third_user) + + assert %{} == + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/lists/#{list.id}/accounts?account_ids[]=#{other_user.id}") + |> json_response_and_validate_schema(:ok) + + %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) + assert following == [third_user.follower_address] + end + + test "listing users in a list" do + %{user: user, conn: conn} = oauth_access(["read:lists"]) + other_user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + + assert [%{"id" => id}] = json_response_and_validate_schema(conn, 200) + assert id == to_string(other_user.id) + end + + test "retrieving a list" do + %{user: user, conn: conn} = oauth_access(["read:lists"]) + {:ok, list} = Pleroma.List.create("name", user) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/lists/#{list.id}") + + assert %{"id" => id} = json_response_and_validate_schema(conn, 200) + assert id == to_string(list.id) + end + + test "renders 404 if list is not found" do + %{conn: conn} = oauth_access(["read:lists"]) + + conn = get(conn, "/api/v1/lists/666") + + assert %{"error" => "List not found"} = json_response_and_validate_schema(conn, :not_found) + end + + test "renaming a list" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) + {:ok, list} = Pleroma.List.create("name", user) + + assert %{"title" => "newname"} = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/lists/#{list.id}", %{"title" => "newname"}) + |> json_response_and_validate_schema(:ok) + end + + test "validates title when renaming a list" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) + {:ok, list} = Pleroma.List.create("name", user) + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/json") + |> put("/api/v1/lists/#{list.id}", %{"title" => " "}) + + assert %{"error" => "can't be blank"} == + json_response_and_validate_schema(conn, :unprocessable_entity) + end + + test "deleting a list" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) + {:ok, list} = Pleroma.List.create("name", user) + + conn = delete(conn, "/api/v1/lists/#{list.id}") + + assert %{} = json_response_and_validate_schema(conn, 200) + assert is_nil(Repo.get(Pleroma.List, list.id)) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/marker_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/marker_controller_test.exs @@ -0,0 +1,131 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + describe "GET /api/v1/markers" do + test "gets markers with correct scopes", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: ["read:statuses"]) + insert_list(7, :notification, user: user, activity: insert(:note_activity)) + + {:ok, %{"notifications" => marker}} = + Pleroma.Marker.upsert( + user, + %{"notifications" => %{"last_read_id" => "69420"}} + ) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> get("/api/v1/markers?timeline[]=notifications") + |> json_response_and_validate_schema(200) + + assert response == %{ + "notifications" => %{ + "last_read_id" => "69420", + "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), + "version" => 0, + "pleroma" => %{"unread_count" => 7} + } + } + end + + test "gets markers with missed scopes", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: []) + + Pleroma.Marker.upsert(user, %{"notifications" => %{"last_read_id" => "69420"}}) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> get("/api/v1/markers", %{timeline: ["notifications"]}) + |> json_response_and_validate_schema(403) + + assert response == %{"error" => "Insufficient permissions: read:statuses."} + end + end + + describe "POST /api/v1/markers" do + test "creates a marker with correct scopes", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: ["write:statuses"]) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> put_req_header("content-type", "application/json") + |> post("/api/v1/markers", %{ + home: %{last_read_id: "777"}, + notifications: %{"last_read_id" => "69420"} + }) + |> json_response_and_validate_schema(200) + + assert %{ + "notifications" => %{ + "last_read_id" => "69420", + "updated_at" => _, + "version" => 0, + "pleroma" => %{"unread_count" => 0} + } + } = response + end + + test "updates exist marker", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: ["write:statuses"]) + + {:ok, %{"notifications" => marker}} = + Pleroma.Marker.upsert( + user, + %{"notifications" => %{"last_read_id" => "69477"}} + ) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> put_req_header("content-type", "application/json") + |> post("/api/v1/markers", %{ + home: %{last_read_id: "777"}, + notifications: %{"last_read_id" => "69888"} + }) + |> json_response_and_validate_schema(200) + + assert response == %{ + "notifications" => %{ + "last_read_id" => "69888", + "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), + "version" => 0, + "pleroma" => %{"unread_count" => 0} + } + } + end + + test "creates a marker with missed scopes", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: []) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> put_req_header("content-type", "application/json") + |> post("/api/v1/markers", %{ + home: %{last_read_id: "777"}, + notifications: %{"last_read_id" => "69420"} + }) + |> json_response_and_validate_schema(403) + + assert response == %{"error" => "Insufficient permissions: write:statuses."} + end + 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 @@ -0,0 +1,146 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + + describe "Upload media" do + setup do: oauth_access(["write:media"]) + + setup do + image = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + [image: image] + end + + setup do: clear_config([:media_proxy]) + setup do: clear_config([Pleroma.Upload]) + + test "/api/v1/media", %{conn: conn, image: image} do + 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) + + assert media["type"] == "image" + assert media["description"] == desc + assert media["id"] + + object = Object.get_by_id(media["id"]) + assert object.data["actor"] == User.ap_id(conn.assigns[:user]) + end + + test "/api/v2/media", %{conn: conn, user: user, image: image} do + desc = "Description of the image" + + response = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v2/media", %{"file" => image, "description" => desc}) + |> json_response_and_validate_schema(202) + + assert media_id = response["id"] + + %{conn: conn} = oauth_access(["read:media"], user: user) + + media = + conn + |> get("/api/v1/media/#{media_id}") + |> json_response_and_validate_schema(200) + + assert media["type"] == "image" + assert media["description"] == desc + assert media["id"] + + object = Object.get_by_id(media["id"]) + assert object.data["actor"] == user.ap_id + end + end + + describe "Update media description" do + setup do: oauth_access(["write:media"]) + + setup %{user: actor} do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, %Object{} = object} = + ActivityPub.upload( + file, + actor: User.ap_id(actor), + description: "test-m" + ) + + [object: object] + end + + test "/api/v1/media/:id good request", %{conn: conn, object: object} do + media = + conn + |> put_req_header("content-type", "multipart/form-data") + |> put("/api/v1/media/#{object.id}", %{"description" => "test-media"}) + |> json_response_and_validate_schema(:ok) + + assert media["description"] == "test-media" + assert refresh_record(object).data["name"] == "test-media" + end + end + + describe "Get media by id (/api/v1/media/:id)" do + setup do: oauth_access(["read:media"]) + + setup %{user: actor} do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, %Object{} = object} = + ActivityPub.upload( + file, + actor: User.ap_id(actor), + description: "test-media" + ) + + [object: object] + end + + test "it returns media object when requested by owner", %{conn: conn, object: object} do + media = + conn + |> get("/api/v1/media/#{object.id}") + |> json_response_and_validate_schema(:ok) + + assert media["description"] == "test-media" + assert media["type"] == "image" + assert media["id"] + end + + test "it returns 403 if media object requested by non-owner", %{object: object, user: user} do + %{conn: conn, user: other_user} = oauth_access(["read:media"]) + + assert object.data["actor"] == user.ap_id + refute user.id == other_user.id + + conn + |> get("/api/v1/media/#{object.id}") + |> json_response(403) + end + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs @@ -0,0 +1,626 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Notification + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "does NOT render account/pleroma/relationship by default" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + {:ok, [_notification]} = Notification.create_notifications(activity) + + response = + conn + |> assign(:user, user) + |> get("/api/v1/notifications") + |> json_response_and_validate_schema(200) + + assert Enum.all?(response, fn n -> + get_in(n, ["account", "pleroma", "relationship"]) == %{} + end) + end + + test "list of notifications" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + + {:ok, [_notification]} = Notification.create_notifications(activity) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/notifications") + + expected_response = + "hi <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{user.id}\" href=\"#{ + user.ap_id + }\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>" + + assert [%{"status" => %{"content" => response}} | _rest] = + json_response_and_validate_schema(conn, 200) + + assert response == expected_response + end + + test "by default, does not contain pleroma:chat_mention" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, _activity} = CommonAPI.post_chat_message(other_user, user, "hey") + + result = + conn + |> get("/api/v1/notifications") + |> json_response_and_validate_schema(200) + + assert [] == result + + result = + conn + |> get("/api/v1/notifications?include_types[]=pleroma:chat_mention") + |> json_response_and_validate_schema(200) + + assert [_] = result + end + + test "getting a single notification" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + + {:ok, [notification]} = Notification.create_notifications(activity) + + conn = get(conn, "/api/v1/notifications/#{notification.id}") + + expected_response = + "hi <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{user.id}\" href=\"#{ + user.ap_id + }\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>" + + assert %{"status" => %{"content" => response}} = json_response_and_validate_schema(conn, 200) + assert response == expected_response + end + + test "dismissing a single notification (deprecated endpoint)" do + %{user: user, conn: conn} = oauth_access(["write:notifications"]) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + + {:ok, [notification]} = Notification.create_notifications(activity) + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/json") + |> post("/api/v1/notifications/dismiss", %{"id" => to_string(notification.id)}) + + assert %{} = json_response_and_validate_schema(conn, 200) + end + + test "dismissing a single notification" do + %{user: user, conn: conn} = oauth_access(["write:notifications"]) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + + {:ok, [notification]} = Notification.create_notifications(activity) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/notifications/#{notification.id}/dismiss") + + assert %{} = json_response_and_validate_schema(conn, 200) + end + + test "clearing all notifications" do + %{user: user, conn: conn} = oauth_access(["write:notifications", "read:notifications"]) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + + {:ok, [_notification]} = Notification.create_notifications(activity) + + ret_conn = post(conn, "/api/v1/notifications/clear") + + assert %{} = json_response_and_validate_schema(ret_conn, 200) + + ret_conn = get(conn, "/api/v1/notifications") + + assert all = json_response_and_validate_schema(ret_conn, 200) + assert all == [] + end + + test "paginates notifications using min_id, since_id, max_id, and limit" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, activity1} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + {:ok, activity2} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + {:ok, activity3} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + {:ok, activity4} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + + notification1_id = get_notification_id_by_activity(activity1) + notification2_id = get_notification_id_by_activity(activity2) + notification3_id = get_notification_id_by_activity(activity3) + notification4_id = get_notification_id_by_activity(activity4) + + conn = assign(conn, :user, user) + + # min_id + result = + conn + |> get("/api/v1/notifications?limit=2&min_id=#{notification1_id}") + |> json_response_and_validate_schema(:ok) + + assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result + + # since_id + result = + conn + |> get("/api/v1/notifications?limit=2&since_id=#{notification1_id}") + |> json_response_and_validate_schema(:ok) + + assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result + + # max_id + result = + conn + |> get("/api/v1/notifications?limit=2&max_id=#{notification4_id}") + |> json_response_and_validate_schema(:ok) + + assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result + end + + describe "exclude_visibilities" do + test "filters notifications for mentions" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, public_activity} = + CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "public"}) + + {:ok, direct_activity} = + CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "direct"}) + + {:ok, unlisted_activity} = + CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "unlisted"}) + + {:ok, private_activity} = + CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "private"}) + + query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "private"]}) + conn_res = get(conn, "/api/v1/notifications?" <> query) + + assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200) + assert id == direct_activity.id + + query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "direct"]}) + conn_res = get(conn, "/api/v1/notifications?" <> query) + + assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200) + assert id == private_activity.id + + query = params_to_query(%{exclude_visibilities: ["public", "private", "direct"]}) + conn_res = get(conn, "/api/v1/notifications?" <> query) + + assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200) + assert id == unlisted_activity.id + + query = params_to_query(%{exclude_visibilities: ["unlisted", "private", "direct"]}) + conn_res = get(conn, "/api/v1/notifications?" <> query) + + assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200) + assert id == public_activity.id + end + + test "filters notifications for Like activities" do + user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["read:notifications"]) + + {:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"}) + + {:ok, direct_activity} = + CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "direct"}) + + {:ok, unlisted_activity} = + CommonAPI.post(other_user, %{status: ".", visibility: "unlisted"}) + + {:ok, private_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "private"}) + + {:ok, _} = CommonAPI.favorite(user, public_activity.id) + {:ok, _} = CommonAPI.favorite(user, direct_activity.id) + {:ok, _} = CommonAPI.favorite(user, unlisted_activity.id) + {:ok, _} = CommonAPI.favorite(user, private_activity.id) + + activity_ids = + conn + |> get("/api/v1/notifications?exclude_visibilities[]=direct") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["status"]["id"]) + + assert public_activity.id in activity_ids + assert unlisted_activity.id in activity_ids + assert private_activity.id in activity_ids + refute direct_activity.id in activity_ids + + activity_ids = + conn + |> get("/api/v1/notifications?exclude_visibilities[]=unlisted") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["status"]["id"]) + + assert public_activity.id in activity_ids + refute unlisted_activity.id in activity_ids + assert private_activity.id in activity_ids + assert direct_activity.id in activity_ids + + activity_ids = + conn + |> get("/api/v1/notifications?exclude_visibilities[]=private") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["status"]["id"]) + + assert public_activity.id in activity_ids + assert unlisted_activity.id in activity_ids + refute private_activity.id in activity_ids + assert direct_activity.id in activity_ids + + activity_ids = + conn + |> get("/api/v1/notifications?exclude_visibilities[]=public") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["status"]["id"]) + + refute public_activity.id in activity_ids + assert unlisted_activity.id in activity_ids + assert private_activity.id in activity_ids + assert direct_activity.id in activity_ids + end + + test "filters notifications for Announce activities" do + user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["read:notifications"]) + + {:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"}) + + {:ok, unlisted_activity} = + CommonAPI.post(other_user, %{status: ".", visibility: "unlisted"}) + + {:ok, _} = CommonAPI.repeat(public_activity.id, user) + {:ok, _} = CommonAPI.repeat(unlisted_activity.id, user) + + activity_ids = + conn + |> get("/api/v1/notifications?exclude_visibilities[]=unlisted") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["status"]["id"]) + + assert public_activity.id in activity_ids + refute unlisted_activity.id in activity_ids + end + + test "doesn't return less than the requested amount of records when the user's reply is liked" do + user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["read:notifications"]) + + {:ok, mention} = + CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "public"}) + + {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + + {:ok, reply} = + CommonAPI.post(other_user, %{ + status: ".", + visibility: "public", + in_reply_to_status_id: activity.id + }) + + {:ok, _favorite} = CommonAPI.favorite(user, reply.id) + + activity_ids = + conn + |> get("/api/v1/notifications?exclude_visibilities[]=direct&limit=2") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["status"]["id"]) + + assert [reply.id, mention.id] == activity_ids + end + end + + test "filters notifications using exclude_types" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, mention_activity} = CommonAPI.post(other_user, %{status: "hey @#{user.nickname}"}) + {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, favorite_activity} = CommonAPI.favorite(other_user, create_activity.id) + {:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, other_user) + {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user) + + mention_notification_id = get_notification_id_by_activity(mention_activity) + favorite_notification_id = get_notification_id_by_activity(favorite_activity) + reblog_notification_id = get_notification_id_by_activity(reblog_activity) + follow_notification_id = get_notification_id_by_activity(follow_activity) + + query = params_to_query(%{exclude_types: ["mention", "favourite", "reblog"]}) + conn_res = get(conn, "/api/v1/notifications?" <> query) + + assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200) + + query = params_to_query(%{exclude_types: ["favourite", "reblog", "follow"]}) + conn_res = get(conn, "/api/v1/notifications?" <> query) + + assert [%{"id" => ^mention_notification_id}] = + json_response_and_validate_schema(conn_res, 200) + + query = params_to_query(%{exclude_types: ["reblog", "follow", "mention"]}) + conn_res = get(conn, "/api/v1/notifications?" <> query) + + assert [%{"id" => ^favorite_notification_id}] = + json_response_and_validate_schema(conn_res, 200) + + query = params_to_query(%{exclude_types: ["follow", "mention", "favourite"]}) + conn_res = get(conn, "/api/v1/notifications?" <> query) + + assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200) + end + + test "filters notifications using include_types" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, mention_activity} = CommonAPI.post(other_user, %{status: "hey @#{user.nickname}"}) + {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, favorite_activity} = CommonAPI.favorite(other_user, create_activity.id) + {:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, other_user) + {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user) + + mention_notification_id = get_notification_id_by_activity(mention_activity) + favorite_notification_id = get_notification_id_by_activity(favorite_activity) + 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") + + assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200) + + conn_res = get(conn, "/api/v1/notifications?include_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") + + assert [%{"id" => ^favorite_notification_id}] = + json_response_and_validate_schema(conn_res, 200) + + conn_res = get(conn, "/api/v1/notifications?include_types[]=reblog") + + assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200) + + result = conn |> get("/api/v1/notifications") |> json_response_and_validate_schema(200) + + assert length(result) == 4 + + query = params_to_query(%{include_types: ["follow", "mention", "favourite", "reblog"]}) + + result = + conn + |> get("/api/v1/notifications?" <> query) + |> json_response_and_validate_schema(200) + + assert length(result) == 4 + end + + test "destroy multiple" do + %{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"]) + other_user = insert(:user) + + {:ok, activity1} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + {:ok, activity2} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + {:ok, activity3} = CommonAPI.post(user, %{status: "hi @#{other_user.nickname}"}) + {:ok, activity4} = CommonAPI.post(user, %{status: "hi @#{other_user.nickname}"}) + + notification1_id = get_notification_id_by_activity(activity1) + notification2_id = get_notification_id_by_activity(activity2) + notification3_id = get_notification_id_by_activity(activity3) + notification4_id = get_notification_id_by_activity(activity4) + + result = + conn + |> get("/api/v1/notifications") + |> json_response_and_validate_schema(:ok) + + assert [%{"id" => ^notification2_id}, %{"id" => ^notification1_id}] = result + + conn2 = + conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:notifications"])) + + result = + conn2 + |> get("/api/v1/notifications") + |> json_response_and_validate_schema(:ok) + + assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result + + query = params_to_query(%{ids: [notification1_id, notification2_id]}) + conn_destroy = delete(conn, "/api/v1/notifications/destroy_multiple?" <> query) + + assert json_response_and_validate_schema(conn_destroy, 200) == %{} + + result = + conn2 + |> get("/api/v1/notifications") + |> json_response_and_validate_schema(:ok) + + assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result + end + + test "doesn't see notifications after muting user with notifications" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + user2 = insert(:user) + + {:ok, _, _, _} = CommonAPI.follow(user, user2) + {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"}) + + ret_conn = get(conn, "/api/v1/notifications") + + assert length(json_response_and_validate_schema(ret_conn, 200)) == 1 + + {:ok, _user_relationships} = User.mute(user, user2) + + conn = get(conn, "/api/v1/notifications") + + assert json_response_and_validate_schema(conn, 200) == [] + end + + test "see notifications after muting user without notifications" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + user2 = insert(:user) + + {:ok, _, _, _} = CommonAPI.follow(user, user2) + {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"}) + + ret_conn = get(conn, "/api/v1/notifications") + + assert length(json_response_and_validate_schema(ret_conn, 200)) == 1 + + {:ok, _user_relationships} = User.mute(user, user2, false) + + conn = get(conn, "/api/v1/notifications") + + assert length(json_response_and_validate_schema(conn, 200)) == 1 + end + + test "see notifications after muting user with notifications and with_muted parameter" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + user2 = insert(:user) + + {:ok, _, _, _} = CommonAPI.follow(user, user2) + {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"}) + + ret_conn = get(conn, "/api/v1/notifications") + + assert length(json_response_and_validate_schema(ret_conn, 200)) == 1 + + {:ok, _user_relationships} = User.mute(user, user2) + + conn = get(conn, "/api/v1/notifications?with_muted=true") + + assert length(json_response_and_validate_schema(conn, 200)) == 1 + end + + @tag capture_log: true + test "see move notifications" do + old_user = insert(:user) + new_user = insert(:user, also_known_as: [old_user.ap_id]) + %{user: follower, conn: conn} = oauth_access(["read:notifications"]) + + old_user_url = old_user.ap_id + + body = + File.read!("test/fixtures/users_mock/localhost.json") + |> String.replace("{{nickname}}", old_user.nickname) + |> Jason.encode!() + + Tesla.Mock.mock(fn + %{method: :get, url: ^old_user_url} -> + %Tesla.Env{status: 200, body: body} + end) + + User.follow(follower, old_user) + Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) + Pleroma.Tests.ObanHelpers.perform_all() + + conn = get(conn, "/api/v1/notifications") + + assert length(json_response_and_validate_schema(conn, 200)) == 1 + end + + describe "link headers" do + test "preserves parameters in link headers" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, activity1} = + CommonAPI.post(other_user, %{ + status: "hi @#{user.nickname}", + visibility: "public" + }) + + {:ok, activity2} = + CommonAPI.post(other_user, %{ + status: "hi @#{user.nickname}", + visibility: "public" + }) + + notification1 = Repo.get_by(Notification, activity_id: activity1.id) + notification2 = Repo.get_by(Notification, activity_id: activity2.id) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/notifications?limit=5") + + assert [link_header] = get_resp_header(conn, "link") + assert link_header =~ ~r/limit=5/ + assert link_header =~ ~r/min_id=#{notification2.id}/ + assert link_header =~ ~r/max_id=#{notification1.id}/ + end + end + + describe "from specified user" do + test "account_id" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + + %{id: account_id} = other_user1 = insert(:user) + other_user2 = insert(:user) + + {:ok, _activity} = CommonAPI.post(other_user1, %{status: "hi @#{user.nickname}"}) + {:ok, _activity} = CommonAPI.post(other_user2, %{status: "bye @#{user.nickname}"}) + + assert [%{"account" => %{"id" => ^account_id}}] = + conn + |> assign(:user, user) + |> get("/api/v1/notifications?account_id=#{account_id}") + |> json_response_and_validate_schema(200) + + assert %{"error" => "Account is not found"} = + conn + |> assign(:user, user) + |> get("/api/v1/notifications?account_id=cofe") + |> json_response_and_validate_schema(404) + end + end + + defp get_notification_id_by_activity(%{id: id}) do + Notification + |> Repo.get_by(activity_id: id) + |> Map.get(:id) + |> to_string() + end + + defp params_to_query(%{} = params) do + Enum.map_join(params, "&", fn + {k, v} when is_list(v) -> Enum.map_join(v, "&", &"#{k}[]=#{&1}") + {k, v} -> k <> "=" <> v + end) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/poll_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/poll_controller_test.exs @@ -0,0 +1,171 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.PollControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Object + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "GET /api/v1/polls/:id" do + setup do: oauth_access(["read:statuses"]) + + test "returns poll entity for object id", %{user: user, conn: conn} do + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pleroma does", + poll: %{options: ["what Mastodon't", "n't what Mastodoes"], expires_in: 20} + }) + + object = Object.normalize(activity) + + conn = get(conn, "/api/v1/polls/#{object.id}") + + response = json_response_and_validate_schema(conn, 200) + id = to_string(object.id) + assert %{"id" => ^id, "expired" => false, "multiple" => false} = response + end + + test "does not expose polls for private statuses", %{conn: conn} do + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(other_user, %{ + status: "Pleroma does", + poll: %{options: ["what Mastodon't", "n't what Mastodoes"], expires_in: 20}, + visibility: "private" + }) + + object = Object.normalize(activity) + + conn = get(conn, "/api/v1/polls/#{object.id}") + + assert json_response_and_validate_schema(conn, 404) + end + end + + describe "POST /api/v1/polls/:id/votes" do + setup do: oauth_access(["write:statuses"]) + + test "votes are added to the poll", %{conn: conn} do + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(other_user, %{ + status: "A very delicious sandwich", + poll: %{ + options: ["Lettuce", "Grilled Bacon", "Tomato"], + expires_in: 20, + multiple: true + } + }) + + object = Object.normalize(activity) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]}) + + assert json_response_and_validate_schema(conn, 200) + object = Object.get_by_id(object.id) + + assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => total_items}} -> + total_items == 1 + end) + end + + test "author can't vote", %{user: user, conn: conn} do + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Am I cute?", + poll: %{options: ["Yes", "No"], expires_in: 20} + }) + + object = Object.normalize(activity) + + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]}) + |> json_response_and_validate_schema(422) == %{"error" => "Poll's author can't vote"} + + object = Object.get_by_id(object.id) + + refute Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 1 + end + + test "does not allow multiple choices on a single-choice question", %{conn: conn} do + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(other_user, %{ + status: "The glass is", + poll: %{options: ["half empty", "half full"], expires_in: 20} + }) + + object = Object.normalize(activity) + + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]}) + |> json_response_and_validate_schema(422) == %{"error" => "Too many choices"} + + object = Object.get_by_id(object.id) + + refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => total_items}} -> + total_items == 1 + end) + end + + test "does not allow choice index to be greater than options count", %{conn: conn} do + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(other_user, %{ + status: "Am I cute?", + poll: %{options: ["Yes", "No"], expires_in: 20} + }) + + object = Object.normalize(activity) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [2]}) + + assert json_response_and_validate_schema(conn, 422) == %{"error" => "Invalid indices"} + end + + test "returns 404 error when object is not exist", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/polls/1/votes", %{"choices" => [0]}) + + assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} + end + + test "returns 404 when poll is private and not available for user", %{conn: conn} do + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(other_user, %{ + status: "Am I cute?", + poll: %{options: ["Yes", "No"], expires_in: 20}, + visibility: "private" + }) + + object = Object.normalize(activity) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0]}) + + assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} + end + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/report_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/report_controller_test.exs @@ -0,0 +1,95 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + setup do: oauth_access(["write:reports"]) + + setup do + target_user = insert(:user) + + {:ok, activity} = CommonAPI.post(target_user, %{status: "foobar"}) + + [target_user: target_user, activity: activity] + end + + test "submit a basic report", %{conn: conn, target_user: target_user} do + assert %{"action_taken" => false, "id" => _} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/reports", %{"account_id" => target_user.id}) + |> json_response_and_validate_schema(200) + end + + test "submit a report with statuses and comment", %{ + conn: conn, + target_user: target_user, + activity: activity + } do + assert %{"action_taken" => false, "id" => _} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/reports", %{ + "account_id" => target_user.id, + "status_ids" => [activity.id], + "comment" => "bad status!", + "forward" => "false" + }) + |> json_response_and_validate_schema(200) + end + + test "account_id is required", %{ + conn: conn, + activity: activity + } do + assert %{"error" => "Missing field: account_id."} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/reports", %{"status_ids" => [activity.id]}) + |> json_response_and_validate_schema(400) + end + + test "comment must be up to the size specified in the config", %{ + conn: conn, + target_user: target_user + } do + max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000) + comment = String.pad_trailing("a", max_size + 1, "a") + + error = %{"error" => "Comment must be up to #{max_size} characters"} + + assert ^error = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment}) + |> json_response_and_validate_schema(400) + end + + test "returns error when account is not exist", %{ + conn: conn, + activity: activity + } do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/reports", %{"status_ids" => [activity.id], "account_id" => "foo"}) + + assert json_response_and_validate_schema(conn, 400) == %{"error" => "Account not found"} + end + + test "doesn't fail if an admin has no email", %{conn: conn, target_user: target_user} do + insert(:user, %{is_admin: true, email: nil}) + + assert %{"action_taken" => false, "id" => _} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/reports", %{"account_id" => target_user.id}) + |> json_response_and_validate_schema(200) + end +end 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 @@ -0,0 +1,139 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Repo + alias Pleroma.ScheduledActivity + + import Pleroma.Factory + import Ecto.Query + + setup do: clear_config([ScheduledActivity, :enabled]) + + test "shows scheduled activities" do + %{user: user, conn: conn} = oauth_access(["read:statuses"]) + + scheduled_activity_id1 = insert(:scheduled_activity, user: user).id |> to_string() + scheduled_activity_id2 = insert(:scheduled_activity, user: user).id |> to_string() + scheduled_activity_id3 = insert(:scheduled_activity, user: user).id |> to_string() + scheduled_activity_id4 = insert(:scheduled_activity, user: user).id |> to_string() + + # min_id + conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}") + + result = json_response_and_validate_schema(conn_res, 200) + assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result + + # since_id + conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}") + + result = json_response_and_validate_schema(conn_res, 200) + assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result + + # max_id + conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}") + + result = json_response_and_validate_schema(conn_res, 200) + assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result + end + + test "shows a scheduled activity" do + %{user: user, conn: conn} = oauth_access(["read:statuses"]) + scheduled_activity = insert(:scheduled_activity, user: user) + + res_conn = get(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}") + + assert %{"id" => scheduled_activity_id} = json_response_and_validate_schema(res_conn, 200) + assert scheduled_activity_id == scheduled_activity.id |> to_string() + + res_conn = get(conn, "/api/v1/scheduled_statuses/404") + + assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404) + end + + test "updates a scheduled activity" do + Pleroma.Config.put([ScheduledActivity, :enabled], true) + %{user: user, conn: conn} = oauth_access(["write:statuses"]) + + scheduled_at = Timex.shift(NaiveDateTime.utc_now(), minutes: 60) + + {:ok, scheduled_activity} = + ScheduledActivity.create( + user, + %{ + scheduled_at: scheduled_at, + params: build(:note).data + } + ) + + job = Repo.one(from(j in Oban.Job, where: j.queue == "scheduled_activities")) + + assert job.args == %{"activity_id" => scheduled_activity.id} + assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(scheduled_at) + + new_scheduled_at = + NaiveDateTime.utc_now() + |> Timex.shift(minutes: 120) + |> Timex.format!("%Y-%m-%dT%H:%M:%S.%fZ", :strftime) + + res_conn = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{ + scheduled_at: new_scheduled_at + }) + + assert %{"scheduled_at" => expected_scheduled_at} = + json_response_and_validate_schema(res_conn, 200) + + assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at) + job = refresh_record(job) + + assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(new_scheduled_at) + + res_conn = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at}) + + assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404) + end + + test "deletes a scheduled activity" do + Pleroma.Config.put([ScheduledActivity, :enabled], true) + %{user: user, conn: conn} = oauth_access(["write:statuses"]) + scheduled_at = Timex.shift(NaiveDateTime.utc_now(), minutes: 60) + + {:ok, scheduled_activity} = + ScheduledActivity.create( + user, + %{ + scheduled_at: scheduled_at, + params: build(:note).data + } + ) + + job = Repo.one(from(j in Oban.Job, where: j.queue == "scheduled_activities")) + + assert job.args == %{"activity_id" => scheduled_activity.id} + + res_conn = + conn + |> assign(:user, user) + |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}") + + assert %{} = json_response_and_validate_schema(res_conn, 200) + refute Repo.get(ScheduledActivity, scheduled_activity.id) + refute Repo.get(Oban.Job, job.id) + + res_conn = + conn + |> assign(:user, user) + |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}") + + assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs @@ -0,0 +1,413 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Object + alias Pleroma.Web + alias Pleroma.Web.CommonAPI + import Pleroma.Factory + import ExUnit.CaptureLog + import Tesla.Mock + import Mock + + setup_all do + mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe ".search2" do + test "it returns empty result if user or status search return undefined error", %{conn: conn} do + with_mocks [ + {Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]}, + {Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]} + ] do + capture_log(fn -> + results = + conn + |> get("/api/v2/search?q=2hu") + |> json_response_and_validate_schema(200) + + assert results["accounts"] == [] + assert results["statuses"] == [] + end) =~ + "[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}" + end + end + + test "search", %{conn: conn} do + user = insert(:user) + user_two = insert(:user, %{nickname: "shp@shitposter.club"}) + user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + {:ok, activity} = CommonAPI.post(user, %{status: "This is about 2hu private 天子"}) + + {:ok, _activity} = + CommonAPI.post(user, %{ + status: "This is about 2hu, but private", + visibility: "private" + }) + + {:ok, _} = CommonAPI.post(user_two, %{status: "This isn't"}) + + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu #private"})}") + |> json_response_and_validate_schema(200) + + [account | _] = results["accounts"] + assert account["id"] == to_string(user_three.id) + + assert results["hashtags"] == [ + %{"name" => "private", "url" => "#{Web.base_url()}/tag/private"} + ] + + [status] = results["statuses"] + assert status["id"] == to_string(activity.id) + + results = + get(conn, "/api/v2/search?q=天子") + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "天子", "url" => "#{Web.base_url()}/tag/天子"} + ] + + [status] = results["statuses"] + assert status["id"] == to_string(activity.id) + end + + @tag capture_log: true + test "constructs hashtags from search query", %{conn: conn} do + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "some text with #explicit #hashtags"})}") + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "explicit", "url" => "#{Web.base_url()}/tag/explicit"}, + %{"name" => "hashtags", "url" => "#{Web.base_url()}/tag/hashtags"} + ] + + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "john doe JOHN DOE"})}") + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "john", "url" => "#{Web.base_url()}/tag/john"}, + %{"name" => "doe", "url" => "#{Web.base_url()}/tag/doe"}, + %{"name" => "JohnDoe", "url" => "#{Web.base_url()}/tag/JohnDoe"} + ] + + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "accident-prone"})}") + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "accident", "url" => "#{Web.base_url()}/tag/accident"}, + %{"name" => "prone", "url" => "#{Web.base_url()}/tag/prone"}, + %{"name" => "AccidentProne", "url" => "#{Web.base_url()}/tag/AccidentProne"} + ] + + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "https://shpposter.club/users/shpuld"})}") + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "shpuld", "url" => "#{Web.base_url()}/tag/shpuld"} + ] + + results = + conn + |> get( + "/api/v2/search?#{ + URI.encode_query(%{ + q: + "https://www.washingtonpost.com/sports/2020/06/10/" <> + "nascar-ban-display-confederate-flag-all-events-properties/" + }) + }" + ) + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "nascar", "url" => "#{Web.base_url()}/tag/nascar"}, + %{"name" => "ban", "url" => "#{Web.base_url()}/tag/ban"}, + %{"name" => "display", "url" => "#{Web.base_url()}/tag/display"}, + %{"name" => "confederate", "url" => "#{Web.base_url()}/tag/confederate"}, + %{"name" => "flag", "url" => "#{Web.base_url()}/tag/flag"}, + %{"name" => "all", "url" => "#{Web.base_url()}/tag/all"}, + %{"name" => "events", "url" => "#{Web.base_url()}/tag/events"}, + %{"name" => "properties", "url" => "#{Web.base_url()}/tag/properties"}, + %{ + "name" => "NascarBanDisplayConfederateFlagAllEventsProperties", + "url" => + "#{Web.base_url()}/tag/NascarBanDisplayConfederateFlagAllEventsProperties" + } + ] + end + + test "supports pagination of hashtags search results", %{conn: conn} do + results = + conn + |> get( + "/api/v2/search?#{ + URI.encode_query(%{q: "#some #text #with #hashtags", limit: 2, offset: 1}) + }" + ) + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "text", "url" => "#{Web.base_url()}/tag/text"}, + %{"name" => "with", "url" => "#{Web.base_url()}/tag/with"} + ] + end + + test "excludes a blocked users from search results", %{conn: conn} do + user = insert(:user) + user_smith = insert(:user, %{nickname: "Agent", name: "I love 2hu"}) + user_neo = insert(:user, %{nickname: "Agent Neo", name: "Agent"}) + + {:ok, act1} = CommonAPI.post(user, %{status: "This is about 2hu private 天子"}) + {:ok, act2} = CommonAPI.post(user_smith, %{status: "Agent Smith"}) + {:ok, act3} = CommonAPI.post(user_neo, %{status: "Agent Smith"}) + Pleroma.User.block(user, user_smith) + + results = + conn + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) + |> get("/api/v2/search?q=Agent") + |> json_response_and_validate_schema(200) + + status_ids = Enum.map(results["statuses"], fn g -> g["id"] end) + + assert act3.id in status_ids + refute act2.id in status_ids + refute act1.id in status_ids + end + end + + describe ".account_search" do + test "account search", %{conn: conn} do + user_two = insert(:user, %{nickname: "shp@shitposter.club"}) + user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + results = + conn + |> get("/api/v1/accounts/search?q=shp") + |> json_response_and_validate_schema(200) + + result_ids = for result <- results, do: result["acct"] + + assert user_two.nickname in result_ids + assert user_three.nickname in result_ids + + results = + conn + |> get("/api/v1/accounts/search?q=2hu") + |> json_response_and_validate_schema(200) + + result_ids = for result <- results, do: result["acct"] + + assert user_three.nickname in result_ids + end + + test "returns account if query contains a space", %{conn: conn} do + insert(:user, %{nickname: "shp@shitposter.club"}) + + results = + conn + |> get("/api/v1/accounts/search?q=shp@shitposter.club xxx") + |> json_response_and_validate_schema(200) + + assert length(results) == 1 + end + end + + describe ".search" do + test "it returns empty result if user or status search return undefined error", %{conn: conn} do + with_mocks [ + {Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]}, + {Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]} + ] do + capture_log(fn -> + results = + conn + |> get("/api/v1/search?q=2hu") + |> json_response_and_validate_schema(200) + + assert results["accounts"] == [] + assert results["statuses"] == [] + end) =~ + "[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}" + end + end + + test "search", %{conn: conn} do + user = insert(:user) + user_two = insert(:user, %{nickname: "shp@shitposter.club"}) + user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + {:ok, activity} = CommonAPI.post(user, %{status: "This is about 2hu"}) + + {:ok, _activity} = + CommonAPI.post(user, %{ + status: "This is about 2hu, but private", + visibility: "private" + }) + + {:ok, _} = CommonAPI.post(user_two, %{status: "This isn't"}) + + results = + conn + |> get("/api/v1/search?q=2hu") + |> json_response_and_validate_schema(200) + + [account | _] = results["accounts"] + assert account["id"] == to_string(user_three.id) + + assert results["hashtags"] == ["2hu"] + + [status] = results["statuses"] + assert status["id"] == to_string(activity.id) + end + + test "search fetches remote statuses and prefers them over other results", %{conn: conn} do + capture_log(fn -> + {: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) + + assert [ + %{"url" => "http://mastodon.example.org/@admin/99541947525187367"}, + %{"id" => ^activity_id} + ] = results["statuses"] + end) + end + + test "search doesn't show statuses that it shouldn't", %{conn: conn} do + {:ok, activity} = + CommonAPI.post(insert(:user), %{ + status: "This is about 2hu, but private", + visibility: "private" + }) + + capture_log(fn -> + q = Object.normalize(activity).data["id"] + + results = + conn + |> get("/api/v1/search?q=#{q}") + |> json_response_and_validate_schema(200) + + [] = results["statuses"] + end) + end + + test "search fetches remote accounts", %{conn: conn} do + user = insert(:user) + + query = URI.encode_query(%{q: " mike@osada.macgirvin.com ", resolve: true}) + + results = + conn + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) + |> get("/api/v1/search?#{query}") + |> json_response_and_validate_schema(200) + + [account] = results["accounts"] + assert account["acct"] == "mike@osada.macgirvin.com" + end + + test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do + results = + conn + |> get("/api/v1/search?q=mike@osada.macgirvin.com&resolve=false") + |> json_response_and_validate_schema(200) + + assert [] == results["accounts"] + end + + test "search with limit and offset", %{conn: conn} do + user = insert(:user) + _user_two = insert(:user, %{nickname: "shp@shitposter.club"}) + _user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + {:ok, _activity1} = CommonAPI.post(user, %{status: "This is about 2hu"}) + {:ok, _activity2} = CommonAPI.post(user, %{status: "This is also about 2hu"}) + + result = + conn + |> get("/api/v1/search?q=2hu&limit=1") + + assert results = json_response_and_validate_schema(result, 200) + assert [%{"id" => activity_id1}] = results["statuses"] + assert [_] = results["accounts"] + + results = + conn + |> get("/api/v1/search?q=2hu&limit=1&offset=1") + |> json_response_and_validate_schema(200) + + assert [%{"id" => activity_id2}] = results["statuses"] + assert [] = results["accounts"] + + assert activity_id1 != activity_id2 + end + + test "search returns results only for the given type", %{conn: conn} do + user = insert(:user) + _user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + {:ok, _activity} = CommonAPI.post(user, %{status: "This is about 2hu"}) + + assert %{"statuses" => [_activity], "accounts" => [], "hashtags" => []} = + conn + |> get("/api/v1/search?q=2hu&type=statuses") + |> json_response_and_validate_schema(200) + + assert %{"statuses" => [], "accounts" => [_user_two], "hashtags" => []} = + conn + |> get("/api/v1/search?q=2hu&type=accounts") + |> json_response_and_validate_schema(200) + end + + test "search uses account_id to filter statuses by the author", %{conn: conn} do + user = insert(:user, %{nickname: "shp@shitposter.club"}) + user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + {:ok, activity1} = CommonAPI.post(user, %{status: "This is about 2hu"}) + {:ok, activity2} = CommonAPI.post(user_two, %{status: "This is also about 2hu"}) + + results = + conn + |> get("/api/v1/search?q=2hu&account_id=#{user.id}") + |> json_response_and_validate_schema(200) + + assert [%{"id" => activity_id1}] = results["statuses"] + assert activity_id1 == activity1.id + assert [_] = results["accounts"] + + results = + conn + |> get("/api/v1/search?q=2hu&account_id=#{user_two.id}") + |> json_response_and_validate_schema(200) + + assert [%{"id" => activity_id2}] = results["statuses"] + assert activity_id2 == activity2.id + end + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -0,0 +1,1743 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do + use Pleroma.Web.ConnCase + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.Conversation.Participation + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.ScheduledActivity + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + + 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]) + + describe "posting statuses" do + setup do: oauth_access(["write:statuses"]) + + test "posting a status does not increment reblog_count when relaying", %{conn: conn} do + Config.put([:instance, :federating], true) + Config.get([:instance, :allow_relay], true) + + response = + conn + |> put_req_header("content-type", "application/json") + |> post("api/v1/statuses", %{ + "content_type" => "text/plain", + "source" => "Pleroma FE", + "status" => "Hello world", + "visibility" => "public" + }) + |> json_response_and_validate_schema(200) + + assert response["reblogs_count"] == 0 + ObanHelpers.perform_all() + + response = + conn + |> get("api/v1/statuses/#{response["id"]}", %{}) + |> json_response_and_validate_schema(200) + + assert response["reblogs_count"] == 0 + end + + test "posting a status", %{conn: conn} do + idempotency_key = "Pikachu rocks!" + + conn_one = + conn + |> put_req_header("content-type", "application/json") + |> put_req_header("idempotency-key", idempotency_key) + |> post("/api/v1/statuses", %{ + "status" => "cofe", + "spoiler_text" => "2hu", + "sensitive" => "0" + }) + + {:ok, ttl} = Cachex.ttl(:idempotency_cache, idempotency_key) + # Six hours + assert ttl > :timer.seconds(6 * 60 * 60 - 1) + + assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} = + json_response_and_validate_schema(conn_one, 200) + + assert Activity.get_by_id(id) + + conn_two = + conn + |> put_req_header("content-type", "application/json") + |> put_req_header("idempotency-key", idempotency_key) + |> post("/api/v1/statuses", %{ + "status" => "cofe", + "spoiler_text" => "2hu", + "sensitive" => 0 + }) + + assert %{"id" => second_id} = json_response(conn_two, 200) + assert id == second_id + + conn_three = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "cofe", + "spoiler_text" => "2hu", + "sensitive" => "False" + }) + + assert %{"id" => third_id} = json_response_and_validate_schema(conn_three, 200) + refute id == third_id + + # An activity that will expire: + # 2 hours + expires_in = 2 * 60 * 60 + + expires_at = DateTime.add(DateTime.utc_now(), expires_in) + + conn_four = + conn + |> put_req_header("content-type", "application/json") + |> post("api/v1/statuses", %{ + "status" => "oolong", + "expires_in" => expires_in + }) + + assert %{"id" => fourth_id} = json_response_and_validate_schema(conn_four, 200) + + assert Activity.get_by_id(fourth_id) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: fourth_id}, + scheduled_at: expires_at + ) + end + + test "it fails to create a status if `expires_in` is less or equal than an hour", %{ + conn: conn + } do + # 1 minute + expires_in = 1 * 60 + + assert %{"error" => "Expiry date is too soon"} = + conn + |> put_req_header("content-type", "application/json") + |> post("api/v1/statuses", %{ + "status" => "oolong", + "expires_in" => expires_in + }) + |> json_response_and_validate_schema(422) + + # 5 minutes + expires_in = 5 * 60 + + assert %{"error" => "Expiry date is too soon"} = + conn + |> put_req_header("content-type", "application/json") + |> post("api/v1/statuses", %{ + "status" => "oolong", + "expires_in" => expires_in + }) + |> json_response_and_validate_schema(422) + end + + test "Get MRF reason when posting a status is rejected by one", %{conn: conn} do + Config.put([:mrf_keyword, :reject], ["GNO"]) + Config.put([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) + + assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} = + conn + |> put_req_header("content-type", "application/json") + |> post("api/v1/statuses", %{"status" => "GNO/Linux"}) + |> json_response_and_validate_schema(422) + end + + test "posting an undefined status with an attachment", %{user: user, conn: conn} do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "media_ids" => [to_string(upload.id)] + }) + + assert json_response_and_validate_schema(conn, 200) + end + + test "replying to a status", %{user: user, conn: conn} do + {:ok, replied_to} = CommonAPI.post(user, %{status: "cofe"}) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id}) + + assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200) + + activity = Activity.get_by_id(id) + + assert activity.data["context"] == replied_to.data["context"] + assert Activity.get_in_reply_to_activity(activity).id == replied_to.id + end + + test "replying to a direct message with visibility other than direct", %{ + user: user, + conn: conn + } do + {:ok, replied_to} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"}) + + Enum.each(["public", "private", "unlisted"], fn visibility -> + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "@#{user.nickname} hey", + "in_reply_to_id" => replied_to.id, + "visibility" => visibility + }) + + assert json_response_and_validate_schema(conn, 422) == %{ + "error" => "The message visibility must be direct" + } + end) + end + + test "posting a status with an invalid in_reply_to_id", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""}) + + assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200) + assert Activity.get_by_id(id) + end + + test "posting a sensitive status", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true}) + + assert %{"content" => "cofe", "id" => id, "sensitive" => true} = + json_response_and_validate_schema(conn, 200) + + assert Activity.get_by_id(id) + end + + test "posting a fake status", %{conn: conn} do + real_conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => + "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it" + }) + + real_status = json_response_and_validate_schema(real_conn, 200) + + assert real_status + assert Object.get_by_ap_id(real_status["uri"]) + + real_status = + real_status + |> Map.put("id", nil) + |> Map.put("url", nil) + |> Map.put("uri", nil) + |> Map.put("created_at", nil) + |> Kernel.put_in(["pleroma", "conversation_id"], nil) + + fake_conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => + "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it", + "preview" => true + }) + + fake_status = json_response_and_validate_schema(fake_conn, 200) + + assert fake_status + refute Object.get_by_ap_id(fake_status["uri"]) + + fake_status = + fake_status + |> Map.put("id", nil) + |> Map.put("url", nil) + |> Map.put("uri", nil) + |> Map.put("created_at", 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(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}" + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"}) + + assert %{"id" => id} = response = json_response_and_validate_schema(conn, 200) + assert response["visibility"] == "direct" + assert response["pleroma"]["direct_conversation_id"] + assert activity = Activity.get_by_id(id) + assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id] + assert activity.data["to"] == [user2.ap_id] + assert activity.data["cc"] == [] + end + end + + describe "posting scheduled statuses" do + setup do: oauth_access(["write:statuses"]) + + test "creates a scheduled activity", %{conn: conn} do + scheduled_at = + NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) + |> NaiveDateTime.to_iso8601() + |> Kernel.<>("Z") + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "scheduled", + "scheduled_at" => scheduled_at + }) + + assert %{"scheduled_at" => expected_scheduled_at} = + json_response_and_validate_schema(conn, 200) + + assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at) + assert [] == Repo.all(Activity) + end + + test "ignores nil values", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "not scheduled", + "scheduled_at" => nil + }) + + assert result = json_response_and_validate_schema(conn, 200) + assert Activity.get_by_id(result["id"]) + end + + test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do + scheduled_at = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(120), :millisecond) + |> NaiveDateTime.to_iso8601() + |> Kernel.<>("Z") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "media_ids" => [to_string(upload.id)], + "status" => "scheduled", + "scheduled_at" => scheduled_at + }) + + assert %{"media_attachments" => [media_attachment]} = + json_response_and_validate_schema(conn, 200) + + assert %{"type" => "image"} = media_attachment + end + + test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now", + %{conn: conn} do + scheduled_at = + NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond) + |> NaiveDateTime.to_iso8601() + |> Kernel.<>("Z") + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "not scheduled", + "scheduled_at" => scheduled_at + }) + + assert %{"content" => "not scheduled"} = json_response_and_validate_schema(conn, 200) + assert [] == Repo.all(ScheduledActivity) + end + + test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + # TODO + |> Kernel.<>("Z") + + attrs = %{params: %{}, scheduled_at: today} + {:ok, _} = ScheduledActivity.create(user, attrs) + {:ok, _} = ScheduledActivity.create(user, attrs) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today}) + + assert %{"error" => "daily limit exceeded"} == json_response_and_validate_schema(conn, 422) + end + + test "returns error when total user limit is exceeded", %{user: user, conn: conn} do + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + |> Kernel.<>("Z") + + tomorrow = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.hours(36), :millisecond) + |> NaiveDateTime.to_iso8601() + |> Kernel.<>("Z") + + attrs = %{params: %{}, scheduled_at: today} + {:ok, _} = ScheduledActivity.create(user, attrs) + {:ok, _} = ScheduledActivity.create(user, attrs) + {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow}) + + assert %{"error" => "total limit exceeded"} == json_response_and_validate_schema(conn, 422) + end + end + + describe "posting polls" do + setup do: oauth_access(["write:statuses"]) + + test "posting a poll", %{conn: conn} do + time = NaiveDateTime.utc_now() + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "Who is the #bestgrill?", + "poll" => %{ + "options" => ["Rei", "Asuka", "Misato"], + "expires_in" => 420 + } + }) + + response = json_response_and_validate_schema(conn, 200) + + assert Enum.all?(response["poll"]["options"], fn %{"title" => title} -> + title in ["Rei", "Asuka", "Misato"] + end) + + assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430 + refute response["poll"]["expred"] + + question = Object.get_by_id(response["poll"]["id"]) + + # closed contains utc timezone + assert question.data["closed"] =~ "Z" + end + + test "option limit is enforced", %{conn: conn} do + limit = Config.get([:instance, :poll_limits, :max_options]) + + conn = + conn + |> 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} + }) + + %{"error" => error} = json_response_and_validate_schema(conn, 422) + assert error == "Poll can't contain more than #{limit} options" + end + + test "option character limit is enforced", %{conn: conn} do + limit = Config.get([:instance, :poll_limits, :max_option_chars]) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "...", + "poll" => %{ + "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)], + "expires_in" => 1 + } + }) + + %{"error" => error} = json_response_and_validate_schema(conn, 422) + assert error == "Poll options cannot be longer than #{limit} characters each" + end + + test "minimal date limit is enforced", %{conn: conn} do + limit = Config.get([:instance, :poll_limits, :min_expiration]) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "imagine arbitrary limits", + "poll" => %{ + "options" => ["this post was made by pleroma gang"], + "expires_in" => limit - 1 + } + }) + + %{"error" => error} = json_response_and_validate_schema(conn, 422) + assert error == "Expiration date is too soon" + end + + test "maximum date limit is enforced", %{conn: conn} do + limit = Config.get([:instance, :poll_limits, :max_expiration]) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "imagine arbitrary limits", + "poll" => %{ + "options" => ["this post was made by pleroma gang"], + "expires_in" => limit + 1 + } + }) + + %{"error" => error} = json_response_and_validate_schema(conn, 422) + assert error == "Expiration date is too far in the future" + end + end + + test "get a status" do + %{conn: conn} = oauth_access(["read:statuses"]) + activity = insert(:note_activity) + + conn = get(conn, "/api/v1/statuses/#{activity.id}") + + assert %{"id" => id} = json_response_and_validate_schema(conn, 200) + assert id == to_string(activity.id) + end + + defp local_and_remote_activities do + local = insert(:note_activity) + remote = insert(:note_activity, local: false) + {:ok, local: local, remote: remote} + end + + describe "status with restrict unauthenticated activities for local and remote" do + setup do: local_and_remote_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, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + + assert json_response_and_validate_schema(res_conn, :not_found) == %{ + "error" => "Record not found" + } + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + + assert json_response_and_validate_schema(res_conn, :not_found) == %{ + "error" => "Record not found" + } + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + end + end + + describe "status with restrict unauthenticated activities for local" do + setup do: local_and_remote_activities() + + setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + + assert json_response_and_validate_schema(res_conn, :not_found) == %{ + "error" => "Record not found" + } + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + end + end + + describe "status with restrict unauthenticated activities for remote" do + setup do: local_and_remote_activities() + + setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + + assert json_response_and_validate_schema(res_conn, :not_found) == %{ + "error" => "Record not found" + } + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + end + end + + test "getting a status that doesn't exist returns 404" do + %{conn: conn} = oauth_access(["read:statuses"]) + activity = insert(:note_activity) + + conn = get(conn, "/api/v1/statuses/#{String.downcase(activity.id)}") + + assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} + end + + test "get a direct status" do + %{user: user, conn: conn} = oauth_access(["read:statuses"]) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "direct"}) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/statuses/#{activity.id}") + + [participation] = Participation.for_user(user) + + res = json_response_and_validate_schema(conn, 200) + assert res["pleroma"]["direct_conversation_id"] == participation.id + end + + test "get statuses by IDs" do + %{conn: conn} = oauth_access(["read:statuses"]) + %{id: id1} = insert(:note_activity) + %{id: id2} = insert(:note_activity) + + query_string = "ids[]=#{id1}&ids[]=#{id2}" + conn = get(conn, "/api/v1/statuses/?#{query_string}") + + assert [%{"id" => ^id1}, %{"id" => ^id2}] = + Enum.sort_by(json_response_and_validate_schema(conn, :ok), & &1["id"]) + end + + describe "getting statuses by ids with restricted unauthenticated for local and remote" do + setup do: local_and_remote_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, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") + + assert json_response_and_validate_schema(res_conn, 200) == [] + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") + + assert length(json_response_and_validate_schema(res_conn, 200)) == 2 + end + end + + describe "getting statuses by ids with restricted unauthenticated for local" do + setup do: local_and_remote_activities() + + setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") + + remote_id = remote.id + assert [%{"id" => ^remote_id}] = json_response_and_validate_schema(res_conn, 200) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") + + assert length(json_response_and_validate_schema(res_conn, 200)) == 2 + end + end + + describe "getting statuses by ids with restricted unauthenticated for remote" do + setup do: local_and_remote_activities() + + setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") + + local_id = local.id + assert [%{"id" => ^local_id}] = json_response_and_validate_schema(res_conn, 200) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") + + assert length(json_response_and_validate_schema(res_conn, 200)) == 2 + end + end + + describe "deleting a status" do + test "when you created it" do + %{user: author, conn: conn} = oauth_access(["write:statuses"]) + activity = insert(:note_activity, user: author) + object = Object.normalize(activity) + + content = object.data["content"] + source = object.data["source"] + + result = + conn + |> assign(:user, author) + |> delete("/api/v1/statuses/#{activity.id}") + |> json_response_and_validate_schema(200) + + assert match?(%{"content" => ^content, "text" => ^source}, result) + + refute Activity.get_by_id(activity.id) + end + + test "when it doesn't exist" do + %{user: author, conn: conn} = oauth_access(["write:statuses"]) + activity = insert(:note_activity, user: author) + + conn = + conn + |> assign(:user, author) + |> delete("/api/v1/statuses/#{String.downcase(activity.id)}") + + assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404) + end + + test "when you didn't create it" do + %{conn: conn} = oauth_access(["write:statuses"]) + activity = insert(:note_activity) + + conn = delete(conn, "/api/v1/statuses/#{activity.id}") + + assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404) + + 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) + + res_conn = + conn + |> assign(:user, admin) + |> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"])) + |> delete("/api/v1/statuses/#{activity1.id}") + + assert %{} = json_response_and_validate_schema(res_conn, 200) + + res_conn = + conn + |> assign(:user, moderator) + |> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"])) + |> delete("/api/v1/statuses/#{activity2.id}") + + assert %{} = json_response_and_validate_schema(res_conn, 200) + + refute Activity.get_by_id(activity1.id) + refute Activity.get_by_id(activity2.id) + end + end + + describe "reblogging" do + setup do: oauth_access(["write:statuses"]) + + test "reblogs and returns the reblogged status", %{conn: conn} do + activity = insert(:note_activity) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/reblog") + + assert %{ + "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}, + "reblogged" => true + } = json_response_and_validate_schema(conn, 200) + + assert to_string(activity.id) == id + end + + test "returns 404 if the reblogged status doesn't exist", %{conn: conn} do + activity = insert(:note_activity) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{String.downcase(activity.id)}/reblog") + + assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404) + end + + test "reblogs privately and returns the reblogged status", %{conn: conn} do + activity = insert(:note_activity) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post( + "/api/v1/statuses/#{activity.id}/reblog", + %{"visibility" => "private"} + ) + + assert %{ + "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}, + "reblogged" => true, + "visibility" => "private" + } = json_response_and_validate_schema(conn, 200) + + assert to_string(activity.id) == id + end + + test "reblogged status for another user" do + activity = insert(:note_activity) + user1 = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + {:ok, _} = CommonAPI.favorite(user2, activity.id) + {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id) + {:ok, reblog_activity1} = CommonAPI.repeat(activity.id, user1) + {:ok, _} = CommonAPI.repeat(activity.id, user2) + + conn_res = + build_conn() + |> assign(:user, user3) + |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"])) + |> get("/api/v1/statuses/#{reblog_activity1.id}") + + assert %{ + "reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2}, + "reblogged" => false, + "favourited" => false, + "bookmarked" => false + } = json_response_and_validate_schema(conn_res, 200) + + conn_res = + build_conn() + |> assign(:user, user2) + |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"])) + |> get("/api/v1/statuses/#{reblog_activity1.id}") + + assert %{ + "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2}, + "reblogged" => true, + "favourited" => true, + "bookmarked" => true + } = json_response_and_validate_schema(conn_res, 200) + + assert to_string(activity.id) == id + end + end + + describe "unreblogging" do + setup do: oauth_access(["write:statuses"]) + + test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do + activity = insert(:note_activity) + + {:ok, _} = CommonAPI.repeat(activity.id, user) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/unreblog") + + assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = + json_response_and_validate_schema(conn, 200) + + assert to_string(activity.id) == id + end + + test "returns 404 error when activity does not exist", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/foo/unreblog") + + assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} + end + end + + describe "favoriting" do + setup do: oauth_access(["write:favourites"]) + + test "favs a status and returns it", %{conn: conn} do + activity = insert(:note_activity) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/favourite") + + assert %{"id" => id, "favourites_count" => 1, "favourited" => true} = + json_response_and_validate_schema(conn, 200) + + assert to_string(activity.id) == id + end + + test "favoriting twice will just return 200", %{conn: conn} do + activity = insert(:note_activity) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/favourite") + + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/favourite") + |> json_response_and_validate_schema(200) + end + + test "returns 404 error for a wrong id", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/1/favourite") + + assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} + end + end + + describe "unfavoriting" do + setup do: oauth_access(["write:favourites"]) + + test "unfavorites a status and returns it", %{user: user, conn: conn} do + activity = insert(:note_activity) + + {:ok, _} = CommonAPI.favorite(user, activity.id) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/unfavourite") + + assert %{"id" => id, "favourites_count" => 0, "favourited" => false} = + json_response_and_validate_schema(conn, 200) + + assert to_string(activity.id) == id + end + + test "returns 404 error for a wrong id", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/1/unfavourite") + + assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} + end + end + + describe "pinned statuses" do + setup do: oauth_access(["write:accounts"]) + + setup %{user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"}) + + %{activity: activity} + end + + setup do: clear_config([:instance, :max_pinned_statuses], 1) + + test "pin status", %{conn: conn, user: user, activity: activity} do + id_str = to_string(activity.id) + + assert %{"id" => ^id_str, "pinned" => true} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/pin") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^id_str, "pinned" => true}] = + conn + |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") + |> json_response_and_validate_schema(200) + end + + test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do + {:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"}) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{dm.id}/pin") + + assert json_response_and_validate_schema(conn, 400) == %{"error" => "Could not pin"} + end + + test "unpin status", %{conn: conn, user: user, activity: activity} do + {:ok, _} = CommonAPI.pin(activity.id, user) + user = refresh_record(user) + + id_str = to_string(activity.id) + + assert %{"id" => ^id_str, "pinned" => false} = + conn + |> assign(:user, user) + |> post("/api/v1/statuses/#{activity.id}/unpin") + |> json_response_and_validate_schema(200) + + assert [] = + conn + |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") + |> json_response_and_validate_schema(200) + end + + test "/unpin: returns 400 error when activity is not exist", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/1/unpin") + + assert json_response_and_validate_schema(conn, 400) == %{"error" => "Could not unpin"} + end + + test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do + {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"}) + + id_str_one = to_string(activity_one.id) + + assert %{"id" => ^id_str_one, "pinned" => true} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{id_str_one}/pin") + |> json_response_and_validate_schema(200) + + user = refresh_record(user) + + assert %{"error" => "You have already pinned the maximum number of statuses"} = + conn + |> assign(:user, user) + |> post("/api/v1/statuses/#{activity_two.id}/pin") + |> json_response_and_validate_schema(400) + end + + test "on pin removes deletion job, on unpin reschedule deletion" do + %{conn: conn} = oauth_access(["write:accounts", "write:statuses"]) + expires_in = 2 * 60 * 60 + + expires_at = DateTime.add(DateTime.utc_now(), expires_in) + + assert %{"id" => id} = + conn + |> put_req_header("content-type", "application/json") + |> post("api/v1/statuses", %{ + "status" => "oolong", + "expires_in" => expires_in + }) + |> json_response_and_validate_schema(200) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: id}, + scheduled_at: expires_at + ) + + assert %{"id" => ^id, "pinned" => true} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{id}/pin") + |> json_response_and_validate_schema(200) + + refute_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: id}, + scheduled_at: expires_at + ) + + assert %{"id" => ^id, "pinned" => false} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{id}/unpin") + |> json_response_and_validate_schema(200) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: id}, + scheduled_at: expires_at + ) + end + end + + describe "cards" do + setup do + Config.put([:rich_media, :enabled], true) + + oauth_access(["read:statuses"]) + end + + test "returns rich-media card", %{conn: conn, user: user} do + Tesla.Mock.mock(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(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" + + %{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"]) + author = insert(:user) + + {:ok, activity1} = CommonAPI.post(author, %{status: "heweoo?"}) + {:ok, activity2} = CommonAPI.post(author, %{status: "heweoo!"}) + + response1 = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity1.id}/bookmark") + + assert json_response_and_validate_schema(response1, 200)["bookmarked"] == true + + response2 = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity2.id}/bookmark") + + assert json_response_and_validate_schema(response2, 200)["bookmarked"] == true + + bookmarks = get(conn, bookmarks_uri) + + assert [ + json_response_and_validate_schema(response2, 200), + json_response_and_validate_schema(response1, 200) + ] == + json_response_and_validate_schema(bookmarks, 200) + + response1 = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity1.id}/unbookmark") + + assert json_response_and_validate_schema(response1, 200)["bookmarked"] == false + + bookmarks = get(conn, bookmarks_uri) + + assert [json_response_and_validate_schema(response2, 200)] == + json_response_and_validate_schema(bookmarks, 200) + end + + describe "conversation muting" do + setup do: oauth_access(["write:mutes"]) + + setup do + post_user = insert(:user) + {:ok, activity} = CommonAPI.post(post_user, %{status: "HIE"}) + %{activity: activity} + end + + test "mute conversation", %{conn: conn, activity: activity} do + id_str = to_string(activity.id) + + assert %{"id" => ^id_str, "muted" => true} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/mute") + |> json_response_and_validate_schema(200) + end + + test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do + {:ok, _} = CommonAPI.add_mute(user, activity) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/mute") + + assert json_response_and_validate_schema(conn, 400) == %{ + "error" => "conversation is already muted" + } + end + + test "unmute conversation", %{conn: conn, user: user, activity: activity} do + {:ok, _} = CommonAPI.add_mute(user, activity) + + id_str = to_string(activity.id) + + assert %{"id" => ^id_str, "muted" => false} = + conn + # |> assign(:user, user) + |> post("/api/v1/statuses/#{activity.id}/unmute") + |> json_response_and_validate_schema(200) + end + end + + test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do + user1 = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + + {:ok, replied_to} = CommonAPI.post(user1, %{status: "cofe"}) + + # Reply to status from another user + conn1 = + conn + |> assign(:user, user2) + |> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"])) + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id}) + + assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn1, 200) + + activity = Activity.get_by_id_with_object(id) + + assert Object.normalize(activity).data["inReplyTo"] == Object.normalize(replied_to).data["id"] + assert Activity.get_in_reply_to_activity(activity).id == replied_to.id + + # Reblog from the third user + conn2 = + conn + |> assign(:user, user3) + |> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"])) + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/reblog") + + assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} = + json_response_and_validate_schema(conn2, 200) + + assert to_string(activity.id) == id + + # Getting third user status + conn3 = + conn + |> assign(:user, user3) + |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"])) + |> get("api/v1/timelines/home") + + [reblogged_activity] = json_response(conn3, 200) + + assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id + + replied_to_user = User.get_by_ap_id(replied_to.data["actor"]) + assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id + end + + describe "GET /api/v1/statuses/:id/favourited_by" do + setup do: oauth_access(["read:accounts"]) + + setup %{user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "test"}) + + %{activity: activity} + end + + test "returns users who have favorited the status", %{conn: conn, activity: activity} do + other_user = insert(:user) + {:ok, _} = CommonAPI.favorite(other_user, activity.id) + + response = + conn + |> get("/api/v1/statuses/#{activity.id}/favourited_by") + |> json_response_and_validate_schema(:ok) + + [%{"id" => id}] = response + + assert id == other_user.id + end + + test "returns empty array when status has not been favorited yet", %{ + conn: conn, + activity: activity + } do + response = + conn + |> get("/api/v1/statuses/#{activity.id}/favourited_by") + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response) + end + + test "does not return users who have favorited the status but are blocked", %{ + conn: %{assigns: %{user: user}} = conn, + activity: activity + } do + other_user = insert(:user) + {:ok, _user_relationship} = User.block(user, other_user) + + {:ok, _} = CommonAPI.favorite(other_user, activity.id) + + response = + conn + |> get("/api/v1/statuses/#{activity.id}/favourited_by") + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response) + end + + test "does not fail on an unauthenticated request", %{activity: activity} do + other_user = insert(:user) + {:ok, _} = CommonAPI.favorite(other_user, activity.id) + + response = + build_conn() + |> get("/api/v1/statuses/#{activity.id}/favourited_by") + |> json_response_and_validate_schema(:ok) + + [%{"id" => id}] = response + assert id == other_user.id + end + + test "requires authentication for private posts", %{user: user} do + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "@#{other_user.nickname} wanna get some #cofe together?", + visibility: "direct" + }) + + {:ok, _} = CommonAPI.favorite(other_user, activity.id) + + favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by" + + build_conn() + |> get(favourited_by_url) + |> json_response_and_validate_schema(404) + + conn = + build_conn() + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) + + conn + |> assign(:token, nil) + |> get(favourited_by_url) + |> json_response_and_validate_schema(404) + + response = + conn + |> get(favourited_by_url) + |> json_response_and_validate_schema(200) + + [%{"id" => id}] = response + assert id == other_user.id + end + + test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do + clear_config([:instance, :show_reactions], false) + + other_user = insert(:user) + {:ok, _} = CommonAPI.favorite(other_user, activity.id) + + response = + conn + |> get("/api/v1/statuses/#{activity.id}/favourited_by") + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response) + end + end + + describe "GET /api/v1/statuses/:id/reblogged_by" do + setup do: oauth_access(["read:accounts"]) + + setup %{user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "test"}) + + %{activity: activity} + end + + test "returns users who have reblogged the status", %{conn: conn, activity: activity} do + other_user = insert(:user) + {:ok, _} = CommonAPI.repeat(activity.id, other_user) + + response = + conn + |> get("/api/v1/statuses/#{activity.id}/reblogged_by") + |> json_response_and_validate_schema(:ok) + + [%{"id" => id}] = response + + assert id == other_user.id + end + + test "returns empty array when status has not been reblogged yet", %{ + conn: conn, + activity: activity + } do + response = + conn + |> get("/api/v1/statuses/#{activity.id}/reblogged_by") + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response) + end + + test "does not return users who have reblogged the status but are blocked", %{ + conn: %{assigns: %{user: user}} = conn, + activity: activity + } do + other_user = insert(:user) + {:ok, _user_relationship} = User.block(user, other_user) + + {:ok, _} = CommonAPI.repeat(activity.id, other_user) + + response = + conn + |> get("/api/v1/statuses/#{activity.id}/reblogged_by") + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response) + end + + test "does not return users who have reblogged the status privately", %{ + conn: conn + } do + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(other_user, %{status: "my secret post"}) + + {:ok, _} = CommonAPI.repeat(activity.id, other_user, %{visibility: "private"}) + + response = + conn + |> get("/api/v1/statuses/#{activity.id}/reblogged_by") + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response) + end + + test "does not fail on an unauthenticated request", %{activity: activity} do + other_user = insert(:user) + {:ok, _} = CommonAPI.repeat(activity.id, other_user) + + response = + build_conn() + |> get("/api/v1/statuses/#{activity.id}/reblogged_by") + |> json_response_and_validate_schema(:ok) + + [%{"id" => id}] = response + assert id == other_user.id + end + + test "requires authentication for private posts", %{user: user} do + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "@#{other_user.nickname} wanna get some #cofe together?", + visibility: "direct" + }) + + build_conn() + |> get("/api/v1/statuses/#{activity.id}/reblogged_by") + |> json_response_and_validate_schema(404) + + response = + build_conn() + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) + |> get("/api/v1/statuses/#{activity.id}/reblogged_by") + |> json_response_and_validate_schema(200) + + assert [] == response + end + end + + test "context" do + user = insert(:user) + + {:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"}) + {:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1}) + {:ok, %{id: id3}} = CommonAPI.post(user, %{status: "3", in_reply_to_status_id: id2}) + {:ok, %{id: id4}} = CommonAPI.post(user, %{status: "4", in_reply_to_status_id: id3}) + {:ok, %{id: id5}} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4}) + + response = + build_conn() + |> get("/api/v1/statuses/#{id3}/context") + |> json_response_and_validate_schema(:ok) + + assert %{ + "ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}], + "descendants" => [%{"id" => ^id4}, %{"id" => ^id5}] + } = response + end + + test "favorites paginate correctly" do + %{user: user, conn: conn} = oauth_access(["read:favourites"]) + other_user = insert(:user) + {:ok, first_post} = CommonAPI.post(other_user, %{status: "bla"}) + {:ok, second_post} = CommonAPI.post(other_user, %{status: "bla"}) + {:ok, third_post} = CommonAPI.post(other_user, %{status: "bla"}) + + {:ok, _first_favorite} = CommonAPI.favorite(user, third_post.id) + {:ok, _second_favorite} = CommonAPI.favorite(user, first_post.id) + {:ok, third_favorite} = CommonAPI.favorite(user, second_post.id) + + result = + conn + |> get("/api/v1/favourites?limit=1") + + assert [%{"id" => post_id}] = json_response_and_validate_schema(result, 200) + assert post_id == second_post.id + + # Using the header for pagination works correctly + [next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ") + [_, max_id] = Regex.run(~r/max_id=([^&]+)/, next) + + assert max_id == third_favorite.id + + result = + conn + |> get("/api/v1/favourites?max_id=#{max_id}") + + assert [%{"id" => first_post_id}, %{"id" => third_post_id}] = + json_response_and_validate_schema(result, 200) + + assert first_post_id == first_post.id + assert third_post_id == third_post.id + end + + test "returns the favorites of a user" do + %{user: user, conn: conn} = oauth_access(["read:favourites"]) + other_user = insert(:user) + + {:ok, _} = CommonAPI.post(other_user, %{status: "bla"}) + {:ok, activity} = CommonAPI.post(other_user, %{status: "trees are happy"}) + + {:ok, last_like} = CommonAPI.favorite(user, activity.id) + + first_conn = get(conn, "/api/v1/favourites") + + assert [status] = json_response_and_validate_schema(first_conn, 200) + assert status["id"] == to_string(activity.id) + + assert [{"link", _link_header}] = + Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end) + + # Honours query params + {:ok, second_activity} = + CommonAPI.post(other_user, %{ + status: "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful." + }) + + {:ok, _} = CommonAPI.favorite(user, second_activity.id) + + second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like.id}") + + assert [second_status] = json_response_and_validate_schema(second_conn, 200) + assert second_status["id"] == to_string(second_activity.id) + + third_conn = get(conn, "/api/v1/favourites?limit=0") + + assert [] = json_response_and_validate_schema(third_conn, 200) + end + + test "expires_at is nil for another user" do + %{conn: conn, user: user} = oauth_access(["read:statuses"]) + expires_at = DateTime.add(DateTime.utc_now(), 1_000_000) + {:ok, activity} = CommonAPI.post(user, %{status: "foobar", expires_in: 1_000_000}) + + assert %{"pleroma" => %{"expires_at" => a_expires_at}} = + conn + |> get("/api/v1/statuses/#{activity.id}") + |> json_response_and_validate_schema(:ok) + + {:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at) + assert DateTime.diff(expires_at, a_expires_at) == 0 + + %{conn: conn} = oauth_access(["read:statuses"]) + + assert %{"pleroma" => %{"expires_at" => nil}} = + conn + |> get("/api/v1/statuses/#{activity.id}") + |> json_response_and_validate_schema(:ok) + 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 @@ -0,0 +1,199 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Web.Push + alias Pleroma.Web.Push.Subscription + + @sub %{ + "endpoint" => "https://example.com/example/1234", + "keys" => %{ + "auth" => "8eDyX_uCN0XRhSbY5hs7Hg==", + "p256dh" => + "BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=" + } + } + @server_key Keyword.get(Push.vapid_config(), :public_key) + + setup do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: ["push"]) + + conn = + build_conn() + |> assign(:user, user) + |> assign(:token, token) + |> put_req_header("content-type", "application/json") + + %{conn: conn, user: user, token: token} + end + + 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 "creates push subscription" do + test "returns error when push disabled ", %{conn: conn} do + assert_error_when_disable_push do + conn + |> post("/api/v1/push/subscription", %{subscription: @sub}) + |> json_response_and_validate_schema(403) + end + end + + test "successful creation", %{conn: conn} do + result = + conn + |> post("/api/v1/push/subscription", %{ + "data" => %{ + "alerts" => %{"mention" => true, "test" => true, "pleroma:chat_mention" => true} + }, + "subscription" => @sub + }) + |> json_response_and_validate_schema(200) + + [subscription] = Pleroma.Repo.all(Subscription) + + assert %{ + "alerts" => %{"mention" => true, "pleroma:chat_mention" => true}, + "endpoint" => subscription.endpoint, + "id" => to_string(subscription.id), + "server_key" => @server_key + } == result + end + end + + describe "gets a user subscription" do + test "returns error when push disabled ", %{conn: conn} do + assert_error_when_disable_push do + conn + |> get("/api/v1/push/subscription", %{}) + |> json_response_and_validate_schema(403) + end + end + + test "returns error when user hasn't subscription", %{conn: conn} do + res = + conn + |> get("/api/v1/push/subscription", %{}) + |> json_response_and_validate_schema(404) + + assert %{"error" => "Record not found"} == res + end + + test "returns a user subsciption", %{conn: conn, user: user, token: token} do + subscription = + insert(:push_subscription, + user: user, + token: token, + data: %{"alerts" => %{"mention" => true}} + ) + + res = + conn + |> get("/api/v1/push/subscription", %{}) + |> json_response_and_validate_schema(200) + + expect = %{ + "alerts" => %{"mention" => true}, + "endpoint" => "https://example.com/example/1234", + "id" => to_string(subscription.id), + "server_key" => @server_key + } + + assert expect == res + end + end + + describe "updates a user subsciption" do + setup %{conn: conn, user: user, token: token} do + subscription = + insert(:push_subscription, + user: user, + token: token, + data: %{"alerts" => %{"mention" => true}} + ) + + %{conn: conn, user: user, token: token, subscription: subscription} + end + + test "returns error when push disabled ", %{conn: conn} do + assert_error_when_disable_push do + conn + |> put("/api/v1/push/subscription", %{data: %{"alerts" => %{"mention" => false}}}) + |> json_response_and_validate_schema(403) + end + end + + test "returns updated subsciption", %{conn: conn, subscription: subscription} do + res = + conn + |> put("/api/v1/push/subscription", %{ + data: %{"alerts" => %{"mention" => false, "follow" => true}} + }) + |> json_response_and_validate_schema(200) + + expect = %{ + "alerts" => %{"follow" => true, "mention" => false}, + "endpoint" => "https://example.com/example/1234", + "id" => to_string(subscription.id), + "server_key" => @server_key + } + + assert expect == res + end + end + + describe "deletes the user subscription" do + test "returns error when push disabled ", %{conn: conn} do + assert_error_when_disable_push do + conn + |> delete("/api/v1/push/subscription", %{}) + |> json_response_and_validate_schema(403) + end + end + + test "returns error when user hasn't subscription", %{conn: conn} do + res = + conn + |> delete("/api/v1/push/subscription", %{}) + |> json_response_and_validate_schema(404) + + assert %{"error" => "Record not found"} == res + end + + test "returns empty result and delete user subsciption", %{ + conn: conn, + user: user, + token: token + } do + subscription = + insert(:push_subscription, + user: user, + token: token, + data: %{"alerts" => %{"mention" => true}} + ) + + res = + conn + |> delete("/api/v1/push/subscription", %{}) + |> json_response_and_validate_schema(200) + + assert %{} == res + refute Pleroma.Repo.get(Subscription, subscription.id) + end + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/suggestion_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/suggestion_controller_test.exs @@ -0,0 +1,18 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do + use Pleroma.Web.ConnCase + + setup do: oauth_access(["read"]) + + test "returns empty result", %{conn: conn} do + res = + conn + |> get("/api/v1/suggestions") + |> json_response_and_validate_schema(200) + + assert res == [] + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -0,0 +1,560 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + import Tesla.Mock + + alias Pleroma.Config + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "home" do + setup do: oauth_access(["read:statuses"]) + + test "does NOT embed account/pleroma/relationship in statuses", %{ + user: user, + conn: conn + } do + other_user = insert(:user) + + {:ok, _} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + + response = + conn + |> assign(:user, user) + |> get("/api/v1/timelines/home") + |> json_response_and_validate_schema(200) + + assert Enum.all?(response, fn n -> + get_in(n, ["account", "pleroma", "relationship"]) == %{} + end) + end + + test "the home timeline when the direct messages are excluded", %{user: user, conn: conn} do + {:ok, public_activity} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + {:ok, direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + + {:ok, unlisted_activity} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + + {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"}) + + conn = get(conn, "/api/v1/timelines/home?exclude_visibilities[]=direct") + + assert status_ids = json_response_and_validate_schema(conn, :ok) |> Enum.map(& &1["id"]) + assert public_activity.id in status_ids + assert unlisted_activity.id in status_ids + assert private_activity.id in status_ids + refute direct_activity.id in status_ids + end + end + + describe "public" do + @tag capture_log: true + test "the public timeline", %{conn: conn} do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test"}) + + _activity = insert(:note_activity, local: false) + + conn = get(conn, "/api/v1/timelines/public?local=False") + + assert length(json_response_and_validate_schema(conn, :ok)) == 2 + + conn = get(build_conn(), "/api/v1/timelines/public?local=True") + + assert [%{"content" => "test"}] = json_response_and_validate_schema(conn, :ok) + + conn = get(build_conn(), "/api/v1/timelines/public?local=1") + + assert [%{"content" => "test"}] = json_response_and_validate_schema(conn, :ok) + + # does not contain repeats + {:ok, _} = CommonAPI.repeat(activity.id, user) + + conn = get(build_conn(), "/api/v1/timelines/public?local=true") + + assert [_] = json_response_and_validate_schema(conn, :ok) + end + + test "the public timeline includes only public statuses for an authenticated user" do + %{user: user, conn: conn} = oauth_access(["read:statuses"]) + + {:ok, _activity} = CommonAPI.post(user, %{status: "test"}) + {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "private"}) + {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "unlisted"}) + {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "direct"}) + + res_conn = get(conn, "/api/v1/timelines/public") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + end + + test "doesn't return replies if follower is posting with blocked user" do + %{conn: conn, user: blocker} = oauth_access(["read:statuses"]) + [blockee, friend] = insert_list(2, :user) + {:ok, blocker} = User.follow(blocker, friend) + {:ok, _} = User.block(blocker, blockee) + + conn = assign(conn, :user, blocker) + + {:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"}) + + {:ok, reply_from_blockee} = + CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity}) + + {:ok, _reply_from_friend} = + CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee}) + + # Still shows replies from yourself + {:ok, %{id: reply_from_me}} = + CommonAPI.post(blocker, %{status: "status", in_reply_to_status_id: reply_from_blockee}) + + response = + get(conn, "/api/v1/timelines/public") + |> json_response_and_validate_schema(200) + + assert length(response) == 2 + [%{"id" => ^reply_from_me}, %{"id" => ^activity_id}] = response + end + + test "doesn't return replies if follow is posting with users from blocked domain" do + %{conn: conn, user: blocker} = oauth_access(["read:statuses"]) + friend = insert(:user) + blockee = insert(:user, ap_id: "https://example.com/users/blocked") + {:ok, blocker} = User.follow(blocker, friend) + {:ok, blocker} = User.block_domain(blocker, "example.com") + + conn = assign(conn, :user, blocker) + + {:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"}) + + {:ok, reply_from_blockee} = + CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity}) + + {:ok, _reply_from_friend} = + CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee}) + + res_conn = get(conn, "/api/v1/timelines/public") + + activities = json_response_and_validate_schema(res_conn, 200) + [%{"id" => ^activity_id}] = activities + end + end + + defp local_and_remote_activities do + insert(:note_activity) + insert(:note_activity, local: false) + :ok + end + + describe "public with restrict unauthenticated timeline for local and federated timelines" do + setup do: local_and_remote_activities() + + setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true) + + setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true) + + test "if user is unauthenticated", %{conn: conn} do + res_conn = get(conn, "/api/v1/timelines/public?local=true") + + assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ + "error" => "authorization required for timeline view" + } + + res_conn = get(conn, "/api/v1/timelines/public?local=false") + + assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ + "error" => "authorization required for timeline view" + } + end + + test "if user is authenticated" do + %{conn: conn} = oauth_access(["read:statuses"]) + + res_conn = get(conn, "/api/v1/timelines/public?local=true") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/timelines/public?local=false") + assert length(json_response_and_validate_schema(res_conn, 200)) == 2 + end + end + + describe "public with restrict unauthenticated timeline for local" do + setup do: local_and_remote_activities() + + setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true) + + test "if user is unauthenticated", %{conn: conn} do + res_conn = get(conn, "/api/v1/timelines/public?local=true") + + assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ + "error" => "authorization required for timeline view" + } + + res_conn = get(conn, "/api/v1/timelines/public?local=false") + assert length(json_response_and_validate_schema(res_conn, 200)) == 2 + end + + test "if user is authenticated", %{conn: _conn} do + %{conn: conn} = oauth_access(["read:statuses"]) + + res_conn = get(conn, "/api/v1/timelines/public?local=true") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/timelines/public?local=false") + assert length(json_response_and_validate_schema(res_conn, 200)) == 2 + end + end + + describe "public with restrict unauthenticated timeline for remote" do + setup do: local_and_remote_activities() + + setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true) + + test "if user is unauthenticated", %{conn: conn} do + res_conn = get(conn, "/api/v1/timelines/public?local=true") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/timelines/public?local=false") + + assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ + "error" => "authorization required for timeline view" + } + end + + test "if user is authenticated", %{conn: _conn} do + %{conn: conn} = oauth_access(["read:statuses"]) + + res_conn = get(conn, "/api/v1/timelines/public?local=true") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/timelines/public?local=false") + assert length(json_response_and_validate_schema(res_conn, 200)) == 2 + end + end + + describe "direct" do + test "direct timeline", %{conn: conn} do + user_one = insert(:user) + user_two = insert(:user) + + {:ok, user_two} = User.follow(user_two, user_one) + + {:ok, direct} = + CommonAPI.post(user_one, %{ + status: "Hi @#{user_two.nickname}!", + visibility: "direct" + }) + + {:ok, _follower_only} = + CommonAPI.post(user_one, %{ + status: "Hi @#{user_two.nickname}!", + visibility: "private" + }) + + conn_user_two = + conn + |> assign(:user, user_two) + |> 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") + + assert [status] = json_response_and_validate_schema(res_conn, :ok) + + assert %{"visibility" => "direct"} = status + assert status["url"] != direct.data["id"] + + # User should be able to see their own direct message + res_conn = + build_conn() + |> assign(:user, user_one) + |> assign(:token, insert(:oauth_token, user: user_one, scopes: ["read:statuses"])) + |> 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") + + [_s1, _s2] = json_response_and_validate_schema(res_conn, :ok) + + # Test pagination + Enum.each(1..20, fn _ -> + {:ok, _} = + CommonAPI.post(user_one, %{ + status: "Hi @#{user_two.nickname}!", + visibility: "direct" + }) + end) + + 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}") + + assert [status] = json_response_and_validate_schema(res_conn, :ok) + + assert status["url"] != direct.data["id"] + end + + test "doesn't include DMs from blocked users" do + %{user: blocker, conn: conn} = oauth_access(["read:statuses"]) + blocked = insert(:user) + other_user = insert(:user) + {:ok, _user_relationship} = User.block(blocker, blocked) + + {:ok, _blocked_direct} = + CommonAPI.post(blocked, %{ + status: "Hi @#{blocker.nickname}!", + visibility: "direct" + }) + + {:ok, direct} = + CommonAPI.post(other_user, %{ + status: "Hi @#{blocker.nickname}!", + visibility: "direct" + }) + + res_conn = get(conn, "api/v1/timelines/direct") + + [status] = json_response_and_validate_schema(res_conn, :ok) + assert status["id"] == direct.id + end + end + + describe "list" do + setup do: oauth_access(["read:lists"]) + + test "does not contain retoots", %{user: user, conn: conn} do + other_user = insert(:user) + {:ok, activity_one} = CommonAPI.post(user, %{status: "Marisa is cute."}) + {:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is stupid."}) + {:ok, _} = CommonAPI.repeat(activity_one.id, other_user) + + {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + + conn = get(conn, "/api/v1/timelines/list/#{list.id}") + + assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok) + + assert id == to_string(activity_two.id) + end + + test "works with pagination", %{user: user, conn: conn} do + other_user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + + Enum.each(1..30, fn i -> + CommonAPI.post(other_user, %{status: "post number #{i}"}) + end) + + res = + get(conn, "/api/v1/timelines/list/#{list.id}?limit=1") + |> json_response_and_validate_schema(:ok) + + assert length(res) == 1 + + [first] = res + + res = + get(conn, "/api/v1/timelines/list/#{list.id}?max_id=#{first["id"]}&limit=30") + |> json_response_and_validate_schema(:ok) + + assert length(res) == 29 + end + + test "list timeline", %{user: user, conn: conn} do + other_user = insert(:user) + {:ok, _activity_one} = CommonAPI.post(user, %{status: "Marisa is cute."}) + {:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is cute."}) + {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + + conn = get(conn, "/api/v1/timelines/list/#{list.id}") + + assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok) + + assert id == to_string(activity_two.id) + end + + test "list timeline does not leak non-public statuses for unfollowed users", %{ + user: user, + conn: conn + } do + other_user = insert(:user) + {:ok, activity_one} = CommonAPI.post(other_user, %{status: "Marisa is cute."}) + + {:ok, _activity_two} = + CommonAPI.post(other_user, %{ + status: "Marisa is cute.", + visibility: "private" + }) + + {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + + conn = get(conn, "/api/v1/timelines/list/#{list.id}") + + assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok) + + assert id == to_string(activity_one.id) + end + end + + describe "hashtag" do + setup do: oauth_access(["n/a"]) + + @tag capture_log: true + test "hashtag timeline", %{conn: conn} do + following = insert(:user) + + {:ok, activity} = CommonAPI.post(following, %{status: "test #2hu"}) + + nconn = get(conn, "/api/v1/timelines/tag/2hu") + + assert [%{"id" => id}] = json_response_and_validate_schema(nconn, :ok) + + assert id == to_string(activity.id) + + # works for different capitalization too + nconn = get(conn, "/api/v1/timelines/tag/2HU") + + assert [%{"id" => id}] = json_response_and_validate_schema(nconn, :ok) + + assert id == to_string(activity.id) + end + + test "multi-hashtag timeline", %{conn: conn} do + user = insert(:user) + + {:ok, activity_test} = CommonAPI.post(user, %{status: "#test"}) + {:ok, activity_test1} = CommonAPI.post(user, %{status: "#test #test1"}) + {:ok, activity_none} = CommonAPI.post(user, %{status: "#test #none"}) + + any_test = get(conn, "/api/v1/timelines/tag/test?any[]=test1") + + [status_none, status_test1, status_test] = json_response_and_validate_schema(any_test, :ok) + + assert to_string(activity_test.id) == status_test["id"] + assert to_string(activity_test1.id) == status_test1["id"] + assert to_string(activity_none.id) == status_none["id"] + + restricted_test = get(conn, "/api/v1/timelines/tag/test?all[]=test1&none[]=none") + + assert [status_test1] == json_response_and_validate_schema(restricted_test, :ok) + + all_test = get(conn, "/api/v1/timelines/tag/test?all[]=none") + + assert [status_none] == json_response_and_validate_schema(all_test, :ok) + end + end + + describe "hashtag timeline handling of :restrict_unauthenticated setting" do + setup do + user = insert(:user) + {:ok, activity1} = CommonAPI.post(user, %{status: "test #tag1"}) + {:ok, _activity2} = CommonAPI.post(user, %{status: "test #tag1"}) + + activity1 + |> Ecto.Changeset.change(%{local: false}) + |> Pleroma.Repo.update() + + base_uri = "/api/v1/timelines/tag/tag1" + error_response = %{"error" => "authorization required for timeline view"} + + %{base_uri: base_uri, error_response: error_response} + end + + defp ensure_authenticated_access(base_uri) do + %{conn: auth_conn} = oauth_access(["read:statuses"]) + + res_conn = get(auth_conn, "#{base_uri}?local=true") + assert length(json_response(res_conn, 200)) == 1 + + res_conn = get(auth_conn, "#{base_uri}?local=false") + assert length(json_response(res_conn, 200)) == 2 + end + + test "with default settings on private instances, returns 403 for unauthenticated users", %{ + conn: conn, + base_uri: base_uri, + error_response: error_response + } do + clear_config([:instance, :public], false) + clear_config([:restrict_unauthenticated, :timelines]) + + for local <- [true, false] do + res_conn = get(conn, "#{base_uri}?local=#{local}") + + assert json_response(res_conn, :unauthorized) == error_response + end + + ensure_authenticated_access(base_uri) + end + + test "with `%{local: true, federated: true}`, returns 403 for unauthenticated users", %{ + conn: conn, + base_uri: base_uri, + error_response: error_response + } do + clear_config([:restrict_unauthenticated, :timelines, :local], true) + clear_config([:restrict_unauthenticated, :timelines, :federated], true) + + for local <- [true, false] do + res_conn = get(conn, "#{base_uri}?local=#{local}") + + assert json_response(res_conn, :unauthorized) == error_response + end + + ensure_authenticated_access(base_uri) + end + + test "with `%{local: false, federated: true}`, forbids unauthenticated access to federated timeline", + %{conn: conn, base_uri: base_uri, error_response: error_response} do + clear_config([:restrict_unauthenticated, :timelines, :local], false) + clear_config([:restrict_unauthenticated, :timelines, :federated], true) + + res_conn = get(conn, "#{base_uri}?local=true") + assert length(json_response(res_conn, 200)) == 1 + + res_conn = get(conn, "#{base_uri}?local=false") + assert json_response(res_conn, :unauthorized) == error_response + + ensure_authenticated_access(base_uri) + end + + test "with `%{local: true, federated: false}`, forbids unauthenticated access to public timeline" <> + "(but not to local public activities which are delivered as part of federated timeline)", + %{conn: conn, base_uri: base_uri, error_response: error_response} do + clear_config([:restrict_unauthenticated, :timelines, :local], true) + clear_config([:restrict_unauthenticated, :timelines, :federated], false) + + res_conn = get(conn, "#{base_uri}?local=true") + assert json_response(res_conn, :unauthorized) == error_response + + # Note: local activities get delivered as part of federated timeline + res_conn = get(conn, "#{base_uri}?local=false") + assert length(json_response(res_conn, 200)) == 2 + + ensure_authenticated_access(base_uri) + end + end +end diff --git a/test/pleroma/web/mastodon_api/masto_fe_controller_test.exs b/test/pleroma/web/mastodon_api/masto_fe_controller_test.exs @@ -0,0 +1,85 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MastoFEControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Config + alias Pleroma.User + + import Pleroma.Factory + + setup do: clear_config([:instance, :public]) + + test "put settings", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:accounts"])) + |> put("/api/web/settings", %{"data" => %{"programming" => "socks"}}) + + assert _result = json_response(conn, 200) + + user = User.get_cached_by_ap_id(user.ap_id) + assert user.mastofe_settings == %{"programming" => "socks"} + end + + describe "index/2 redirections" do + setup %{conn: conn} do + session_opts = [ + store: :cookie, + key: "_test", + signing_salt: "cooldude" + ] + + conn = + conn + |> Plug.Session.call(Plug.Session.init(session_opts)) + |> fetch_session() + + test_path = "/web/statuses/test" + %{conn: conn, path: test_path} + end + + test "redirects not logged-in users to the login page", %{conn: conn, path: path} do + conn = get(conn, path) + + assert conn.status == 302 + assert redirected_to(conn) == "/web/login" + end + + test "redirects not logged-in users to the login page on private instances", %{ + conn: conn, + path: path + } do + Config.put([:instance, :public], false) + + conn = get(conn, path) + + assert conn.status == 302 + assert redirected_to(conn) == "/web/login" + end + + test "does not redirect logged in users to the login page", %{conn: conn, path: path} do + token = insert(:oauth_token, scopes: ["read"]) + + conn = + conn + |> assign(:user, token.user) + |> assign(:token, token) + |> get(path) + + assert conn.status == 200 + end + + test "saves referer path to session", %{conn: conn, path: path} do + conn = get(conn, path) + return_to = Plug.Conn.get_session(conn, :return_to) + + assert return_to == path + end + end +end diff --git a/test/pleroma/web/mastodon_api/mastodon_api_controller_test.exs b/test/pleroma/web/mastodon_api/mastodon_api_controller_test.exs @@ -0,0 +1,34 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do + use Pleroma.Web.ConnCase + + describe "empty_array/2 (stubs)" do + test "GET /api/v1/accounts/:id/identity_proofs" do + %{user: user, conn: conn} = oauth_access(["read:accounts"]) + + assert [] == + conn + |> get("/api/v1/accounts/#{user.id}/identity_proofs") + |> json_response(200) + end + + test "GET /api/v1/endorsements" do + %{conn: conn} = oauth_access(["read:accounts"]) + + assert [] == + conn + |> get("/api/v1/endorsements") + |> json_response(200) + end + + test "GET /api/v1/trends", %{conn: conn} do + assert [] == + conn + |> get("/api/v1/trends") + |> json_response(200) + end + end +end diff --git a/test/pleroma/web/mastodon_api/mastodon_api_test.exs b/test/pleroma/web/mastodon_api/mastodon_api_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.Web.MastodonAPI.MastodonAPITest do + use Pleroma.Web.ConnCase + + alias Pleroma.Notification + alias Pleroma.ScheduledActivity + alias Pleroma.User + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.MastodonAPI + + import Pleroma.Factory + + describe "follow/3" do + test "returns error when followed user is deactivated" do + follower = insert(:user) + user = insert(:user, local: true, deactivated: true) + assert {:error, _error} = MastodonAPI.follow(follower, user) + end + + test "following for user" do + follower = insert(:user) + user = insert(:user) + {:ok, follower} = MastodonAPI.follow(follower, user) + assert User.following?(follower, user) + end + + test "returns ok if user already followed" do + follower = insert(:user) + user = insert(:user) + {:ok, follower} = User.follow(follower, user) + {:ok, follower} = MastodonAPI.follow(follower, refresh_record(user)) + assert User.following?(follower, user) + end + end + + describe "get_followers/2" do + test "returns user followers" do + follower1_user = insert(:user) + follower2_user = insert(:user) + user = insert(:user) + {:ok, _follower1_user} = User.follow(follower1_user, user) + {:ok, follower2_user} = User.follow(follower2_user, user) + + assert MastodonAPI.get_followers(user, %{"limit" => 1}) == [follower2_user] + end + end + + describe "get_friends/2" do + test "returns user friends" do + user = insert(:user) + followed_one = insert(:user) + followed_two = insert(:user) + followed_three = insert(:user) + + {:ok, user} = User.follow(user, followed_one) + {:ok, user} = User.follow(user, followed_two) + {:ok, user} = User.follow(user, followed_three) + res = MastodonAPI.get_friends(user) + + assert length(res) == 3 + assert Enum.member?(res, refresh_record(followed_three)) + assert Enum.member?(res, refresh_record(followed_two)) + assert Enum.member?(res, refresh_record(followed_one)) + end + end + + describe "get_notifications/2" do + test "returns notifications for user" do + user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, user) + + {:ok, status} = CommonAPI.post(user, %{status: "Akariiiin"}) + + {:ok, status1} = CommonAPI.post(user, %{status: "Magi"}) + {:ok, [notification]} = Notification.create_notifications(status) + {:ok, [notification1]} = Notification.create_notifications(status1) + res = MastodonAPI.get_notifications(subscriber) + + assert Enum.member?(Enum.map(res, & &1.id), notification.id) + assert Enum.member?(Enum.map(res, & &1.id), notification1.id) + end + end + + describe "get_scheduled_activities/2" do + test "returns user scheduled activities" do + user = insert(:user) + + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + + attrs = %{params: %{}, scheduled_at: today} + {:ok, schedule} = ScheduledActivity.create(user, attrs) + assert MastodonAPI.get_scheduled_activities(user) == [schedule] + end + end +end diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs @@ -0,0 +1,529 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do + alias Pleroma.Repo + alias Pleroma.User + + use Pleroma.Web.ConnCase + + import Mock + import Pleroma.Factory + + setup do: clear_config([:instance, :max_account_fields]) + + describe "updating credentials" do + setup do: oauth_access(["write:accounts"]) + setup :request_content_type + + test "sets user settings in a generic way", %{conn: conn} do + res_conn = + patch(conn, "/api/v1/accounts/update_credentials", %{ + "pleroma_settings_store" => %{ + pleroma_fe: %{ + theme: "bla" + } + } + }) + + assert user_data = json_response_and_validate_schema(res_conn, 200) + assert user_data["pleroma"]["settings_store"] == %{"pleroma_fe" => %{"theme" => "bla"}} + + user = Repo.get(User, user_data["id"]) + + res_conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{ + "pleroma_settings_store" => %{ + masto_fe: %{ + theme: "bla" + } + } + }) + + assert user_data = json_response_and_validate_schema(res_conn, 200) + + assert user_data["pleroma"]["settings_store"] == + %{ + "pleroma_fe" => %{"theme" => "bla"}, + "masto_fe" => %{"theme" => "bla"} + } + + user = Repo.get(User, user_data["id"]) + + clear_config([:instance, :federating], true) + + with_mock Pleroma.Web.Federator, + publish: fn _activity -> :ok end do + res_conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{ + "pleroma_settings_store" => %{ + masto_fe: %{ + theme: "blub" + } + } + }) + + assert user_data = json_response_and_validate_schema(res_conn, 200) + + assert user_data["pleroma"]["settings_store"] == + %{ + "pleroma_fe" => %{"theme" => "bla"}, + "masto_fe" => %{"theme" => "blub"} + } + + assert_called(Pleroma.Web.Federator.publish(:_)) + end + end + + test "updates the user's bio", %{conn: conn} do + user2 = insert(:user) + + raw_bio = "I drink #cofe with @#{user2.nickname}\n\nsuya.." + + conn = patch(conn, "/api/v1/accounts/update_credentials", %{"note" => raw_bio}) + + assert user_data = json_response_and_validate_schema(conn, 200) + + assert user_data["note"] == + ~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe">#cofe</a> with <span class="h-card"><a class="u-url mention" data-user="#{ + user2.id + }" href="#{user2.ap_id}" rel="ugc">@<span>#{user2.nickname}</span></a></span><br/><br/>suya..) + + assert user_data["source"]["note"] == raw_bio + + user = Repo.get(User, user_data["id"]) + + assert user.raw_bio == raw_bio + end + + test "updates the user's locking status", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{locked: "true"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["locked"] == true + end + + test "updates the user's chat acceptance status", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{accepts_chat_messages: "false"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["pleroma"]["accepts_chat_messages"] == false + end + + test "updates the user's allow_following_move", %{user: user, conn: conn} do + assert user.allow_following_move == true + + conn = patch(conn, "/api/v1/accounts/update_credentials", %{allow_following_move: "false"}) + + assert refresh_record(user).allow_following_move == false + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["pleroma"]["allow_following_move"] == false + end + + test "updates the user's default scope", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{default_scope: "unlisted"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["source"]["privacy"] == "unlisted" + end + + test "updates the user's privacy", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{source: %{privacy: "unlisted"}}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["source"]["privacy"] == "unlisted" + end + + test "updates the user's hide_followers status", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_followers: "true"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["pleroma"]["hide_followers"] == true + end + + test "updates the user's discoverable status", %{conn: conn} do + assert %{"source" => %{"pleroma" => %{"discoverable" => true}}} = + conn + |> patch("/api/v1/accounts/update_credentials", %{discoverable: "true"}) + |> json_response_and_validate_schema(:ok) + + assert %{"source" => %{"pleroma" => %{"discoverable" => false}}} = + conn + |> patch("/api/v1/accounts/update_credentials", %{discoverable: "false"}) + |> json_response_and_validate_schema(:ok) + end + + test "updates the user's hide_followers_count and hide_follows_count", %{conn: conn} do + conn = + patch(conn, "/api/v1/accounts/update_credentials", %{ + hide_followers_count: "true", + hide_follows_count: "true" + }) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["pleroma"]["hide_followers_count"] == true + assert user_data["pleroma"]["hide_follows_count"] == true + end + + test "updates the user's skip_thread_containment option", %{user: user, conn: conn} do + response = + conn + |> patch("/api/v1/accounts/update_credentials", %{skip_thread_containment: "true"}) + |> json_response_and_validate_schema(200) + + assert response["pleroma"]["skip_thread_containment"] == true + assert refresh_record(user).skip_thread_containment + end + + test "updates the user's hide_follows status", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_follows: "true"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["pleroma"]["hide_follows"] == true + end + + test "updates the user's hide_favorites status", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_favorites: "true"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["pleroma"]["hide_favorites"] == true + end + + test "updates the user's show_role status", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{show_role: "false"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["source"]["pleroma"]["show_role"] == false + end + + test "updates the user's no_rich_text status", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{no_rich_text: "true"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["source"]["pleroma"]["no_rich_text"] == true + end + + test "updates the user's name", %{conn: conn} do + conn = + patch(conn, "/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["display_name"] == "markorepairs" + + update_activity = Repo.one(Pleroma.Activity) + assert update_activity.data["type"] == "Update" + assert update_activity.data["object"]["name"] == "markorepairs" + end + + test "updates the user's avatar", %{user: user, conn: conn} do + new_avatar = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + assert user.avatar == %{} + + res = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar}) + + assert user_response = json_response_and_validate_schema(res, 200) + assert user_response["avatar"] != User.avatar_url(user) + + user = User.get_by_id(user.id) + refute user.avatar == %{} + + # Also resets it + _res = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => ""}) + + user = User.get_by_id(user.id) + assert user.avatar == nil + end + + test "updates the user's banner", %{user: user, conn: conn} do + new_header = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + res = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header}) + + assert user_response = json_response_and_validate_schema(res, 200) + assert user_response["header"] != User.banner_url(user) + + # Also resets it + _res = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => ""}) + + user = User.get_by_id(user.id) + assert user.banner == nil + end + + test "updates the user's background", %{conn: conn, user: user} do + new_header = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + res = + patch(conn, "/api/v1/accounts/update_credentials", %{ + "pleroma_background_image" => new_header + }) + + assert user_response = json_response_and_validate_schema(res, 200) + assert user_response["pleroma"]["background_image"] + # + # Also resets it + _res = + patch(conn, "/api/v1/accounts/update_credentials", %{"pleroma_background_image" => ""}) + + user = User.get_by_id(user.id) + assert user.background == nil + end + + test "requires 'write:accounts' permission" do + token1 = insert(:oauth_token, scopes: ["read"]) + token2 = insert(:oauth_token, scopes: ["write", "follow"]) + + for token <- [token1, token2] do + conn = + build_conn() + |> put_req_header("content-type", "multipart/form-data") + |> put_req_header("authorization", "Bearer #{token.token}") + |> patch("/api/v1/accounts/update_credentials", %{}) + + if token == token1 do + assert %{"error" => "Insufficient permissions: write:accounts."} == + json_response_and_validate_schema(conn, 403) + else + assert json_response_and_validate_schema(conn, 200) + end + end + end + + test "updates profile emojos", %{user: user, conn: conn} do + note = "*sips :blank:*" + name = "I am :firefox:" + + ret_conn = + patch(conn, "/api/v1/accounts/update_credentials", %{ + "note" => note, + "display_name" => name + }) + + assert json_response_and_validate_schema(ret_conn, 200) + + conn = get(conn, "/api/v1/accounts/#{user.id}") + + assert user_data = json_response_and_validate_schema(conn, 200) + + assert user_data["note"] == note + assert user_data["display_name"] == name + assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = user_data["emojis"] + end + + test "update fields", %{conn: conn} do + fields = [ + %{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "<script>bar</script>"}, + %{"name" => "link.io", "value" => "cofe.io"} + ] + + account_data = + conn + |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) + |> json_response_and_validate_schema(200) + + assert account_data["fields"] == [ + %{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "bar"}, + %{ + "name" => "link.io", + "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>) + } + ] + + assert account_data["source"]["fields"] == [ + %{ + "name" => "<a href=\"http://google.com\">foo</a>", + "value" => "<script>bar</script>" + }, + %{"name" => "link.io", "value" => "cofe.io"} + ] + end + + test "emojis in fields labels", %{conn: conn} do + fields = [ + %{"name" => ":firefox:", "value" => "is best 2hu"}, + %{"name" => "they wins", "value" => ":blank:"} + ] + + account_data = + conn + |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) + |> json_response_and_validate_schema(200) + + assert account_data["fields"] == [ + %{"name" => ":firefox:", "value" => "is best 2hu"}, + %{"name" => "they wins", "value" => ":blank:"} + ] + + assert account_data["source"]["fields"] == [ + %{"name" => ":firefox:", "value" => "is best 2hu"}, + %{"name" => "they wins", "value" => ":blank:"} + ] + + assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = account_data["emojis"] + end + + test "update fields via x-www-form-urlencoded", %{conn: conn} do + fields = + [ + "fields_attributes[1][name]=link", + "fields_attributes[1][value]=http://cofe.io", + "fields_attributes[0][name]=foo", + "fields_attributes[0][value]=bar" + ] + |> Enum.join("&") + + account = + conn + |> put_req_header("content-type", "application/x-www-form-urlencoded") + |> patch("/api/v1/accounts/update_credentials", fields) + |> json_response_and_validate_schema(200) + + assert account["fields"] == [ + %{"name" => "foo", "value" => "bar"}, + %{ + "name" => "link", + "value" => ~S(<a href="http://cofe.io" rel="ugc">http://cofe.io</a>) + } + ] + + assert account["source"]["fields"] == [ + %{"name" => "foo", "value" => "bar"}, + %{"name" => "link", "value" => "http://cofe.io"} + ] + end + + test "update fields with empty name", %{conn: conn} do + fields = [ + %{"name" => "foo", "value" => ""}, + %{"name" => "", "value" => "bar"} + ] + + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) + |> json_response_and_validate_schema(200) + + assert account["fields"] == [ + %{"name" => "foo", "value" => ""} + ] + end + + test "update fields when invalid request", %{conn: conn} do + name_limit = Pleroma.Config.get([:instance, :account_field_name_length]) + value_limit = Pleroma.Config.get([:instance, :account_field_value_length]) + + long_name = Enum.map(0..name_limit, fn _ -> "x" end) |> Enum.join() + long_value = Enum.map(0..value_limit, fn _ -> "x" end) |> Enum.join() + + fields = [%{"name" => "foo", "value" => long_value}] + + assert %{"error" => "Invalid request"} == + conn + |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) + |> json_response_and_validate_schema(403) + + fields = [%{"name" => long_name, "value" => "bar"}] + + assert %{"error" => "Invalid request"} == + conn + |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) + |> json_response_and_validate_schema(403) + + Pleroma.Config.put([:instance, :max_account_fields], 1) + + fields = [ + %{"name" => "foo", "value" => "bar"}, + %{"name" => "link", "value" => "cofe.io"} + ] + + assert %{"error" => "Invalid request"} == + conn + |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) + |> json_response_and_validate_schema(403) + end + end + + describe "Mark account as bot" do + setup do: oauth_access(["write:accounts"]) + setup :request_content_type + + test "changing actor_type to Service makes account a bot", %{conn: conn} do + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{actor_type: "Service"}) + |> json_response_and_validate_schema(200) + + assert account["bot"] + assert account["source"]["pleroma"]["actor_type"] == "Service" + end + + test "changing actor_type to Person makes account a human", %{conn: conn} do + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{actor_type: "Person"}) + |> json_response_and_validate_schema(200) + + refute account["bot"] + assert account["source"]["pleroma"]["actor_type"] == "Person" + end + + test "changing actor_type to Application causes error", %{conn: conn} do + response = + conn + |> patch("/api/v1/accounts/update_credentials", %{actor_type: "Application"}) + |> json_response_and_validate_schema(403) + + assert %{"error" => "Invalid request"} == response + end + + test "changing bot field to true changes actor_type to Service", %{conn: conn} do + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{bot: "true"}) + |> json_response_and_validate_schema(200) + + assert account["bot"] + assert account["source"]["pleroma"]["actor_type"] == "Service" + end + + test "changing bot field to false changes actor_type to Person", %{conn: conn} do + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{bot: "false"}) + |> json_response_and_validate_schema(200) + + refute account["bot"] + assert account["source"]["pleroma"]["actor_type"] == "Person" + end + + test "actor_type field has a higher priority than bot", %{conn: conn} do + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{ + actor_type: "Person", + bot: "true" + }) + |> json_response_and_validate_schema(200) + + refute account["bot"] + assert account["source"]["pleroma"]["actor_type"] == "Person" + 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 @@ -0,0 +1,575 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.AccountViewTest do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.User + alias Pleroma.UserRelationship + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.AccountView + + import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "Represent a user account" do + background_image = %{ + "url" => [%{"href" => "https://example.com/images/asuka_hospital.png"}] + } + + user = + insert(:user, %{ + follower_count: 3, + note_count: 5, + background: background_image, + nickname: "shp@shitposter.club", + name: ":karjalanpiirakka: shp", + bio: + "<script src=\"invalid-html\"></script><span>valid html</span>. a<br>b<br/>c<br >d<br />f '&<>\"", + inserted_at: ~N[2017-08-15 15:47:06.597036], + emoji: %{"karjalanpiirakka" => "/file.png"}, + raw_bio: "valid html. a\nb\nc\nd\nf '&<>\"" + }) + + expected = %{ + id: to_string(user.id), + username: "shp", + acct: user.nickname, + display_name: user.name, + locked: false, + created_at: "2017-08-15T15:47:06.000Z", + followers_count: 3, + following_count: 0, + statuses_count: 5, + note: "<span>valid html</span>. a<br/>b<br/>c<br/>d<br/>f &#39;&amp;&lt;&gt;&quot;", + url: user.ap_id, + avatar: "http://localhost:4001/images/avi.png", + avatar_static: "http://localhost:4001/images/avi.png", + header: "http://localhost:4001/images/banner.png", + header_static: "http://localhost:4001/images/banner.png", + emojis: [ + %{ + static_url: "/file.png", + url: "/file.png", + shortcode: "karjalanpiirakka", + visible_in_picker: false + } + ], + fields: [], + bot: false, + source: %{ + note: "valid html. a\nb\nc\nd\nf '&<>\"", + sensitive: false, + pleroma: %{ + actor_type: "Person", + discoverable: true + }, + fields: [] + }, + pleroma: %{ + ap_id: user.ap_id, + background_image: "https://example.com/images/asuka_hospital.png", + favicon: nil, + confirmation_pending: false, + tags: [], + is_admin: false, + is_moderator: false, + hide_favorites: true, + hide_followers: false, + hide_follows: false, + hide_followers_count: false, + hide_follows_count: false, + relationship: %{}, + skip_thread_containment: false, + accepts_chat_messages: nil + } + } + + assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + end + + describe "favicon" do + setup do + [user: insert(:user)] + end + + test "is parsed when :instance_favicons is enabled", %{user: user} do + clear_config([:instances_favicons, :enabled], true) + + assert %{ + pleroma: %{ + favicon: + "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png" + } + } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + end + + test "is nil when :instances_favicons is disabled", %{user: user} do + assert %{pleroma: %{favicon: nil}} = + AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + end + end + + test "Represent the user account for the account owner" do + user = insert(:user) + + notification_settings = %{ + block_from_strangers: false, + hide_notification_contents: false + } + + privacy = user.default_scope + + assert %{ + pleroma: %{notification_settings: ^notification_settings, allow_following_move: true}, + source: %{privacy: ^privacy} + } = AccountView.render("show.json", %{user: user, for: user}) + end + + test "Represent a Service(bot) account" do + user = + insert(:user, %{ + follower_count: 3, + note_count: 5, + actor_type: "Service", + nickname: "shp@shitposter.club", + inserted_at: ~N[2017-08-15 15:47:06.597036] + }) + + expected = %{ + id: to_string(user.id), + username: "shp", + acct: user.nickname, + display_name: user.name, + locked: false, + created_at: "2017-08-15T15:47:06.000Z", + followers_count: 3, + following_count: 0, + statuses_count: 5, + note: user.bio, + url: user.ap_id, + avatar: "http://localhost:4001/images/avi.png", + avatar_static: "http://localhost:4001/images/avi.png", + header: "http://localhost:4001/images/banner.png", + header_static: "http://localhost:4001/images/banner.png", + emojis: [], + fields: [], + bot: true, + source: %{ + note: user.bio, + sensitive: false, + pleroma: %{ + actor_type: "Service", + discoverable: true + }, + fields: [] + }, + pleroma: %{ + ap_id: user.ap_id, + background_image: nil, + favicon: nil, + confirmation_pending: false, + tags: [], + is_admin: false, + is_moderator: false, + hide_favorites: true, + hide_followers: false, + hide_follows: false, + hide_followers_count: false, + hide_follows_count: false, + relationship: %{}, + skip_thread_containment: false, + accepts_chat_messages: nil + } + } + + assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + end + + test "Represent a Funkwhale channel" do + {:ok, user} = + User.get_or_fetch_by_ap_id( + "https://channels.tests.funkwhale.audio/federation/actors/compositions" + ) + + assert represented = + AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + + assert represented.acct == "compositions@channels.tests.funkwhale.audio" + 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) + deactivated_user = insert(:user, deactivated: true) + represented = AccountView.render("show.json", %{user: deactivated_user, for: admin}) + assert represented[:pleroma][:deactivated] == true + end + + test "Represent a smaller mention" do + user = insert(:user) + + expected = %{ + id: to_string(user.id), + acct: user.nickname, + username: user.nickname, + url: user.ap_id + } + + assert expected == AccountView.render("mention.json", %{user: user}) + end + + test "demands :for or :skip_visibility_check option for account rendering" do + clear_config([:restrict_unauthenticated, :profiles, :local], false) + + user = insert(:user) + user_id = user.id + + assert %{id: ^user_id} = AccountView.render("show.json", %{user: user, for: nil}) + assert %{id: ^user_id} = AccountView.render("show.json", %{user: user, for: user}) + + assert %{id: ^user_id} = + AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + + assert_raise RuntimeError, ~r/:skip_visibility_check or :for option is required/, fn -> + AccountView.render("show.json", %{user: user}) + end + end + + describe "relationship" do + defp test_relationship_rendering(user, other_user, expected_result) do + opts = %{user: user, target: other_user, relationships: nil} + assert expected_result == AccountView.render("relationship.json", opts) + + relationships_opt = UserRelationship.view_relationships_option(user, [other_user]) + opts = Map.put(opts, :relationships, relationships_opt) + assert expected_result == AccountView.render("relationship.json", opts) + + assert [expected_result] == + AccountView.render("relationships.json", %{user: user, targets: [other_user]}) + end + + @blank_response %{ + following: false, + followed_by: false, + blocking: false, + blocked_by: false, + muting: false, + muting_notifications: false, + subscribing: false, + requested: false, + domain_blocking: false, + showing_reblogs: true, + endorsed: false + } + + test "represent a relationship for the following and followed user" do + user = insert(:user) + other_user = insert(:user) + + {:ok, user} = User.follow(user, other_user) + {:ok, other_user} = User.follow(other_user, user) + {:ok, _subscription} = User.subscribe(user, other_user) + {:ok, _user_relationships} = User.mute(user, other_user, true) + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user) + + expected = + Map.merge( + @blank_response, + %{ + following: true, + followed_by: true, + muting: true, + muting_notifications: true, + subscribing: true, + showing_reblogs: false, + id: to_string(other_user.id) + } + ) + + test_relationship_rendering(user, other_user, expected) + end + + test "represent a relationship for the blocking and blocked user" do + user = insert(:user) + other_user = insert(:user) + + {:ok, user} = User.follow(user, other_user) + {:ok, _subscription} = User.subscribe(user, other_user) + {:ok, _user_relationship} = User.block(user, other_user) + {:ok, _user_relationship} = User.block(other_user, user) + + expected = + Map.merge( + @blank_response, + %{following: false, blocking: true, blocked_by: true, id: to_string(other_user.id)} + ) + + test_relationship_rendering(user, other_user, expected) + end + + test "represent a relationship for the user blocking a domain" do + user = insert(:user) + other_user = insert(:user, ap_id: "https://bad.site/users/other_user") + + {:ok, user} = User.block_domain(user, "bad.site") + + expected = + Map.merge( + @blank_response, + %{domain_blocking: true, blocking: false, id: to_string(other_user.id)} + ) + + test_relationship_rendering(user, other_user, expected) + end + + test "represent a relationship for the user with a pending follow request" do + user = insert(:user) + other_user = insert(:user, locked: true) + + {:ok, user, other_user, _} = CommonAPI.follow(user, other_user) + user = User.get_cached_by_id(user.id) + other_user = User.get_cached_by_id(other_user.id) + + expected = + Map.merge( + @blank_response, + %{requested: true, following: false, id: to_string(other_user.id)} + ) + + test_relationship_rendering(user, other_user, expected) + end + end + + test "returns the settings store if the requesting user is the represented user and it's requested specifically" do + user = insert(:user, pleroma_settings_store: %{fe: "test"}) + + result = + AccountView.render("show.json", %{user: user, for: user, with_pleroma_settings: true}) + + assert result.pleroma.settings_store == %{:fe => "test"} + + result = AccountView.render("show.json", %{user: user, for: nil, with_pleroma_settings: true}) + assert result.pleroma[:settings_store] == nil + + result = AccountView.render("show.json", %{user: user, for: user}) + assert result.pleroma[:settings_store] == nil + end + + test "doesn't sanitize display names" do + user = insert(:user, name: "<marquee> username </marquee>") + result = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + assert result.display_name == "<marquee> username </marquee>" + end + + test "never display nil user follow counts" do + user = insert(:user, following_count: 0, follower_count: 0) + result = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + + assert result.following_count == 0 + assert result.followers_count == 0 + end + + describe "hiding follows/following" do + test "shows when follows/followers stats are hidden and sets follow/follower count to 0" do + user = + insert(:user, %{ + hide_followers: true, + hide_followers_count: true, + hide_follows: true, + hide_follows_count: true + }) + + other_user = insert(:user) + {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{ + followers_count: 0, + following_count: 0, + pleroma: %{hide_follows_count: true, hide_followers_count: true} + } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + end + + test "shows when follows/followers are hidden" do + user = insert(:user, hide_followers: true, hide_follows: true) + other_user = insert(:user) + {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{ + followers_count: 1, + following_count: 1, + pleroma: %{hide_follows: true, hide_followers: true} + } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + end + + test "shows actual follower/following count to the account owner" do + user = insert(:user, hide_followers: true, hide_follows: true) + other_user = insert(:user) + {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) + + assert User.following?(user, other_user) + assert Pleroma.FollowingRelationship.follower_count(other_user) == 1 + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{ + followers_count: 1, + following_count: 1 + } = AccountView.render("show.json", %{user: user, for: user}) + end + + test "shows unread_conversation_count only to the account owner" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(other_user, %{ + status: "Hey @#{user.nickname}.", + visibility: "direct" + }) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert AccountView.render("show.json", %{user: user, for: other_user})[:pleroma][ + :unread_conversation_count + ] == nil + + assert AccountView.render("show.json", %{user: user, for: user})[:pleroma][ + :unread_conversation_count + ] == 1 + end + + test "shows unread_count only to the account owner" do + user = insert(:user) + insert_list(7, :notification, user: user, activity: insert(:note_activity)) + other_user = insert(:user) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert AccountView.render( + "show.json", + %{user: user, for: other_user} + )[:pleroma][:unread_notifications_count] == nil + + assert AccountView.render( + "show.json", + %{user: user, for: user} + )[:pleroma][:unread_notifications_count] == 7 + end + end + + describe "follow requests counter" do + test "shows zero when no follow requests are pending" do + user = insert(:user) + + assert %{follow_requests_count: 0} = + AccountView.render("show.json", %{user: user, for: user}) + + other_user = insert(:user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{follow_requests_count: 0} = + AccountView.render("show.json", %{user: user, for: user}) + end + + test "shows non-zero when follow requests are pending" do + user = insert(:user, locked: true) + + assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) + + other_user = insert(:user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{locked: true, follow_requests_count: 1} = + AccountView.render("show.json", %{user: user, for: user}) + end + + test "decreases when accepting a follow request" do + user = insert(:user, locked: true) + + assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) + + other_user = insert(:user) + {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{locked: true, follow_requests_count: 1} = + AccountView.render("show.json", %{user: user, for: user}) + + {:ok, _other_user} = CommonAPI.accept_follow_request(other_user, user) + + assert %{locked: true, follow_requests_count: 0} = + AccountView.render("show.json", %{user: user, for: user}) + end + + test "decreases when rejecting a follow request" do + user = insert(:user, locked: true) + + assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) + + other_user = insert(:user) + {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{locked: true, follow_requests_count: 1} = + AccountView.render("show.json", %{user: user, for: user}) + + {:ok, _other_user} = CommonAPI.reject_follow_request(other_user, user) + + assert %{locked: true, follow_requests_count: 0} = + AccountView.render("show.json", %{user: user, for: user}) + end + + test "shows non-zero when historical unapproved requests are present" do + user = insert(:user, locked: true) + + assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) + + other_user = insert(:user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + {:ok, user} = User.update_and_set_cache(user, %{locked: false}) + + assert %{locked: false, follow_requests_count: 1} = + AccountView.render("show.json", %{user: user, for: user}) + end + end + + test "uses mediaproxy urls when it's enabled (regardless of media preview proxy state)" do + clear_config([:media_proxy, :enabled], true) + clear_config([:media_preview_proxy, :enabled]) + + user = + insert(:user, + avatar: %{"url" => [%{"href" => "https://evil.website/avatar.png"}]}, + banner: %{"url" => [%{"href" => "https://evil.website/banner.png"}]}, + emoji: %{"joker_smile" => "https://evil.website/society.png"} + ) + + with media_preview_enabled <- [false, true] do + Config.put([:media_preview_proxy, :enabled], media_preview_enabled) + + AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + |> Enum.all?(fn + {key, url} when key in [:avatar, :avatar_static, :header, :header_static] -> + String.starts_with?(url, Pleroma.Web.base_url()) + + {:emojis, emojis} -> + Enum.all?(emojis, fn %{url: url, static_url: static_url} -> + String.starts_with?(url, Pleroma.Web.base_url()) && + String.starts_with?(static_url, Pleroma.Web.base_url()) + end) + + _ -> + true + end) + |> assert() + end + end +end diff --git a/test/pleroma/web/mastodon_api/views/conversation_view_test.exs b/test/pleroma/web/mastodon_api/views/conversation_view_test.exs @@ -0,0 +1,44 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ConversationViewTest do + use Pleroma.DataCase + + alias Pleroma.Conversation.Participation + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.ConversationView + + import Pleroma.Factory + + test "represents a Mastodon Conversation entity" do + user = insert(:user) + other_user = insert(:user) + + {:ok, parent} = CommonAPI.post(user, %{status: "parent"}) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname}", + visibility: "direct", + in_reply_to_id: parent.id + }) + + {:ok, _reply_activity} = + CommonAPI.post(user, %{status: "hu", visibility: "public", in_reply_to_id: parent.id}) + + [participation] = Participation.for_user_with_last_activity_id(user) + + assert participation + + conversation = + ConversationView.render("participation.json", %{participation: participation, for: user}) + + assert conversation.id == participation.id |> to_string() + assert conversation.last_status.id == activity.id + + assert [account] = conversation.accounts + assert account.id == other_user.id + assert conversation.last_status.pleroma.direct_conversation_id == participation.id + end +end diff --git a/test/pleroma/web/mastodon_api/views/list_view_test.exs b/test/pleroma/web/mastodon_api/views/list_view_test.exs @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ListViewTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.MastodonAPI.ListView + + test "show" do + user = insert(:user) + title = "mortal enemies" + {:ok, list} = Pleroma.List.create(title, user) + + expected = %{ + id: to_string(list.id), + title: title + } + + assert expected == ListView.render("show.json", %{list: list}) + end + + test "index" do + user = insert(:user) + + {:ok, list} = Pleroma.List.create("my list", user) + {:ok, list2} = Pleroma.List.create("cofe", user) + + assert [%{id: _, title: "my list"}, %{id: _, title: "cofe"}] = + ListView.render("index.json", lists: [list, list2]) + end +end diff --git a/test/pleroma/web/mastodon_api/views/marker_view_test.exs b/test/pleroma/web/mastodon_api/views/marker_view_test.exs @@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do + use Pleroma.DataCase + alias Pleroma.Web.MastodonAPI.MarkerView + import Pleroma.Factory + + test "returns markers" do + marker1 = insert(:marker, timeline: "notifications", last_read_id: "17", unread_count: 5) + marker2 = insert(:marker, timeline: "home", last_read_id: "42") + + assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{ + "home" => %{ + last_read_id: "42", + updated_at: NaiveDateTime.to_iso8601(marker2.updated_at), + version: 0, + pleroma: %{unread_count: 0} + }, + "notifications" => %{ + last_read_id: "17", + updated_at: NaiveDateTime.to_iso8601(marker1.updated_at), + version: 0, + pleroma: %{unread_count: 5} + } + } + 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 @@ -0,0 +1,231 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Notification + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.NotificationView + alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView + import Pleroma.Factory + + defp test_notifications_rendering(notifications, user, expected_result) do + result = NotificationView.render("index.json", %{notifications: notifications, for: user}) + + assert expected_result == result + + result = + NotificationView.render("index.json", %{ + notifications: notifications, + for: user, + relationships: nil + }) + + assert expected_result == result + end + + test "ChatMessage notification" do + user = insert(:user) + recipient = insert(:user) + {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "what's up my dude") + + {:ok, [notification]} = Notification.create_notifications(activity) + + object = Object.normalize(activity) + chat = Chat.get(recipient.id, user.ap_id) + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false, is_muted: false}, + type: "pleroma:chat_mention", + account: AccountView.render("show.json", %{user: user, for: recipient}), + chat_message: MessageReferenceView.render("show.json", %{chat_message_reference: cm_ref}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], recipient, [expected]) + end + + test "Mention notification" do + user = insert(:user) + mentioned_user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{mentioned_user.nickname}"}) + {: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: "mention", + account: + AccountView.render("show.json", %{ + user: user, + for: mentioned_user + }), + status: StatusView.render("show.json", %{activity: activity, for: mentioned_user}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], mentioned_user, [expected]) + end + + test "Favourite notification" do + user = insert(:user) + another_user = insert(:user) + {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, favorite_activity} = CommonAPI.favorite(another_user, create_activity.id) + {:ok, [notification]} = Notification.create_notifications(favorite_activity) + create_activity = Activity.get_by_id(create_activity.id) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false, is_muted: false}, + type: "favourite", + account: AccountView.render("show.json", %{user: another_user, for: user}), + status: StatusView.render("show.json", %{activity: create_activity, for: user}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], user, [expected]) + end + + test "Reblog notification" do + user = insert(:user) + another_user = insert(:user) + {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, another_user) + {:ok, [notification]} = Notification.create_notifications(reblog_activity) + reblog_activity = Activity.get_by_id(create_activity.id) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false, is_muted: false}, + type: "reblog", + account: AccountView.render("show.json", %{user: another_user, for: user}), + status: StatusView.render("show.json", %{activity: reblog_activity, for: user}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], user, [expected]) + end + + test "Follow notification" do + follower = insert(:user) + followed = insert(:user) + {:ok, follower, followed, _activity} = CommonAPI.follow(follower, followed) + notification = Notification |> Repo.one() |> Repo.preload(:activity) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false, is_muted: false}, + type: "follow", + account: AccountView.render("show.json", %{user: follower, for: followed}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], followed, [expected]) + + User.perform(:delete, follower) + refute Repo.one(Notification) + end + + @tag capture_log: true + test "Move notification" do + old_user = insert(:user) + new_user = insert(:user, also_known_as: [old_user.ap_id]) + follower = insert(:user) + + old_user_url = old_user.ap_id + + body = + File.read!("test/fixtures/users_mock/localhost.json") + |> String.replace("{{nickname}}", old_user.nickname) + |> Jason.encode!() + + Tesla.Mock.mock(fn + %{method: :get, url: ^old_user_url} -> + %Tesla.Env{status: 200, body: body} + end) + + User.follow(follower, old_user) + Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) + Pleroma.Tests.ObanHelpers.perform_all() + + old_user = refresh_record(old_user) + new_user = refresh_record(new_user) + + [notification] = Notification.for_user(follower) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false, is_muted: false}, + type: "move", + account: AccountView.render("show.json", %{user: old_user, for: follower}), + target: AccountView.render("show.json", %{user: new_user, for: follower}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], follower, [expected]) + end + + test "EmojiReact notification" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + {:ok, _activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + + 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: "☕", + 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) + } + + test_notifications_rendering([notification], user, [expected]) + end + + test "muted notification" do + user = insert(:user) + another_user = insert(:user) + + {:ok, _} = Pleroma.UserRelationship.create_mute(user, another_user) + {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, favorite_activity} = CommonAPI.favorite(another_user, create_activity.id) + {:ok, [notification]} = Notification.create_notifications(favorite_activity) + create_activity = Activity.get_by_id(create_activity.id) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: true, is_muted: true}, + type: "favourite", + account: AccountView.render("show.json", %{user: another_user, for: user}), + status: StatusView.render("show.json", %{activity: create_activity, for: user}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], user, [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 @@ -0,0 +1,167 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.PollViewTest do + use Pleroma.DataCase + + alias Pleroma.Object + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.PollView + + import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "renders a poll" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Is Tenshi eating a corndog cute?", + poll: %{ + options: ["absolutely!", "sure", "yes", "why are you even asking?"], + expires_in: 20 + } + }) + + object = Object.normalize(activity) + + expected = %{ + emojis: [], + expired: false, + id: to_string(object.id), + multiple: false, + options: [ + %{title: "absolutely!", votes_count: 0}, + %{title: "sure", votes_count: 0}, + %{title: "yes", votes_count: 0}, + %{title: "why are you even asking?", votes_count: 0} + ], + voted: false, + votes_count: 0, + voters_count: nil + } + + result = PollView.render("show.json", %{object: object}) + expires_at = result.expires_at + result = Map.delete(result, :expires_at) + + assert result == expected + + expires_at = NaiveDateTime.from_iso8601!(expires_at) + assert NaiveDateTime.diff(expires_at, NaiveDateTime.utc_now()) in 15..20 + end + + test "detects if it is multiple choice" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which Mastodon developer is your favourite?", + poll: %{ + options: ["Gargron", "Eugen"], + expires_in: 20, + multiple: true + } + }) + + voter = insert(:user) + + object = Object.normalize(activity) + + {:ok, _votes, object} = CommonAPI.vote(voter, object, [0, 1]) + + assert match?( + %{ + multiple: true, + voters_count: 1, + votes_count: 2 + }, + PollView.render("show.json", %{object: object}) + ) + end + + test "detects emoji" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "What's with the smug face?", + poll: %{ + options: [":blank: sip", ":blank::blank: sip", ":blank::blank::blank: sip"], + expires_in: 20 + } + }) + + object = Object.normalize(activity) + + assert %{emojis: [%{shortcode: "blank"}]} = PollView.render("show.json", %{object: object}) + end + + test "detects vote status" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which input devices do you use?", + poll: %{ + options: ["mouse", "trackball", "trackpoint"], + multiple: true, + expires_in: 20 + } + }) + + object = Object.normalize(activity) + + {:ok, _, object} = CommonAPI.vote(other_user, object, [1, 2]) + + result = PollView.render("show.json", %{object: object, for: other_user}) + + assert result[:voted] == true + assert Enum.at(result[:options], 1)[:votes_count] == 1 + assert Enum.at(result[:options], 2)[:votes_count] == 1 + end + + test "does not crash on polls with no end date" do + object = Object.normalize("https://skippers-bin.com/notes/7x9tmrp97i") + result = PollView.render("show.json", %{object: object}) + + assert result[:expires_at] == nil + assert result[:expired] == false + end + + test "doesn't strips HTML tags" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "What's with the smug face?", + poll: %{ + options: [ + "<input type=\"date\">", + "<input type=\"date\" >", + "<input type=\"date\"/>", + "<input type=\"date\"></input>" + ], + expires_in: 20 + } + }) + + object = Object.normalize(activity) + + assert %{ + options: [ + %{title: "<input type=\"date\">", votes_count: 0}, + %{title: "<input type=\"date\" >", votes_count: 0}, + %{title: "<input type=\"date\"/>", votes_count: 0}, + %{title: "<input type=\"date\"></input>", votes_count: 0} + ] + } = PollView.render("show.json", %{object: object}) + 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 @@ -0,0 +1,68 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ScheduledActivityViewTest do + use Pleroma.DataCase + alias Pleroma.ScheduledActivity + 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 Pleroma.Factory + + test "A scheduled activity with a media attachment" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "hi"}) + + scheduled_at = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(10), :millisecond) + |> NaiveDateTime.to_iso8601() + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + + attrs = %{ + params: %{ + "media_ids" => [upload.id], + "status" => "hi", + "sensitive" => true, + "spoiler_text" => "spoiler", + "visibility" => "unlisted", + "in_reply_to_id" => to_string(activity.id) + }, + scheduled_at: scheduled_at + } + + {:ok, scheduled_activity} = ScheduledActivity.create(user, attrs) + result = ScheduledActivityView.render("show.json", %{scheduled_activity: scheduled_activity}) + + expected = %{ + id: to_string(scheduled_activity.id), + media_attachments: + %{media_ids: [upload.id]} + |> Utils.attachments_from_ids() + |> Enum.map(&StatusView.render("attachment.json", %{attachment: &1})), + params: %{ + in_reply_to_id: to_string(activity.id), + media_ids: [upload.id], + poll: nil, + scheduled_at: nil, + sensitive: true, + spoiler_text: "spoiler", + text: "hi", + visibility: "unlisted" + }, + scheduled_at: Utils.to_masto_date(scheduled_activity.scheduled_at) + } + + assert expected == result + end +end diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -0,0 +1,664 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.StatusViewTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Bookmark + alias Pleroma.Conversation.Participation + alias Pleroma.HTML + alias Pleroma.Object + alias Pleroma.Repo + 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 + + import Pleroma.Factory + import Tesla.Mock + import OpenApiSpex.TestAssertions + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "has an emoji reaction list" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "dae cofe??"}) + + {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "☕") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + 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} + ] + + status = StatusView.render("show.json", activity: activity, for: user) + + assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) + + assert status[:pleroma][:emoji_reactions] == [ + %{name: "☕", count: 2, me: true}, + %{name: "🍵", count: 1, me: false} + ] + end + + test "works correctly with badly formatted emojis" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "yo"}) + + activity + |> Object.normalize(false) + |> 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} + ] + end + + test "loads and returns the direct conversation id when given the `with_direct_conversation_id` option" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"}) + [participation] = Participation.for_user(user) + + status = + StatusView.render("show.json", + activity: activity, + with_direct_conversation_id: true, + for: user + ) + + assert status[:pleroma][:direct_conversation_id] == participation.id + + status = StatusView.render("show.json", activity: activity, for: user) + assert status[:pleroma][:direct_conversation_id] == nil + assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "returns the direct conversation id when given the `direct_conversation_id` option" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"}) + [participation] = Participation.for_user(user) + + status = + StatusView.render("show.json", + activity: activity, + direct_conversation_id: participation.id, + for: user + ) + + assert status[:pleroma][:direct_conversation_id] == participation.id + assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "returns a temporary ap_id based user for activities missing db users" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"}) + + Repo.delete(user) + Cachex.clear(:user_cache) + + finger_url = + "https://localhost/.well-known/webfinger?resource=acct:#{user.nickname}@localhost" + + Tesla.Mock.mock_global(fn + %{method: :get, url: "http://localhost/.well-known/host-meta"} -> + %Tesla.Env{status: 404, body: ""} + + %{method: :get, url: "https://localhost/.well-known/host-meta"} -> + %Tesla.Env{status: 404, body: ""} + + %{ + method: :get, + url: ^finger_url + } -> + %Tesla.Env{status: 404, body: ""} + end) + + %{account: ms_user} = StatusView.render("show.json", activity: activity) + + assert ms_user.acct == "erroruser@example.com" + end + + test "tries to get a user by nickname if fetching by ap_id doesn't work" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"}) + + {:ok, user} = + user + |> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"}) + |> Repo.update() + + Cachex.clear(:user_cache) + + result = StatusView.render("show.json", activity: activity) + + assert result[:account][:id] == to_string(user.id) + assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "a note with null content" do + note = insert(:note_activity) + note_object = Object.normalize(note) + + data = + note_object.data + |> Map.put("content", nil) + + Object.change(note_object, %{data: data}) + |> Object.update_and_set_cache() + + User.get_cached_by_ap_id(note.data["actor"]) + + status = StatusView.render("show.json", %{activity: note}) + + assert status.content == "" + assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "a note activity" do + note = insert(:note_activity) + object_data = Object.normalize(note).data + user = User.get_cached_by_ap_id(note.data["actor"]) + + convo_id = Utils.context_to_conversation_id(object_data["context"]) + + status = StatusView.render("show.json", %{activity: note}) + + created_at = + (object_data["published"] || "") + |> String.replace(~r/\.\d+Z/, ".000Z") + + expected = %{ + id: to_string(note.id), + uri: object_data["id"], + url: Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, note), + account: AccountView.render("show.json", %{user: user, skip_visibility_check: true}), + in_reply_to_id: nil, + in_reply_to_account_id: nil, + card: nil, + reblog: nil, + content: HTML.filter_tags(object_data["content"]), + text: nil, + created_at: created_at, + reblogs_count: 0, + replies_count: 0, + favourites_count: 0, + reblogged: false, + bookmarked: false, + favourited: false, + muted: false, + pinned: false, + sensitive: false, + poll: nil, + spoiler_text: HTML.filter_tags(object_data["summary"]), + visibility: "public", + media_attachments: [], + mentions: [], + tags: [ + %{ + name: "#{object_data["tag"]}", + url: "/tag/#{object_data["tag"]}" + } + ], + application: %{ + name: "Web", + website: nil + }, + language: nil, + emojis: [ + %{ + shortcode: "2hu", + url: "corndog.png", + static_url: "corndog.png", + visible_in_picker: false + } + ], + pleroma: %{ + local: true, + conversation_id: convo_id, + in_reply_to_account_acct: nil, + content: %{"text/plain" => HTML.strip_tags(object_data["content"])}, + spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])}, + expires_at: nil, + direct_conversation_id: nil, + thread_muted: false, + emoji_reactions: [], + parent_visible: false + } + } + + assert status == expected + assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "tells if the message is muted for some reason" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _user_relationships} = User.mute(user, other_user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "test"}) + + relationships_opt = UserRelationship.view_relationships_option(user, [other_user]) + + opts = %{activity: activity} + status = StatusView.render("show.json", opts) + assert status.muted == false + assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) + + status = StatusView.render("show.json", Map.put(opts, :relationships, relationships_opt)) + assert status.muted == false + + for_opts = %{activity: activity, for: user} + status = StatusView.render("show.json", for_opts) + assert status.muted == true + + status = StatusView.render("show.json", Map.put(for_opts, :relationships, relationships_opt)) + assert status.muted == true + assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "tells if the message is thread muted" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _user_relationships} = User.mute(user, other_user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "test"}) + status = StatusView.render("show.json", %{activity: activity, for: user}) + + assert status.pleroma.thread_muted == false + + {:ok, activity} = CommonAPI.add_mute(user, activity) + + status = StatusView.render("show.json", %{activity: activity, for: user}) + + assert status.pleroma.thread_muted == true + end + + test "tells if the status is bookmarked" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "Cute girls doing cute things"}) + status = StatusView.render("show.json", %{activity: activity}) + + assert status.bookmarked == false + + status = StatusView.render("show.json", %{activity: activity, for: user}) + + assert status.bookmarked == false + + {:ok, _bookmark} = Bookmark.create(user.id, activity.id) + + activity = Activity.get_by_id_with_object(activity.id) + + status = StatusView.render("show.json", %{activity: activity, for: user}) + + assert status.bookmarked == true + end + + test "a reply" do + note = insert(:note_activity) + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "he", in_reply_to_status_id: note.id}) + + status = StatusView.render("show.json", %{activity: activity}) + + assert status.in_reply_to_id == to_string(note.id) + + [status] = StatusView.render("index.json", %{activities: [activity], as: :activity}) + + assert status.in_reply_to_id == to_string(note.id) + end + + test "contains mentions" do + user = insert(:user) + mentioned = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hi @#{mentioned.nickname}"}) + + status = StatusView.render("show.json", %{activity: activity}) + + assert status.mentions == + Enum.map([mentioned], fn u -> AccountView.render("mention.json", %{user: u}) end) + + assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "create mentions from the 'to' field" do + %User{ap_id: recipient_ap_id} = insert(:user) + cc = insert_pair(:user) |> Enum.map(& &1.ap_id) + + object = + insert(:note, %{ + data: %{ + "to" => [recipient_ap_id], + "cc" => cc + } + }) + + activity = + insert(:note_activity, %{ + note: object, + recipients: [recipient_ap_id | cc] + }) + + assert length(activity.recipients) == 3 + + %{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity}) + + assert length(mentions) == 1 + assert mention.url == recipient_ap_id + end + + test "create mentions from the 'tag' field" do + recipient = insert(:user) + cc = insert_pair(:user) |> Enum.map(& &1.ap_id) + + object = + insert(:note, %{ + data: %{ + "cc" => cc, + "tag" => [ + %{ + "href" => recipient.ap_id, + "name" => recipient.nickname, + "type" => "Mention" + }, + %{ + "href" => "https://example.com/search?tag=test", + "name" => "#test", + "type" => "Hashtag" + } + ] + } + }) + + activity = + insert(:note_activity, %{ + note: object, + recipients: [recipient.ap_id | cc] + }) + + assert length(activity.recipients) == 3 + + %{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity}) + + assert length(mentions) == 1 + assert mention.url == recipient.ap_id + end + + test "attachments" do + object = %{ + "type" => "Image", + "url" => [ + %{ + "mediaType" => "image/png", + "href" => "someurl" + } + ], + "uuid" => 6 + } + + expected = %{ + id: "1638338801", + type: "image", + url: "someurl", + remote_url: "someurl", + preview_url: "someurl", + text_url: "someurl", + description: nil, + pleroma: %{mime_type: "image/png"} + } + + api_spec = Pleroma.Web.ApiSpec.spec() + + assert expected == StatusView.render("attachment.json", %{attachment: object}) + assert_schema(expected, "Attachment", api_spec) + + # If theres a "id", use that instead of the generated one + object = Map.put(object, "id", 2) + result = StatusView.render("attachment.json", %{attachment: object}) + + assert %{id: "2"} = result + assert_schema(result, "Attachment", api_spec) + end + + test "put the url advertised in the Activity in to the url attribute" do + id = "https://wedistribute.org/wp-json/pterotype/v1/object/85810" + [activity] = Activity.search(nil, id) + + status = StatusView.render("show.json", %{activity: activity}) + + assert status.uri == id + assert status.url == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/" + end + + test "a reblog" do + user = insert(:user) + activity = insert(:note_activity) + + {:ok, reblog} = CommonAPI.repeat(activity.id, user) + + represented = StatusView.render("show.json", %{for: user, activity: reblog}) + + assert represented[:id] == to_string(reblog.id) + assert represented[:reblog][:id] == to_string(activity.id) + assert represented[:emojis] == [] + assert_schema(represented, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "a peertube video" do + user = insert(:user) + + {:ok, object} = + Pleroma.Object.Fetcher.fetch_object_from_id( + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" + ) + + %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) + + represented = StatusView.render("show.json", %{for: user, activity: activity}) + + assert represented[:id] == to_string(activity.id) + assert length(represented[:media_attachments]) == 1 + assert_schema(represented, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "funkwhale audio" do + user = insert(:user) + + {:ok, object} = + Pleroma.Object.Fetcher.fetch_object_from_id( + "https://channels.tests.funkwhale.audio/federation/music/uploads/42342395-0208-4fee-a38d-259a6dae0871" + ) + + %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) + + represented = StatusView.render("show.json", %{for: user, activity: activity}) + + assert represented[:id] == to_string(activity.id) + assert length(represented[:media_attachments]) == 1 + end + + test "a Mobilizon event" do + user = insert(:user) + + {:ok, object} = + Pleroma.Object.Fetcher.fetch_object_from_id( + "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" + ) + + %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) + + represented = StatusView.render("show.json", %{for: user, activity: activity}) + + assert represented[:id] == to_string(activity.id) + + assert represented[:url] == + "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" + + assert represented[:content] == + "<p><a href=\"https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39\">Mobilizon Launching Party</a></p><p>Mobilizon is now federated! 🎉</p><p></p><p>You can view this event from other instances if they are subscribed to mobilizon.org, and soon directly from Mastodon and Pleroma. It is possible that you may see some comments from other instances, including Mastodon ones, just below.</p><p></p><p>With a Mobilizon account on an instance, you may <strong>participate</strong> at events from other instances and <strong>add comments</strong> on events.</p><p></p><p>Of course, it&#39;s still <u>a work in progress</u>: if reports made from an instance on events and comments can be federated, you can&#39;t block people right now, and moderators actions are rather limited, but this <strong>will definitely get fixed over time</strong> until first stable version next year.</p><p></p><p>Anyway, if you want to come up with some feedback, head over to our forum or - if you feel you have technical skills and are familiar with it - on our Gitlab repository.</p><p></p><p>Also, to people that want to set Mobilizon themselves even though we really don&#39;t advise to do that for now, we have a little documentation but it&#39;s quite the early days and you&#39;ll probably need some help. No worries, you can chat with us on our Forum or though our Matrix channel.</p><p></p><p>Check our website for more informations and follow us on Twitter or Mastodon.</p>" + end + + describe "build_tags/1" do + test "it returns a a dictionary tags" do + object_tags = [ + "fediverse", + "mastodon", + "nextcloud", + %{ + "href" => "https://kawen.space/users/lain", + "name" => "@lain@kawen.space", + "type" => "Mention" + } + ] + + assert StatusView.build_tags(object_tags) == [ + %{name: "fediverse", url: "/tag/fediverse"}, + %{name: "mastodon", url: "/tag/mastodon"}, + %{name: "nextcloud", url: "/tag/nextcloud"} + ] + end + end + + describe "rich media cards" do + test "a rich media card without a site name renders correctly" do + page_url = "http://example.com" + + card = %{ + url: 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}) + end + + test "a rich media card without a site name or image renders correctly" do + page_url = "http://example.com" + + card = %{ + url: page_url, + title: "Example website" + } + + %{provider_name: "example.com"} = + StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + end + + test "a rich media card without an image renders correctly" do + page_url = "http://example.com" + + card = %{ + 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}) + 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" + } + + %{provider_name: "example.com"} = + StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + end + end + + test "does not embed a relationship in the account" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "drink more water" + }) + + result = StatusView.render("show.json", %{activity: activity, for: other_user}) + + assert result[:account][:pleroma][:relationship] == %{} + assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "does not embed a relationship in the account in reposts" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "˙˙ɐʎns" + }) + + {:ok, activity} = CommonAPI.repeat(activity.id, other_user) + + result = StatusView.render("show.json", %{activity: activity, for: user}) + + assert result[:account][:pleroma][:relationship] == %{} + assert result[:reblog][:account][:pleroma][:relationship] == %{} + assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "visibility/list" do + user = insert(:user) + + {:ok, list} = Pleroma.List.create("foo", user) + + {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) + + status = StatusView.render("show.json", activity: activity) + + assert status.visibility == "list" + end + + test "has a field for parent visibility" do + user = insert(:user) + poster = insert(:user) + + {:ok, invisible} = CommonAPI.post(poster, %{status: "hey", visibility: "private"}) + + {:ok, visible} = + CommonAPI.post(poster, %{status: "hey", visibility: "private", in_reply_to_id: invisible.id}) + + status = StatusView.render("show.json", activity: visible, for: user) + refute status.pleroma.parent_visible + + status = StatusView.render("show.json", activity: visible, for: poster) + assert status.pleroma.parent_visible + end +end diff --git a/test/pleroma/web/mastodon_api/views/subscription_view_test.exs b/test/pleroma/web/mastodon_api/views/subscription_view_test.exs @@ -0,0 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SubscriptionViewTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.MastodonAPI.SubscriptionView, as: View + alias Pleroma.Web.Push + + test "Represent a subscription" do + subscription = insert(:push_subscription, data: %{"alerts" => %{"mention" => true}}) + + expected = %{ + alerts: %{"mention" => true}, + endpoint: subscription.endpoint, + id: to_string(subscription.id), + server_key: Keyword.get(Push.vapid_config(), :public_key) + } + + assert expected == View.render("show.json", %{subscription: subscription}) + end +end diff --git a/test/pleroma/web/media_proxy/invalidation/http_test.exs b/test/pleroma/web/media_proxy/invalidation/http_test.exs @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MediaProxy.Invalidation.HttpTest do + use ExUnit.Case + alias Pleroma.Web.MediaProxy.Invalidation + + import ExUnit.CaptureLog + import Tesla.Mock + + setup do + on_exit(fn -> Cachex.clear(:banned_urls_cache) end) + end + + test "logs hasn't error message when request is valid" do + mock(fn + %{method: :purge, url: "http://example.com/media/example.jpg"} -> + %Tesla.Env{status: 200} + end) + + refute capture_log(fn -> + assert Invalidation.Http.purge( + ["http://example.com/media/example.jpg"], + [] + ) == {:ok, ["http://example.com/media/example.jpg"]} + end) =~ "Error while cache purge" + end + + test "it write error message in logs when request invalid" do + mock(fn + %{method: :purge, url: "http://example.com/media/example1.jpg"} -> + %Tesla.Env{status: 404} + end) + + assert capture_log(fn -> + assert Invalidation.Http.purge( + ["http://example.com/media/example1.jpg"], + [] + ) == {:ok, ["http://example.com/media/example1.jpg"]} + end) =~ "Error while cache purge: url - http://example.com/media/example1.jpg" + end +end diff --git a/test/pleroma/web/media_proxy/invalidation/script_test.exs b/test/pleroma/web/media_proxy/invalidation/script_test.exs @@ -0,0 +1,30 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MediaProxy.Invalidation.ScriptTest do + use ExUnit.Case + alias Pleroma.Web.MediaProxy.Invalidation + + import ExUnit.CaptureLog + + setup do + on_exit(fn -> Cachex.clear(:banned_urls_cache) end) + end + + test "it logger error when script 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}" + + capture_log(fn -> + assert Invalidation.Script.purge( + ["http://example.com/media/example.jpg"], + [] + ) == {:error, "\"not found script path\""} + end) + end +end diff --git a/test/pleroma/web/media_proxy/invalidation_test.exs b/test/pleroma/web/media_proxy/invalidation_test.exs @@ -0,0 +1,68 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MediaProxy.InvalidationTest do + use ExUnit.Case + use Pleroma.Tests.Helpers + + alias Pleroma.Config + alias Pleroma.Web.MediaProxy.Invalidation + + import ExUnit.CaptureLog + import Mock + import Tesla.Mock + + setup do: clear_config([:media_proxy]) + + setup do + on_exit(fn -> Cachex.clear(:banned_urls_cache) end) + end + + describe "Invalidation.Http" do + test "perform request to clear cache" do + Config.put([:media_proxy, :enabled], false) + Config.put([:media_proxy, :invalidation, :enabled], true) + Config.put([:media_proxy, :invalidation, :provider], Invalidation.Http) + + Config.put([Invalidation.Http], method: :purge, headers: [{"x-refresh", 1}]) + image_url = "http://example.com/media/example.jpg" + Pleroma.Web.MediaProxy.put_in_banned_urls(image_url) + + mock(fn + %{ + method: :purge, + url: "http://example.com/media/example.jpg", + headers: [{"x-refresh", 1}] + } -> + %Tesla.Env{status: 200} + end) + + assert capture_log(fn -> + assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) + assert Invalidation.purge([image_url]) == {:ok, [image_url]} + assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) + end) =~ "Running cache purge: [\"#{image_url}\"]" + end + end + + describe "Invalidation.Script" do + test "run script to clear cache" do + Config.put([:media_proxy, :enabled], false) + Config.put([:media_proxy, :invalidation, :enabled], true) + Config.put([:media_proxy, :invalidation, :provider], Invalidation.Script) + Config.put([Invalidation.Script], script_path: "purge-nginx") + + image_url = "http://example.com/media/example.jpg" + Pleroma.Web.MediaProxy.put_in_banned_urls(image_url) + + with_mocks [{System, [], [cmd: fn _, _ -> {"ok", 0} end]}] do + assert capture_log(fn -> + assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) + assert Invalidation.purge([image_url]) == {:ok, [image_url]} + assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) + end) =~ "Running cache purge: [\"#{image_url}\"]" + end + end + end +end diff --git a/test/pleroma/web/media_proxy/media_proxy_controller_test.exs b/test/pleroma/web/media_proxy/media_proxy_controller_test.exs @@ -0,0 +1,342 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do + use Pleroma.Web.ConnCase + + import Mock + + alias Pleroma.Web.MediaProxy + alias Plug.Conn + + setup do + on_exit(fn -> Cachex.clear(:banned_urls_cache) end) + end + + describe "Media Proxy" do + setup do + clear_config([:media_proxy, :enabled], true) + clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") + + [url: MediaProxy.encode_url("https://google.fn/test.png")] + end + + test "it returns 404 when disabled", %{conn: conn} do + clear_config([:media_proxy, :enabled], false) + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/hhgfh/eeeee") + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/hhgfh/eeee/fff") + end + + test "it returns 403 for invalid signature", %{conn: conn, url: url} do + Pleroma.Config.put([Pleroma.Web.Endpoint, :secret_key_base], "000") + %{path: path} = URI.parse(url) + + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, path) + + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, "/proxy/hhgfh/eeee") + + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, "/proxy/hhgfh/eeee/fff") + end + + test "redirects to valid url when filename is invalidated", %{conn: conn, url: url} do + invalid_url = String.replace(url, "test.png", "test-file.png") + response = get(conn, invalid_url) + assert response.status == 302 + assert redirected_to(response) == url + end + + test "it performs ReverseProxy.call with valid signature", %{conn: conn, url: url} do + with_mock Pleroma.ReverseProxy, + call: fn _conn, _url, _opts -> %Conn{status: :success} end do + assert %Conn{status: :success} = get(conn, url) + end + end + + test "it returns 404 when url is in banned_urls cache", %{conn: conn, url: url} do + MediaProxy.put_in_banned_urls("https://google.fn/test.png") + + with_mock Pleroma.ReverseProxy, + call: fn _conn, _url, _opts -> %Conn{status: :success} end do + assert %Conn{status: 404, resp_body: "Not Found"} = get(conn, url) + end + end + end + + describe "Media Preview Proxy" do + def assert_dependencies_installed do + missing_dependencies = Pleroma.Helpers.MediaHelper.missing_dependencies() + + assert missing_dependencies == [], + "Error: missing dependencies (please refer to `docs/installation`): #{ + inspect(missing_dependencies) + }" + end + + setup do + clear_config([:media_proxy, :enabled], true) + clear_config([:media_preview_proxy, :enabled], true) + clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") + + original_url = "https://google.fn/test.png" + + [ + url: MediaProxy.encode_preview_url(original_url), + media_proxy_url: MediaProxy.encode_url(original_url) + ] + end + + test "returns 404 when media proxy is disabled", %{conn: conn} do + clear_config([:media_proxy, :enabled], false) + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/preview/hhgfh/eeeee") + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/preview/hhgfh/fff") + end + + test "returns 404 when disabled", %{conn: conn} do + clear_config([:media_preview_proxy, :enabled], false) + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/preview/hhgfh/eeeee") + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/preview/hhgfh/fff") + end + + test "it returns 403 for invalid signature", %{conn: conn, url: url} do + Pleroma.Config.put([Pleroma.Web.Endpoint, :secret_key_base], "000") + %{path: path} = URI.parse(url) + + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, path) + + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, "/proxy/preview/hhgfh/eeee") + + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, "/proxy/preview/hhgfh/eeee/fff") + end + + test "redirects to valid url when filename is invalidated", %{conn: conn, url: url} do + invalid_url = String.replace(url, "test.png", "test-file.png") + response = get(conn, invalid_url) + assert response.status == 302 + assert redirected_to(response) == url + end + + test "responds with 424 Failed Dependency if HEAD request to media proxy fails", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 500, body: ""} + end) + + response = get(conn, url) + assert response.status == 424 + assert response.resp_body == "Can't fetch HTTP headers (HTTP 500)." + end + + test "redirects to media proxy URI on unsupported content type", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "application/pdf"}]} + end) + + response = get(conn, url) + assert response.status == 302 + assert redirected_to(response) == media_proxy_url + end + + test "with `static=true` and GIF image preview requested, responds with JPEG image", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + assert_dependencies_installed() + + # Setting a high :min_content_length to ensure this scenario is not affected by its logic + clear_config([:media_preview_proxy, :min_content_length], 1_000_000_000) + + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{ + status: 200, + body: "", + headers: [{"content-type", "image/gif"}, {"content-length", "1001718"}] + } + + %{method: :get, url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.gif")} + end) + + response = get(conn, url <> "?static=true") + + assert response.status == 200 + assert Conn.get_resp_header(response, "content-type") == ["image/jpeg"] + assert response.resp_body != "" + end + + test "with GIF image preview requested and no `static` param, redirects to media proxy URI", + %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/gif"}]} + end) + + response = get(conn, url) + + assert response.status == 302 + assert redirected_to(response) == media_proxy_url + end + + test "with `static` param and non-GIF image preview requested, " <> + "redirects to media preview proxy URI without `static` param", + %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} + end) + + response = get(conn, url <> "?static=true") + + assert response.status == 302 + assert redirected_to(response) == url + end + + test "with :min_content_length setting not matched by Content-Length header, " <> + "redirects to media proxy URI", + %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + clear_config([:media_preview_proxy, :min_content_length], 100_000) + + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{ + status: 200, + body: "", + headers: [{"content-type", "image/gif"}, {"content-length", "5000"}] + } + end) + + response = get(conn, url) + + assert response.status == 302 + assert redirected_to(response) == media_proxy_url + end + + test "thumbnails PNG images into PNG", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + assert_dependencies_installed() + + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/png"}]} + + %{method: :get, url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.png")} + end) + + response = get(conn, url) + + assert response.status == 200 + assert Conn.get_resp_header(response, "content-type") == ["image/png"] + assert response.resp_body != "" + end + + test "thumbnails JPEG images into JPEG", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + assert_dependencies_installed() + + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} + + %{method: :get, url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")} + end) + + response = get(conn, url) + + assert response.status == 200 + assert Conn.get_resp_header(response, "content-type") == ["image/jpeg"] + assert response.resp_body != "" + end + + test "redirects to media proxy URI in case of thumbnailing error", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} + + %{method: :get, url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "<html><body>error</body></html>"} + end) + + response = get(conn, url) + + assert response.status == 302 + assert redirected_to(response) == media_proxy_url + end + end +end diff --git a/test/pleroma/web/media_proxy_test.exs b/test/pleroma/web/media_proxy_test.exs @@ -0,0 +1,234 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MediaProxyTest do + use ExUnit.Case + use Pleroma.Tests.Helpers + + alias Pleroma.Config + alias Pleroma.Web.Endpoint + alias Pleroma.Web.MediaProxy + + defp decode_result(encoded) do + [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/") + {:ok, decoded} = MediaProxy.decode_url(sig, base64) + decoded + end + + describe "when enabled" do + setup do: clear_config([:media_proxy, :enabled], true) + + test "ignores invalid url" do + assert MediaProxy.url(nil) == nil + assert MediaProxy.url("") == nil + end + + test "ignores relative url" do + assert MediaProxy.url("/local") == "/local" + assert MediaProxy.url("/") == "/" + end + + test "ignores local url" do + local_url = Endpoint.url() <> "/hello" + local_root = Endpoint.url() + assert MediaProxy.url(local_url) == local_url + assert MediaProxy.url(local_root) == local_root + end + + test "encodes and decodes URL" do + url = "https://pleroma.soykaf.com/static/logo.png" + encoded = MediaProxy.url(url) + + assert String.starts_with?( + encoded, + Config.get([:media_proxy, :base_url], Pleroma.Web.base_url()) + ) + + assert String.ends_with?(encoded, "/logo.png") + + assert decode_result(encoded) == url + end + + test "encodes and decodes URL without a path" do + url = "https://pleroma.soykaf.com" + encoded = MediaProxy.url(url) + assert decode_result(encoded) == url + end + + test "encodes and decodes URL without an extension" do + url = "https://pleroma.soykaf.com/path/" + encoded = MediaProxy.url(url) + assert String.ends_with?(encoded, "/path") + assert decode_result(encoded) == url + end + + test "encodes and decodes URL and ignores query params for the path" do + url = "https://pleroma.soykaf.com/static/logo.png?93939393939&bunny=true" + encoded = MediaProxy.url(url) + assert String.ends_with?(encoded, "/logo.png") + assert decode_result(encoded) == url + end + + test "validates signature" do + encoded = MediaProxy.url("https://pleroma.social") + + clear_config( + [Endpoint, :secret_key_base], + "00000000000000000000000000000000000000000000000" + ) + + [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/") + assert MediaProxy.decode_url(sig, base64) == {:error, :invalid_signature} + end + + def test_verify_request_path_and_url(request_path, url, expected_result) do + assert MediaProxy.verify_request_path_and_url(request_path, url) == expected_result + + assert MediaProxy.verify_request_path_and_url( + %Plug.Conn{ + params: %{"filename" => Path.basename(request_path)}, + request_path: request_path + }, + url + ) == expected_result + end + + test "if first arg of `verify_request_path_and_url/2` is a Plug.Conn without \"filename\" " <> + "parameter, `verify_request_path_and_url/2` returns :ok " do + assert MediaProxy.verify_request_path_and_url( + %Plug.Conn{params: %{}, request_path: "/some/path"}, + "https://instance.com/file.jpg" + ) == :ok + + assert MediaProxy.verify_request_path_and_url( + %Plug.Conn{params: %{}, request_path: "/path/to/file.jpg"}, + "https://instance.com/file.jpg" + ) == :ok + end + + test "`verify_request_path_and_url/2` preserves the encoded or decoded path" do + test_verify_request_path_and_url( + "/Hello world.jpg", + "http://pleroma.social/Hello world.jpg", + :ok + ) + + test_verify_request_path_and_url( + "/Hello%20world.jpg", + "http://pleroma.social/Hello%20world.jpg", + :ok + ) + + test_verify_request_path_and_url( + "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", + "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", + :ok + ) + + test_verify_request_path_and_url( + # Note: `conn.request_path` returns encoded url + "/ANALYSE-DAI-_-LE-STABLECOIN-100-D%C3%89CENTRALIS%C3%89-BQ.jpg", + "https://mydomain.com/uploads/2019/07/ANALYSE-DAI-_-LE-STABLECOIN-100-DÉCENTRALISÉ-BQ.jpg", + :ok + ) + + test_verify_request_path_and_url( + "/my%2Flong%2Furl%2F2019%2F07%2FS", + "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", + {:wrong_filename, "my%2Flong%2Furl%2F2019%2F07%2FS.jpg"} + ) + end + + test "uses the configured base_url" do + base_url = "https://cache.pleroma.social" + clear_config([:media_proxy, :base_url], base_url) + + url = "https://pleroma.soykaf.com/static/logo.png" + encoded = MediaProxy.url(url) + + assert String.starts_with?(encoded, base_url) + end + + # Some sites expect ASCII encoded characters in the URL to be preserved even if + # unnecessary. + # Issues: https://git.pleroma.social/pleroma/pleroma/issues/580 + # https://git.pleroma.social/pleroma/pleroma/issues/1055 + test "preserve ASCII encoding" do + url = + "https://pleroma.com/%20/%21/%22/%23/%24/%25/%26/%27/%28/%29/%2A/%2B/%2C/%2D/%2E/%2F/%30/%31/%32/%33/%34/%35/%36/%37/%38/%39/%3A/%3B/%3C/%3D/%3E/%3F/%40/%41/%42/%43/%44/%45/%46/%47/%48/%49/%4A/%4B/%4C/%4D/%4E/%4F/%50/%51/%52/%53/%54/%55/%56/%57/%58/%59/%5A/%5B/%5C/%5D/%5E/%5F/%60/%61/%62/%63/%64/%65/%66/%67/%68/%69/%6A/%6B/%6C/%6D/%6E/%6F/%70/%71/%72/%73/%74/%75/%76/%77/%78/%79/%7A/%7B/%7C/%7D/%7E/%7F/%80/%81/%82/%83/%84/%85/%86/%87/%88/%89/%8A/%8B/%8C/%8D/%8E/%8F/%90/%91/%92/%93/%94/%95/%96/%97/%98/%99/%9A/%9B/%9C/%9D/%9E/%9F/%C2%A0/%A1/%A2/%A3/%A4/%A5/%A6/%A7/%A8/%A9/%AA/%AB/%AC/%C2%AD/%AE/%AF/%B0/%B1/%B2/%B3/%B4/%B5/%B6/%B7/%B8/%B9/%BA/%BB/%BC/%BD/%BE/%BF/%C0/%C1/%C2/%C3/%C4/%C5/%C6/%C7/%C8/%C9/%CA/%CB/%CC/%CD/%CE/%CF/%D0/%D1/%D2/%D3/%D4/%D5/%D6/%D7/%D8/%D9/%DA/%DB/%DC/%DD/%DE/%DF/%E0/%E1/%E2/%E3/%E4/%E5/%E6/%E7/%E8/%E9/%EA/%EB/%EC/%ED/%EE/%EF/%F0/%F1/%F2/%F3/%F4/%F5/%F6/%F7/%F8/%F9/%FA/%FB/%FC/%FD/%FE/%FF" + + encoded = MediaProxy.url(url) + assert decode_result(encoded) == url + end + + # This includes unsafe/reserved characters which are not interpreted as part of the URL + # and would otherwise have to be ASCII encoded. It is our role to ensure the proxied URL + # is unmodified, so we are testing these characters anyway. + test "preserve non-unicode characters per RFC3986" do + url = + "https://pleroma.com/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-._~:/?#[]@!$&'()*+,;=|^`{}" + + encoded = MediaProxy.url(url) + assert decode_result(encoded) == url + end + + test "preserve unicode characters" do + url = "https://ko.wikipedia.org/wiki/위키백과:대문" + + encoded = MediaProxy.url(url) + assert decode_result(encoded) == url + end + end + + describe "when disabled" do + setup do: clear_config([:media_proxy, :enabled], false) + + test "does not encode remote urls" do + assert MediaProxy.url("https://google.fr") == "https://google.fr" + end + end + + describe "whitelist" do + setup do: clear_config([:media_proxy, :enabled], true) + + test "mediaproxy whitelist" do + clear_config([:media_proxy, :whitelist], ["https://google.com", "https://feld.me"]) + url = "https://feld.me/foo.png" + + unencoded = MediaProxy.url(url) + assert unencoded == url + end + + # TODO: delete after removing support bare domains for media proxy whitelist + test "mediaproxy whitelist bare domains whitelist (deprecated)" do + clear_config([:media_proxy, :whitelist], ["google.com", "feld.me"]) + url = "https://feld.me/foo.png" + + unencoded = MediaProxy.url(url) + assert unencoded == url + end + + test "does not change whitelisted urls" do + clear_config([:media_proxy, :whitelist], ["mycdn.akamai.com"]) + clear_config([:media_proxy, :base_url], "https://cache.pleroma.social") + + media_url = "https://mycdn.akamai.com" + + url = "#{media_url}/static/logo.png" + encoded = MediaProxy.url(url) + + assert String.starts_with?(encoded, media_url) + end + + test "ensure Pleroma.Upload base_url is always whitelisted" do + media_url = "https://media.pleroma.social" + clear_config([Pleroma.Upload, :base_url], media_url) + + url = "#{media_url}/static/logo.png" + encoded = MediaProxy.url(url) + + assert String.starts_with?(encoded, media_url) + end + end +end diff --git a/test/pleroma/web/metadata/player_view_test.exs b/test/pleroma/web/metadata/player_view_test.exs @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.PlayerViewTest do + use Pleroma.DataCase + + alias Pleroma.Web.Metadata.PlayerView + + test "it renders audio tag" do + res = + PlayerView.render( + "player.html", + %{"mediaType" => "audio", "href" => "test-href"} + ) + |> Phoenix.HTML.safe_to_string() + + assert res == + "<audio controls><source src=\"test-href\" type=\"audio\">Your browser does not support audio playback.</audio>" + end + + test "it renders videos tag" do + res = + PlayerView.render( + "player.html", + %{"mediaType" => "video", "href" => "test-href"} + ) + |> Phoenix.HTML.safe_to_string() + + assert res == + "<video controls loop><source src=\"test-href\" type=\"video\">Your browser does not support video playback.</video>" + end +end diff --git a/test/pleroma/web/metadata/providers/feed_test.exs b/test/pleroma/web/metadata/providers/feed_test.exs @@ -0,0 +1,18 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.FeedTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.Metadata.Providers.Feed + + test "it renders a link to user's atom feed" do + user = insert(:user, nickname: "lain") + + assert Feed.build_tags(%{user: user}) == [ + {:link, + [rel: "alternate", type: "application/atom+xml", href: "/users/lain/feed.atom"], []} + ] + end +end diff --git a/test/pleroma/web/metadata/providers/open_graph_test.exs b/test/pleroma/web/metadata/providers/open_graph_test.exs @@ -0,0 +1,96 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.OpenGraphTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.Metadata.Providers.OpenGraph + + setup do: clear_config([Pleroma.Web.Metadata, :unfurl_nsfw]) + + test "it renders all supported types of attachments and skips unknown types" do + user = insert(:user) + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "tag" => [], + "id" => "https://pleroma.gov/objects/whatever", + "content" => "pleroma in a nutshell", + "attachment" => [ + %{ + "url" => [ + %{"mediaType" => "image/png", "href" => "https://pleroma.gov/tenshi.png"} + ] + }, + %{ + "url" => [ + %{ + "mediaType" => "application/octet-stream", + "href" => "https://pleroma.gov/fqa/badapple.sfc" + } + ] + }, + %{ + "url" => [ + %{"mediaType" => "video/webm", "href" => "https://pleroma.gov/about/juche.webm"} + ] + }, + %{ + "url" => [ + %{ + "mediaType" => "audio/basic", + "href" => "http://www.gnu.org/music/free-software-song.au" + } + ] + } + ] + } + }) + + result = OpenGraph.build_tags(%{object: note, url: note.data["id"], user: user}) + + assert Enum.all?( + [ + {:meta, [property: "og:image", content: "https://pleroma.gov/tenshi.png"], []}, + {:meta, + [property: "og:audio", content: "http://www.gnu.org/music/free-software-song.au"], + []}, + {:meta, [property: "og:video", content: "https://pleroma.gov/about/juche.webm"], + []} + ], + fn element -> element in result end + ) + end + + test "it does not render attachments if post is nsfw" do + Pleroma.Config.put([Pleroma.Web.Metadata, :unfurl_nsfw], false) + user = insert(:user, avatar: %{"url" => [%{"href" => "https://pleroma.gov/tenshi.png"}]}) + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "id" => "https://pleroma.gov/objects/whatever", + "content" => "#cuteposting #nsfw #hambaga", + "tag" => ["cuteposting", "nsfw", "hambaga"], + "sensitive" => true, + "attachment" => [ + %{ + "url" => [ + %{"mediaType" => "image/png", "href" => "https://misskey.microsoft/corndog.png"} + ] + } + ] + } + }) + + result = OpenGraph.build_tags(%{object: note, url: note.data["id"], user: user}) + + assert {:meta, [property: "og:image", content: "https://pleroma.gov/tenshi.png"], []} in result + + refute {:meta, [property: "og:image", content: "https://misskey.microsoft/corndog.png"], []} in result + end +end diff --git a/test/pleroma/web/metadata/providers/rel_me_test.exs b/test/pleroma/web/metadata/providers/rel_me_test.exs @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.RelMeTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.Metadata.Providers.RelMe + + test "it renders all links with rel='me' from user bio" 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}) + + assert RelMe.build_tags(%{user: user}) == [ + {:link, [rel: "me", href: "http://some3.com"], []}, + {:link, [rel: "me", href: "https://another-link.com"], []} + ] + end +end diff --git a/test/pleroma/web/metadata/providers/restrict_indexing_test.exs b/test/pleroma/web/metadata/providers/restrict_indexing_test.exs @@ -0,0 +1,27 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.RestrictIndexingTest do + use ExUnit.Case, async: true + + describe "build_tags/1" do + test "for remote user" do + assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{ + user: %Pleroma.User{local: false} + }) == [{:meta, [name: "robots", content: "noindex, noarchive"], []}] + end + + test "for local user" do + assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{ + user: %Pleroma.User{local: true, discoverable: true} + }) == [] + end + + test "for local user when discoverable is false" do + assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{ + user: %Pleroma.User{local: true, discoverable: false} + }) == [{:meta, [name: "robots", content: "noindex, noarchive"], []}] + end + end +end diff --git a/test/pleroma/web/metadata/providers/twitter_card_test.exs b/test/pleroma/web/metadata/providers/twitter_card_test.exs @@ -0,0 +1,150 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.User + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Endpoint + alias Pleroma.Web.Metadata.Providers.TwitterCard + alias Pleroma.Web.Metadata.Utils + alias Pleroma.Web.Router + + setup do: clear_config([Pleroma.Web.Metadata, :unfurl_nsfw]) + + test "it renders twitter card for user info" do + user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") + avatar_url = Utils.attachment_url(User.avatar_url(user)) + 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"], []} + ] + end + + test "it uses summary twittercard if post has no attachment" 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", + "content" => "pleroma in a nutshell" + } + }) + + 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, [property: "twitter:card", content: "summary"], []} + ] == result + end + + test "it renders avatar not attachment if post is nsfw and unfurl_nsfw is disabled" do + Pleroma.Config.put([Pleroma.Web.Metadata, :unfurl_nsfw], false) + 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", + "content" => "pleroma in a nutshell", + "sensitive" => true, + "attachment" => [ + %{ + "url" => [%{"mediaType" => "image/png", "href" => "https://pleroma.gov/tenshi.png"}] + }, + %{ + "url" => [ + %{ + "mediaType" => "application/octet-stream", + "href" => "https://pleroma.gov/fqa/badapple.sfc" + } + ] + }, + %{ + "url" => [ + %{"mediaType" => "video/webm", "href" => "https://pleroma.gov/about/juche.webm"} + ] + } + ] + } + }) + + 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, [property: "twitter:card", content: "summary"], []} + ] == result + end + + test "it renders supported types of attachments and skips unknown types" 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", + "content" => "pleroma in a nutshell", + "attachment" => [ + %{ + "url" => [%{"mediaType" => "image/png", "href" => "https://pleroma.gov/tenshi.png"}] + }, + %{ + "url" => [ + %{ + "mediaType" => "application/octet-stream", + "href" => "https://pleroma.gov/fqa/badapple.sfc" + } + ] + }, + %{ + "url" => [ + %{"mediaType" => "video/webm", "href" => "https://pleroma.gov/about/juche.webm"} + ] + } + ] + } + }) + + 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:card", content: "player"], []}, + {:meta, + [ + property: "twitter:player", + content: Router.Helpers.o_status_url(Endpoint, :notice_player, activity.id) + ], []}, + {:meta, [property: "twitter:player:width", content: "480"], []}, + {:meta, [property: "twitter:player:height", content: "480"], []} + ] == result + end +end diff --git a/test/pleroma/web/metadata/utils_test.exs b/test/pleroma/web/metadata/utils_test.exs @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.UtilsTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.Metadata.Utils + + describe "scrub_html_and_truncate/1" do + test "it returns text without encode HTML" do + user = insert(:user) + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "id" => "https://pleroma.gov/objects/whatever", + "content" => "Pleroma's really cool!" + } + }) + + assert Utils.scrub_html_and_truncate(note) == "Pleroma's really cool!" + end + end + + describe "scrub_html_and_truncate/2" do + test "it returns text without encode HTML" do + assert Utils.scrub_html_and_truncate("Pleroma's really cool!") == "Pleroma's really cool!" + end + end +end diff --git a/test/pleroma/web/metadata_test.exs b/test/pleroma/web/metadata_test.exs @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MetadataTest do + use Pleroma.DataCase, async: true + + import Pleroma.Factory + + describe "restrict indexing remote users" do + test "for remote user" do + user = insert(:user, local: false) + + assert Pleroma.Web.Metadata.build_tags(%{user: user}) =~ + "<meta content=\"noindex, noarchive\" name=\"robots\">" + end + + test "for local user" do + user = insert(:user, discoverable: false) + + assert Pleroma.Web.Metadata.build_tags(%{user: user}) =~ + "<meta content=\"noindex, noarchive\" name=\"robots\">" + end + + test "for local user set to discoverable" do + user = insert(:user, discoverable: true) + + refute Pleroma.Web.Metadata.build_tags(%{user: user}) =~ + "<meta content=\"noindex, noarchive\" name=\"robots\">" + end + end + + describe "no metadata for private instances" do + test "for local user set to discoverable" do + clear_config([:instance, :public], false) + user = insert(:user, bio: "This is my secret fedi account bio", discoverable: true) + + assert "" = Pleroma.Web.Metadata.build_tags(%{user: user}) + end + + test "search exclusion metadata is included" do + clear_config([:instance, :public], false) + user = insert(:user, bio: "This is my secret fedi account bio", discoverable: false) + + assert ~s(<meta content="noindex, noarchive" name="robots">) == + Pleroma.Web.Metadata.build_tags(%{user: user}) + end + end +end diff --git a/test/pleroma/web/mongoose_im_controller_test.exs b/test/pleroma/web/mongoose_im_controller_test.exs @@ -0,0 +1,81 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MongooseIMControllerTest do + use Pleroma.Web.ConnCase + import Pleroma.Factory + + test "/user_exists", %{conn: conn} do + _user = insert(:user, nickname: "lain") + _remote_user = insert(:user, nickname: "alice", local: false) + _deactivated_user = insert(:user, nickname: "konata", deactivated: true) + + res = + conn + |> get(mongoose_im_path(conn, :user_exists), user: "lain") + |> json_response(200) + + assert res == true + + res = + conn + |> get(mongoose_im_path(conn, :user_exists), user: "alice") + |> json_response(404) + + assert res == false + + res = + conn + |> get(mongoose_im_path(conn, :user_exists), user: "bob") + |> json_response(404) + + assert res == false + + res = + conn + |> get(mongoose_im_path(conn, :user_exists), user: "konata") + |> json_response(404) + + assert res == false + end + + test "/check_password", %{conn: conn} do + user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt("cool")) + + _deactivated_user = + insert(:user, + nickname: "konata", + deactivated: true, + password_hash: Pbkdf2.hash_pwd_salt("cool") + ) + + res = + conn + |> get(mongoose_im_path(conn, :check_password), user: user.nickname, pass: "cool") + |> json_response(200) + + assert res == true + + res = + conn + |> get(mongoose_im_path(conn, :check_password), user: user.nickname, pass: "uncool") + |> json_response(403) + + assert res == false + + res = + conn + |> get(mongoose_im_path(conn, :check_password), user: "konata", pass: "cool") + |> json_response(404) + + assert res == false + + res = + conn + |> get(mongoose_im_path(conn, :check_password), user: "nobody", pass: "cool") + |> json_response(404) + + assert res == false + end +end diff --git a/test/pleroma/web/node_info_test.exs b/test/pleroma/web/node_info_test.exs @@ -0,0 +1,188 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.NodeInfoTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Config + + setup do: clear_config([:mrf_simple]) + setup do: clear_config(:instance) + + test "GET /.well-known/nodeinfo", %{conn: conn} do + links = + conn + |> get("/.well-known/nodeinfo") + |> json_response(200) + |> Map.fetch!("links") + + Enum.each(links, fn link -> + href = Map.fetch!(link, "href") + + conn + |> get(href) + |> json_response(200) + end) + end + + test "nodeinfo shows staff accounts", %{conn: conn} do + moderator = insert(:user, local: true, is_moderator: true) + admin = insert(:user, local: true, is_admin: true) + + conn = + conn + |> get("/nodeinfo/2.1.json") + + assert result = json_response(conn, 200) + + assert moderator.ap_id in result["metadata"]["staffAccounts"] + assert admin.ap_id in result["metadata"]["staffAccounts"] + end + + test "nodeinfo shows restricted nicknames", %{conn: conn} do + conn = + conn + |> get("/nodeinfo/2.1.json") + + assert result = json_response(conn, 200) + + assert Config.get([Pleroma.User, :restricted_nicknames]) == + result["metadata"]["restrictedNicknames"] + end + + test "returns software.repository field in nodeinfo 2.1", %{conn: conn} do + conn + |> get("/.well-known/nodeinfo") + |> json_response(200) + + conn = + conn + |> get("/nodeinfo/2.1.json") + + assert result = json_response(conn, 200) + assert Pleroma.Application.repository() == result["software"]["repository"] + end + + test "returns fieldsLimits field", %{conn: conn} do + clear_config([:instance, :max_account_fields], 10) + clear_config([:instance, :max_remote_account_fields], 15) + clear_config([:instance, :account_field_name_length], 255) + clear_config([:instance, :account_field_value_length], 2048) + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + assert response["metadata"]["fieldsLimits"]["maxFields"] == 10 + assert response["metadata"]["fieldsLimits"]["maxRemoteFields"] == 15 + assert response["metadata"]["fieldsLimits"]["nameLength"] == 255 + assert response["metadata"]["fieldsLimits"]["valueLength"] == 2048 + end + + test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do + clear_config([:instance, :safe_dm_mentions], true) + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + assert "safe_dm_mentions" in response["metadata"]["features"] + + Config.put([:instance, :safe_dm_mentions], false) + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + refute "safe_dm_mentions" in response["metadata"]["features"] + end + + describe "`metadata/federation/enabled`" do + setup do: clear_config([:instance, :federating]) + + test "it shows if federation is enabled/disabled", %{conn: conn} do + Config.put([:instance, :federating], true) + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + assert response["metadata"]["federation"]["enabled"] == true + + Config.put([:instance, :federating], false) + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + assert response["metadata"]["federation"]["enabled"] == false + end + end + + test "it shows default features flags", %{conn: conn} do + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + default_features = [ + "pleroma_api", + "mastodon_api", + "mastodon_api_streaming", + "polls", + "pleroma_explicit_addressing", + "shareable_emoji_packs", + "multifetch", + "pleroma_emoji_reactions", + "pleroma:api/v1/notifications:include_types_filter", + "pleroma_chat_messages" + ] + + assert MapSet.subset?( + MapSet.new(default_features), + MapSet.new(response["metadata"]["features"]) + ) + end + + test "it shows MRF transparency data if enabled", %{conn: conn} do + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) + clear_config([:mrf, :transparency], true) + + simple_config = %{"reject" => ["example.com"]} + clear_config(:mrf_simple, simple_config) + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + assert response["metadata"]["federation"]["mrf_simple"] == simple_config + end + + test "it performs exclusions from MRF transparency data if configured", %{conn: conn} do + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) + clear_config([:mrf, :transparency], true) + clear_config([:mrf, :transparency_exclusions], ["other.site"]) + + simple_config = %{"reject" => ["example.com", "other.site"]} + clear_config(:mrf_simple, simple_config) + + expected_config = %{"reject" => ["example.com"]} + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + assert response["metadata"]["federation"]["mrf_simple"] == expected_config + assert response["metadata"]["federation"]["exclusions"] == true + end +end diff --git a/test/pleroma/web/o_auth/app_test.exs b/test/pleroma/web/o_auth/app_test.exs @@ -0,0 +1,44 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.AppTest do + use Pleroma.DataCase + + alias Pleroma.Web.OAuth.App + import Pleroma.Factory + + describe "get_or_make/2" do + test "gets exist app" do + attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} + app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]})) + {:ok, %App{} = exist_app} = App.get_or_make(attrs, []) + assert exist_app == app + end + + test "make app" do + attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} + {:ok, %App{} = app} = App.get_or_make(attrs, ["write"]) + assert app.scopes == ["write"] + end + + test "gets exist app and updates scopes" do + attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} + app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]})) + {:ok, %App{} = exist_app} = App.get_or_make(attrs, ["read", "write", "follow", "push"]) + assert exist_app.id == app.id + assert exist_app.scopes == ["read", "write", "follow", "push"] + end + + test "has unique client_id" do + insert(:oauth_app, client_name: "", redirect_uris: "", client_id: "boop") + + error = + catch_error(insert(:oauth_app, client_name: "", redirect_uris: "", client_id: "boop")) + + assert %Ecto.ConstraintError{} = error + assert error.constraint == "apps_client_id_index" + assert error.type == :unique + end + end +end diff --git a/test/pleroma/web/o_auth/authorization_test.exs b/test/pleroma/web/o_auth/authorization_test.exs @@ -0,0 +1,77 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.AuthorizationTest do + use Pleroma.DataCase + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization + import Pleroma.Factory + + setup do + {:ok, app} = + Repo.insert( + App.register_changeset(%App{}, %{ + client_name: "client", + scopes: ["read", "write"], + redirect_uris: "url" + }) + ) + + %{app: app} + end + + test "create an authorization token for a valid app", %{app: app} do + user = insert(:user) + + {:ok, auth1} = Authorization.create_authorization(app, user) + assert auth1.scopes == app.scopes + + {:ok, auth2} = Authorization.create_authorization(app, user, ["read"]) + assert auth2.scopes == ["read"] + + for auth <- [auth1, auth2] do + assert auth.user_id == user.id + assert auth.app_id == app.id + assert String.length(auth.token) > 10 + assert auth.used == false + end + end + + test "use up a token", %{app: app} do + user = insert(:user) + + {:ok, auth} = Authorization.create_authorization(app, user) + + {:ok, auth} = Authorization.use_token(auth) + + assert auth.used == true + + assert {:error, "already used"} == Authorization.use_token(auth) + + expired_auth = %Authorization{ + user_id: user.id, + app_id: app.id, + valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -10), + token: "mytoken", + used: false + } + + {:ok, expired_auth} = Repo.insert(expired_auth) + + assert {:error, "token expired"} == Authorization.use_token(expired_auth) + end + + test "delete authorizations", %{app: app} do + user = insert(:user) + + {:ok, auth} = Authorization.create_authorization(app, user) + {:ok, auth} = Authorization.use_token(auth) + + Authorization.delete_user_authorizations(user) + + {_, invalid} = Authorization.use_token(auth) + + assert auth != invalid + end +end diff --git a/test/pleroma/web/o_auth/ldap_authorization_test.exs b/test/pleroma/web/o_auth/ldap_authorization_test.exs @@ -0,0 +1,135 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do + use Pleroma.Web.ConnCase + alias Pleroma.Repo + alias Pleroma.Web.OAuth.Token + import Pleroma.Factory + import Mock + + @skip if !Code.ensure_loaded?(:eldap), do: :skip + + setup_all do: clear_config([:ldap, :enabled], true) + + setup_all do: clear_config(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.LDAPAuthenticator) + + @tag @skip + test "authorizes the existing user using LDAP credentials" do + password = "testpassword" + user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) + app = insert(:oauth_app, scopes: ["read", "write"]) + + host = Pleroma.Config.get([:ldap, :host]) |> to_charlist + port = Pleroma.Config.get([:ldap, :port]) + + with_mocks [ + {:eldap, [], + [ + open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end, + simple_bind: fn _connection, _dn, ^password -> :ok end, + close: fn _connection -> + send(self(), :close_connection) + :ok + end + ]} + ] do + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token} = json_response(conn, 200) + + token = Repo.get_by(Token, token: token) + + assert token.user_id == user.id + assert_received :close_connection + end + end + + @tag @skip + test "creates a new user after successful LDAP authorization" do + password = "testpassword" + user = build(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + host = Pleroma.Config.get([:ldap, :host]) |> to_charlist + port = Pleroma.Config.get([:ldap, :port]) + + with_mocks [ + {:eldap, [], + [ + open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end, + simple_bind: fn _connection, _dn, ^password -> :ok end, + equalityMatch: fn _type, _value -> :ok end, + wholeSubtree: fn -> :ok end, + search: fn _connection, _options -> + {:ok, {:eldap_search_result, [{:eldap_entry, '', []}], []}} + end, + close: fn _connection -> + send(self(), :close_connection) + :ok + end + ]} + ] do + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token} = json_response(conn, 200) + + token = Repo.get_by(Token, token: token) |> Repo.preload(:user) + + assert token.user.nickname == user.nickname + assert_received :close_connection + end + end + + @tag @skip + test "disallow authorization for wrong LDAP credentials" do + password = "testpassword" + user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) + app = insert(:oauth_app, scopes: ["read", "write"]) + + host = Pleroma.Config.get([:ldap, :host]) |> to_charlist + port = Pleroma.Config.get([:ldap, :port]) + + with_mocks [ + {:eldap, [], + [ + open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end, + simple_bind: fn _connection, _dn, ^password -> {:error, :invalidCredentials} end, + close: fn _connection -> + send(self(), :close_connection) + :ok + end + ]} + ] do + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"error" => "Invalid credentials"} = json_response(conn, 400) + assert_received :close_connection + end + end +end diff --git a/test/pleroma/web/o_auth/mfa_controller_test.exs b/test/pleroma/web/o_auth/mfa_controller_test.exs @@ -0,0 +1,306 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.MFAControllerTest do + use Pleroma.Web.ConnCase + import Pleroma.Factory + + alias Pleroma.MFA + alias Pleroma.MFA.BackupCodes + alias Pleroma.MFA.TOTP + alias Pleroma.Repo + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.OAuthController + + setup %{conn: conn} do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + backup_codes: [Pbkdf2.hash_pwd_salt("test-code")], + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + app = insert(:oauth_app) + {:ok, conn: conn, user: user, app: app} + end + + describe "show" do + setup %{conn: conn, user: user, app: app} do + mfa_token = + insert(:mfa_token, + user: user, + authorization: build(:oauth_authorization, app: app, scopes: ["write"]) + ) + + {:ok, conn: conn, mfa_token: mfa_token} + end + + test "GET /oauth/mfa renders mfa forms", %{conn: conn, mfa_token: mfa_token} do + conn = + get( + conn, + "/oauth/mfa", + %{ + "mfa_token" => mfa_token.token, + "state" => "a_state", + "redirect_uri" => "http://localhost:8080/callback" + } + ) + + assert response = html_response(conn, 200) + assert response =~ "Two-factor authentication" + assert response =~ mfa_token.token + assert response =~ "http://localhost:8080/callback" + end + + test "GET /oauth/mfa renders mfa recovery forms", %{conn: conn, mfa_token: mfa_token} do + conn = + get( + conn, + "/oauth/mfa", + %{ + "mfa_token" => mfa_token.token, + "state" => "a_state", + "redirect_uri" => "http://localhost:8080/callback", + "challenge_type" => "recovery" + } + ) + + assert response = html_response(conn, 200) + assert response =~ "Two-factor recovery" + assert response =~ mfa_token.token + assert response =~ "http://localhost:8080/callback" + end + end + + describe "verify" do + setup %{conn: conn, user: user, app: app} do + mfa_token = + insert(:mfa_token, + user: user, + authorization: build(:oauth_authorization, app: app, scopes: ["write"]) + ) + + {:ok, conn: conn, user: user, mfa_token: mfa_token, app: app} + end + + test "POST /oauth/mfa/verify, verify totp code", %{ + conn: conn, + user: user, + mfa_token: mfa_token, + app: app + } do + otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) + + conn = + conn + |> post("/oauth/mfa/verify", %{ + "mfa" => %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "totp", + "code" => otp_token, + "state" => "a_state", + "redirect_uri" => OAuthController.default_redirect_uri(app) + } + }) + + target = redirected_to(conn) + target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string() + query = URI.parse(target).query |> URI.query_decoder() |> Map.new() + assert %{"state" => "a_state", "code" => code} = query + assert target_url == OAuthController.default_redirect_uri(app) + auth = Repo.get_by(Authorization, token: code) + assert auth.scopes == ["write"] + end + + test "POST /oauth/mfa/verify, verify recovery code", %{ + conn: conn, + mfa_token: mfa_token, + app: app + } do + conn = + conn + |> post("/oauth/mfa/verify", %{ + "mfa" => %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "recovery", + "code" => "test-code", + "state" => "a_state", + "redirect_uri" => OAuthController.default_redirect_uri(app) + } + }) + + target = redirected_to(conn) + target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string() + query = URI.parse(target).query |> URI.query_decoder() |> Map.new() + assert %{"state" => "a_state", "code" => code} = query + assert target_url == OAuthController.default_redirect_uri(app) + auth = Repo.get_by(Authorization, token: code) + assert auth.scopes == ["write"] + end + end + + describe "challenge/totp" do + test "returns access token with valid code", %{conn: conn, user: user, app: app} do + otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) + + mfa_token = + insert(:mfa_token, + user: user, + authorization: build(:oauth_authorization, app: app, scopes: ["write"]) + ) + + response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "totp", + "code" => otp_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(:ok) + + ap_id = user.ap_id + + assert match?( + %{ + "access_token" => _, + "expires_in" => 600, + "me" => ^ap_id, + "refresh_token" => _, + "scope" => "write", + "token_type" => "Bearer" + }, + response + ) + end + + test "returns errors when mfa token invalid", %{conn: conn, user: user, app: app} do + otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) + + response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => "XXX", + "challenge_type" => "totp", + "code" => otp_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(400) + + assert response == %{"error" => "Invalid code"} + end + + test "returns error when otp code is invalid", %{conn: conn, user: user, app: app} do + mfa_token = insert(:mfa_token, user: user) + + response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "totp", + "code" => "XXX", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(400) + + assert response == %{"error" => "Invalid code"} + end + + test "returns error when client credentails 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) + + response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "totp", + "code" => otp_token, + "client_id" => "xxx", + "client_secret" => "xxx" + }) + |> json_response(400) + + assert response == %{"error" => "Invalid code"} + end + end + + describe "challenge/recovery" do + setup %{conn: conn} do + app = insert(:oauth_app) + {:ok, conn: conn, app: app} + end + + test "returns access token with valid code", %{conn: conn, app: app} do + otp_secret = TOTP.generate_secret() + + [code | _] = backup_codes = BackupCodes.generate() + + hashed_codes = + backup_codes + |> Enum.map(&Pbkdf2.hash_pwd_salt(&1)) + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + backup_codes: hashed_codes, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + mfa_token = + insert(:mfa_token, + user: user, + authorization: build(:oauth_authorization, app: app, scopes: ["write"]) + ) + + response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "recovery", + "code" => code, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(:ok) + + ap_id = user.ap_id + + assert match?( + %{ + "access_token" => _, + "expires_in" => 600, + "me" => ^ap_id, + "refresh_token" => _, + "scope" => "write", + "token_type" => "Bearer" + }, + response + ) + + error_response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "recovery", + "code" => code, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(400) + + assert error_response == %{"error" => "Invalid code"} + end + end +end diff --git a/test/pleroma/web/o_auth/o_auth_controller_test.exs b/test/pleroma/web/o_auth/o_auth_controller_test.exs @@ -0,0 +1,1232 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.OAuthControllerTest do + use Pleroma.Web.ConnCase + import Pleroma.Factory + + alias Pleroma.MFA + alias Pleroma.MFA.TOTP + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.OAuthController + alias Pleroma.Web.OAuth.Token + + @session_opts [ + store: :cookie, + key: "_test", + signing_salt: "cooldude" + ] + setup do + clear_config([:instance, :account_activation_required]) + clear_config([:instance, :account_approval_required]) + end + + describe "in OAuth consumer mode, " do + setup do + [ + app: insert(:oauth_app), + conn: + build_conn() + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + ] + end + + setup do: clear_config([:auth, :oauth_consumer_strategies], ~w(twitter facebook)) + + test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{ + app: app, + conn: conn + } do + conn = + get( + conn, + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "scope" => "read" + } + ) + + assert response = html_response(conn, 200) + assert response =~ "Sign in with Twitter" + assert response =~ o_auth_path(conn, :prepare_request) + end + + test "GET /oauth/prepare_request encodes parameters as `state` and redirects", %{ + app: app, + conn: conn + } do + conn = + get( + conn, + "/oauth/prepare_request", + %{ + "provider" => "twitter", + "authorization" => %{ + "scope" => "read follow", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "a_state" + } + } + ) + + assert response = html_response(conn, 302) + + redirect_query = URI.parse(redirected_to(conn)).query + assert %{"state" => state_param} = URI.decode_query(redirect_query) + assert {:ok, state_components} = Poison.decode(state_param) + + expected_client_id = app.client_id + expected_redirect_uri = app.redirect_uris + + assert %{ + "scope" => "read follow", + "client_id" => ^expected_client_id, + "redirect_uri" => ^expected_redirect_uri, + "state" => "a_state" + } = state_components + end + + test "with user-bound registration, GET /oauth/<provider>/callback redirects to `redirect_uri` with `code`", + %{app: app, conn: conn} do + registration = insert(:registration) + redirect_uri = OAuthController.default_redirect_uri(app) + + state_params = %{ + "scope" => Enum.join(app.scopes, " "), + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "" + } + + conn = + conn + |> assign(:ueberauth_auth, %{provider: registration.provider, uid: registration.uid}) + |> get( + "/oauth/twitter/callback", + %{ + "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", + "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", + "provider" => "twitter", + "state" => Poison.encode!(state_params) + } + ) + + assert response = html_response(conn, 302) + assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ + end + + test "with user-unbound registration, GET /oauth/<provider>/callback renders registration_details page", + %{app: app, conn: conn} do + user = insert(:user) + + state_params = %{ + "scope" => "read write", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "a_state" + } + + conn = + conn + |> assign(:ueberauth_auth, %{ + provider: "twitter", + uid: "171799000", + info: %{nickname: user.nickname, email: user.email, name: user.name, description: nil} + }) + |> get( + "/oauth/twitter/callback", + %{ + "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", + "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", + "provider" => "twitter", + "state" => Poison.encode!(state_params) + } + ) + + assert response = html_response(conn, 200) + assert response =~ ~r/name="op" type="submit" value="register"/ + assert response =~ ~r/name="op" type="submit" value="connect"/ + assert response =~ user.email + assert response =~ user.nickname + end + + test "on authentication error, GET /oauth/<provider>/callback redirects to `redirect_uri`", %{ + app: app, + conn: conn + } do + state_params = %{ + "scope" => Enum.join(app.scopes, " "), + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "" + } + + conn = + conn + |> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]}) + |> get( + "/oauth/twitter/callback", + %{ + "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", + "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", + "provider" => "twitter", + "state" => Poison.encode!(state_params) + } + ) + + assert response = html_response(conn, 302) + assert redirected_to(conn) == app.redirect_uris + assert get_flash(conn, :error) == "Failed to authenticate: (error description)." + end + + test "GET /oauth/registration_details renders registration details form", %{ + app: app, + conn: conn + } do + conn = + get( + conn, + "/oauth/registration_details", + %{ + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "a_state", + "nickname" => nil, + "email" => "john@doe.com" + } + } + ) + + assert response = html_response(conn, 200) + assert response =~ ~r/name="op" type="submit" value="register"/ + assert response =~ ~r/name="op" type="submit" value="connect"/ + end + + test "with valid params, POST /oauth/register?op=register redirects to `redirect_uri` with `code`", + %{ + app: app, + conn: conn + } do + registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) + redirect_uri = OAuthController.default_redirect_uri(app) + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post( + "/oauth/register", + %{ + "op" => "register", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "a_state", + "nickname" => "availablenick", + "email" => "available@email.com" + } + } + ) + + assert response = html_response(conn, 302) + assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ + end + + test "with unlisted `redirect_uri`, POST /oauth/register?op=register results in HTTP 401", + %{ + app: app, + conn: conn + } do + registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) + unlisted_redirect_uri = "http://cross-site-request.com" + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post( + "/oauth/register", + %{ + "op" => "register", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => unlisted_redirect_uri, + "state" => "a_state", + "nickname" => "availablenick", + "email" => "available@email.com" + } + } + ) + + assert response = html_response(conn, 401) + end + + test "with invalid params, POST /oauth/register?op=register renders registration_details page", + %{ + app: app, + conn: conn + } do + another_user = insert(:user) + registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) + + params = %{ + "op" => "register", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "a_state", + "nickname" => "availablenickname", + "email" => "available@email.com" + } + } + + for {bad_param, bad_param_value} <- + [{"nickname", another_user.nickname}, {"email", another_user.email}] do + bad_registration_attrs = %{ + "authorization" => Map.put(params["authorization"], bad_param, bad_param_value) + } + + bad_params = Map.merge(params, bad_registration_attrs) + + conn = + conn + |> put_session(:registration_id, registration.id) + |> 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." + end + end + + test "with valid params, POST /oauth/register?op=connect redirects to `redirect_uri` with `code`", + %{ + app: app, + conn: conn + } do + user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt("testpassword")) + registration = insert(:registration, user: nil) + redirect_uri = OAuthController.default_redirect_uri(app) + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post( + "/oauth/register", + %{ + "op" => "connect", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "a_state", + "name" => user.nickname, + "password" => "testpassword" + } + } + ) + + assert response = html_response(conn, 302) + assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ + end + + test "with unlisted `redirect_uri`, POST /oauth/register?op=connect results in HTTP 401`", + %{ + app: app, + conn: conn + } do + user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt("testpassword")) + registration = insert(:registration, user: nil) + unlisted_redirect_uri = "http://cross-site-request.com" + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post( + "/oauth/register", + %{ + "op" => "connect", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => unlisted_redirect_uri, + "state" => "a_state", + "name" => user.nickname, + "password" => "testpassword" + } + } + ) + + assert response = html_response(conn, 401) + end + + test "with invalid params, POST /oauth/register?op=connect renders registration_details page", + %{ + app: app, + conn: conn + } do + user = insert(:user) + registration = insert(:registration, user: nil) + + params = %{ + "op" => "connect", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "a_state", + "name" => user.nickname, + "password" => "wrong password" + } + } + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post("/oauth/register", params) + + assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/ + assert get_flash(conn, :error) == "Invalid Username/Password" + end + end + + describe "GET /oauth/authorize" do + setup do + [ + app: insert(:oauth_app, redirect_uris: "https://redirect.url"), + conn: + build_conn() + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + ] + end + + test "renders authentication page", %{app: app, conn: conn} do + conn = + get( + conn, + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "scope" => "read" + } + ) + + assert html_response(conn, 200) =~ ~s(type="submit") + end + + test "properly handles internal calls with `authorization`-wrapped params", %{ + app: app, + conn: conn + } do + conn = + get( + conn, + "/oauth/authorize", + %{ + "authorization" => %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "scope" => "read" + } + } + ) + + assert html_response(conn, 200) =~ ~s(type="submit") + end + + test "renders authentication page if user is already authenticated but `force_login` is tru-ish", + %{app: app, conn: conn} do + token = insert(:oauth_token, app: app) + + conn = + conn + |> put_session(:oauth_token, token.token) + |> get( + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "scope" => "read", + "force_login" => "true" + } + ) + + assert html_response(conn, 200) =~ ~s(type="submit") + end + + test "renders authentication page if user is already authenticated but user request with another client", + %{ + app: app, + conn: conn + } do + token = insert(:oauth_token, app: app) + + conn = + conn + |> put_session(:oauth_token, token.token) + |> get( + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => "another_client_id", + "redirect_uri" => OAuthController.default_redirect_uri(app), + "scope" => "read" + } + ) + + assert html_response(conn, 200) =~ ~s(type="submit") + end + + test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params", + %{ + app: app, + conn: conn + } do + token = insert(:oauth_token, app: app) + + conn = + conn + |> put_session(:oauth_token, token.token) + |> get( + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "specific_client_state", + "scope" => "read" + } + ) + + assert URI.decode(redirected_to(conn)) == + "https://redirect.url?access_token=#{token.token}&state=specific_client_state" + end + + test "with existing authentication and unlisted non-OOB `redirect_uri`, redirects without credentials", + %{ + app: app, + conn: conn + } do + unlisted_redirect_uri = "http://cross-site-request.com" + token = insert(:oauth_token, app: app) + + conn = + conn + |> put_session(:oauth_token, token.token) + |> get( + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => unlisted_redirect_uri, + "state" => "specific_client_state", + "scope" => "read" + } + ) + + assert redirected_to(conn) == unlisted_redirect_uri + end + + test "with existing authentication and OOB `redirect_uri`, redirects to app with `token` and `state` params", + %{ + app: app, + conn: conn + } do + token = insert(:oauth_token, app: app) + + conn = + conn + |> put_session(:oauth_token, token.token) + |> get( + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "scope" => "read" + } + ) + + assert html_response(conn, 200) =~ "Authorization exists" + end + end + + describe "POST /oauth/authorize" do + test "redirects with oauth authorization, " <> + "granting requested app-supported scopes to both admin- and non-admin users" do + app_scopes = ["read", "write", "admin", "secret_scope"] + app = insert(:oauth_app, scopes: app_scopes) + redirect_uri = OAuthController.default_redirect_uri(app) + + non_admin = insert(:user, is_admin: false) + admin = insert(:user, is_admin: true) + scopes_subset = ["read:subscope", "write", "admin"] + + # In case scope param is missing, expecting _all_ app-supported scopes to be granted + for user <- [non_admin, admin], + {requested_scopes, expected_scopes} <- + %{scopes_subset => scopes_subset, nil: app_scopes} do + conn = + post( + build_conn(), + "/oauth/authorize", + %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "scope" => requested_scopes, + "state" => "statepassed" + } + } + ) + + target = redirected_to(conn) + assert target =~ redirect_uri + + query = URI.parse(target).query |> URI.query_decoder() |> Map.new() + + assert %{"state" => "statepassed", "code" => code} = query + auth = Repo.get_by(Authorization, token: code) + assert auth + assert auth.scopes == expected_scopes + end + end + + test "redirect to on two-factor auth page" do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + app = insert(:oauth_app, scopes: ["read", "write", "follow"]) + + conn = + build_conn() + |> post("/oauth/authorize", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "scope" => "read write", + "state" => "statepassed" + } + }) + + result = html_response(conn, 200) + + mfa_token = Repo.get_by(MFA.Token, user_id: user.id) + assert result =~ app.redirect_uris + assert result =~ "statepassed" + assert result =~ mfa_token.token + assert result =~ "Two-factor authentication" + end + + test "returns 401 for wrong credentials", %{conn: conn} do + user = insert(:user) + app = insert(:oauth_app) + redirect_uri = OAuthController.default_redirect_uri(app) + + result = + conn + |> post("/oauth/authorize", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "wrong", + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "statepassed", + "scope" => Enum.join(app.scopes, " ") + } + }) + |> html_response(:unauthorized) + + # Keep the details + assert result =~ app.client_id + assert result =~ redirect_uri + + # Error message + assert result =~ "Invalid Username/Password" + end + + test "returns 401 for missing scopes" do + user = insert(:user, is_admin: false) + app = insert(:oauth_app, scopes: ["read", "write", "admin"]) + redirect_uri = OAuthController.default_redirect_uri(app) + + result = + build_conn() + |> post("/oauth/authorize", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "statepassed", + "scope" => "" + } + }) + |> html_response(:unauthorized) + + # Keep the details + assert result =~ app.client_id + assert result =~ redirect_uri + + # Error message + assert result =~ "This action is outside the authorized scopes" + end + + test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + redirect_uri = OAuthController.default_redirect_uri(app) + + result = + conn + |> post("/oauth/authorize", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "statepassed", + "scope" => "read write follow" + } + }) + |> html_response(:unauthorized) + + # Keep the details + assert result =~ app.client_id + assert result =~ redirect_uri + + # Error message + assert result =~ "This action is outside the authorized scopes" + end + end + + describe "POST /oauth/token" do + test "issues a token for an all-body request" do + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "authorization_code", + "code" => auth.token, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token, "me" => ap_id} = json_response(conn, 200) + + token = Repo.get_by(Token, token: token) + assert token + assert token.scopes == auth.scopes + assert user.ap_id == ap_id + end + + test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do + password = "testpassword" + user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) + + app = insert(:oauth_app, scopes: ["read", "write"]) + + # Note: "scope" param is intentionally omitted + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token} = json_response(conn, 200) + + token = Repo.get_by(Token, token: token) + assert token + assert token.scopes == app.scopes + end + + test "issues a mfa token for `password` grant_type, when MFA enabled" do + password = "testpassword" + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + password_hash: Pbkdf2.hash_pwd_salt(password), + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + app = insert(:oauth_app, scopes: ["read", "write"]) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(403) + + assert match?( + %{ + "supported_challenge_types" => "totp", + "mfa_token" => _, + "error" => "mfa_required" + }, + response + ) + + token = Repo.get_by(MFA.Token, token: response["mfa_token"]) + assert token.user_id == user.id + assert token.authorization_id + end + + test "issues a token for request with HTTP basic auth client credentials" do + user = insert(:user) + app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["scope1", "scope2"]) + assert auth.scopes == ["scope1", "scope2"] + + app_encoded = + (URI.encode_www_form(app.client_id) <> ":" <> URI.encode_www_form(app.client_secret)) + |> Base.encode64() + + conn = + build_conn() + |> put_req_header("authorization", "Basic " <> app_encoded) + |> post("/oauth/token", %{ + "grant_type" => "authorization_code", + "code" => auth.token, + "redirect_uri" => OAuthController.default_redirect_uri(app) + }) + + assert %{"access_token" => token, "scope" => scope} = json_response(conn, 200) + + assert scope == "scope1 scope2" + + token = Repo.get_by(Token, token: token) + assert token + assert token.scopes == ["scope1", "scope2"] + end + + test "issue a token for client_credentials grant type" do + app = insert(:oauth_app, scopes: ["read", "write"]) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "client_credentials", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = + json_response(conn, 200) + + assert token + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + assert refresh + assert scope == "read write" + end + + test "rejects token exchange with invalid client credentials" do + user = insert(:user) + app = insert(:oauth_app) + + {:ok, auth} = Authorization.create_authorization(app, user) + + conn = + build_conn() + |> put_req_header("authorization", "Basic JTIxOiVGMCU5RiVBNCVCNwo=") + |> post("/oauth/token", %{ + "grant_type" => "authorization_code", + "code" => auth.token, + "redirect_uri" => OAuthController.default_redirect_uri(app) + }) + + assert resp = json_response(conn, 400) + assert %{"error" => _} = resp + refute Map.has_key?(resp, "access_token") + end + + test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do + Pleroma.Config.put([:instance, :account_activation_required], true) + password = "testpassword" + + {:ok, user} = + insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) + |> User.confirmation_changeset(need_confirmation: true) + |> User.update_and_set_cache() + + refute Pleroma.User.account_status(user) == :active + + app = insert(:oauth_app) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert resp = json_response(conn, 403) + assert %{"error" => _} = resp + refute Map.has_key?(resp, "access_token") + end + + test "rejects token exchange for valid credentials belonging to deactivated user" do + password = "testpassword" + + user = + insert(:user, + password_hash: Pbkdf2.hash_pwd_salt(password), + deactivated: true + ) + + app = insert(:oauth_app) + + resp = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(403) + + assert resp == %{ + "error" => "Your account is currently disabled", + "identifier" => "account_is_disabled" + } + end + + test "rejects token exchange for user with password_reset_pending set to true" do + password = "testpassword" + + user = + insert(:user, + password_hash: Pbkdf2.hash_pwd_salt(password), + password_reset_pending: true + ) + + app = insert(:oauth_app, scopes: ["read", "write"]) + + resp = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(403) + + assert resp == %{ + "error" => "Password reset is required", + "identifier" => "password_reset_required" + } + end + + test "rejects token exchange for user with confirmation_pending set to true" do + Pleroma.Config.put([:instance, :account_activation_required], true) + password = "testpassword" + + user = + insert(:user, + password_hash: Pbkdf2.hash_pwd_salt(password), + confirmation_pending: true + ) + + app = insert(:oauth_app, scopes: ["read", "write"]) + + resp = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(403) + + assert resp == %{ + "error" => "Your login is missing a confirmed e-mail address", + "identifier" => "missing_confirmed_email" + } + end + + test "rejects token exchange for valid credentials belonging to an unapproved user" do + password = "testpassword" + + user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password), approval_pending: true) + + refute Pleroma.User.account_status(user) == :active + + app = insert(:oauth_app) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert resp = json_response(conn, 403) + assert %{"error" => _} = resp + refute Map.has_key?(resp, "access_token") + end + + test "rejects an invalid authorization code" do + app = insert(:oauth_app) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "authorization_code", + "code" => "Imobviouslyinvalid", + "redirect_uri" => OAuthController.default_redirect_uri(app), + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert resp = json_response(conn, 400) + assert %{"error" => _} = json_response(conn, 400) + refute Map.has_key?(resp, "access_token") + end + end + + describe "POST /oauth/token - refresh token" do + setup do: clear_config([:oauth2, :issue_new_refresh_token]) + + test "issues a new access token with keep fresh token" do + Pleroma.Config.put([:oauth2, :issue_new_refresh_token], true) + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + {:ok, token} = Token.exchange_token(app, auth) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => token.refresh_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(200) + + ap_id = user.ap_id + + assert match?( + %{ + "scope" => "write", + "token_type" => "Bearer", + "expires_in" => 600, + "access_token" => _, + "refresh_token" => _, + "me" => ^ap_id + }, + response + ) + + refute Repo.get_by(Token, token: token.token) + new_token = Repo.get_by(Token, token: response["access_token"]) + assert new_token.refresh_token == token.refresh_token + assert new_token.scopes == auth.scopes + assert new_token.user_id == user.id + assert new_token.app_id == app.id + end + + test "issues a new access token with new fresh token" do + Pleroma.Config.put([:oauth2, :issue_new_refresh_token], false) + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + {:ok, token} = Token.exchange_token(app, auth) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => token.refresh_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(200) + + ap_id = user.ap_id + + assert match?( + %{ + "scope" => "write", + "token_type" => "Bearer", + "expires_in" => 600, + "access_token" => _, + "refresh_token" => _, + "me" => ^ap_id + }, + response + ) + + refute Repo.get_by(Token, token: token.token) + new_token = Repo.get_by(Token, token: response["access_token"]) + refute new_token.refresh_token == token.refresh_token + assert new_token.scopes == auth.scopes + assert new_token.user_id == user.id + assert new_token.app_id == app.id + end + + test "returns 400 if we try use access token" do + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + {:ok, token} = Token.exchange_token(app, auth) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => token.token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(400) + + assert %{"error" => "Invalid credentials"} == response + end + + test "returns 400 if refresh_token invalid" do + app = insert(:oauth_app, scopes: ["read", "write"]) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => "token.refresh_token", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(400) + + assert %{"error" => "Invalid credentials"} == response + end + + test "issues a new token if token expired" do + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + {:ok, token} = Token.exchange_token(app, auth) + + change = + Ecto.Changeset.change( + token, + %{valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -86_400 * 30)} + ) + + {:ok, access_token} = Repo.update(change) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => access_token.refresh_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(200) + + ap_id = user.ap_id + + assert match?( + %{ + "scope" => "write", + "token_type" => "Bearer", + "expires_in" => 600, + "access_token" => _, + "refresh_token" => _, + "me" => ^ap_id + }, + response + ) + + refute Repo.get_by(Token, token: token.token) + token = Repo.get_by(Token, token: response["access_token"]) + assert token + assert token.scopes == auth.scopes + assert token.user_id == user.id + assert token.app_id == app.id + end + end + + describe "POST /oauth/token - bad request" do + test "returns 500" do + response = + build_conn() + |> post("/oauth/token", %{}) + |> json_response(500) + + assert %{"error" => "Bad request"} == response + end + end + + describe "POST /oauth/revoke - bad request" do + test "returns 500" do + response = + build_conn() + |> post("/oauth/revoke", %{}) + |> json_response(500) + + assert %{"error" => "Bad request"} == response + end + end +end diff --git a/test/pleroma/web/o_auth/token/utils_test.exs b/test/pleroma/web/o_auth/token/utils_test.exs @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token.UtilsTest do + use Pleroma.DataCase + alias Pleroma.Web.OAuth.Token.Utils + import Pleroma.Factory + + describe "fetch_app/1" do + test "returns error when credentials is invalid" do + assert {:error, :not_found} = + Utils.fetch_app(%Plug.Conn{params: %{"client_id" => 1, "client_secret" => "x"}}) + end + + test "returns App by params credentails" do + app = insert(:oauth_app) + + assert {:ok, load_app} = + Utils.fetch_app(%Plug.Conn{ + params: %{"client_id" => app.client_id, "client_secret" => app.client_secret} + }) + + assert load_app == app + end + + test "returns App by header credentails" do + app = insert(:oauth_app) + header = "Basic " <> Base.encode64("#{app.client_id}:#{app.client_secret}") + + conn = + %Plug.Conn{} + |> Plug.Conn.put_req_header("authorization", header) + + assert {:ok, load_app} = Utils.fetch_app(conn) + assert load_app == app + end + end + + describe "format_created_at/1" do + test "returns formatted created at" do + token = insert(:oauth_token) + date = Utils.format_created_at(token) + + token_date = + token.inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> DateTime.to_unix() + + assert token_date == date + end + end +end diff --git a/test/pleroma/web/o_auth/token_test.exs b/test/pleroma/web/o_auth/token_test.exs @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.TokenTest do + use Pleroma.DataCase + alias Pleroma.Repo + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.Token + + import Pleroma.Factory + + test "exchanges a auth token for an access token, preserving `scopes`" do + {:ok, app} = + Repo.insert( + App.register_changeset(%App{}, %{ + client_name: "client", + scopes: ["read", "write"], + redirect_uris: "url" + }) + ) + + user = insert(:user) + + {:ok, auth} = Authorization.create_authorization(app, user, ["read"]) + assert auth.scopes == ["read"] + + {:ok, token} = Token.exchange_token(app, auth) + + assert token.app_id == app.id + assert token.user_id == user.id + assert token.scopes == auth.scopes + assert String.length(token.token) > 10 + assert String.length(token.refresh_token) > 10 + + auth = Repo.get(Authorization, auth.id) + {:error, "already used"} = Token.exchange_token(app, auth) + end + + test "deletes all tokens of a user" do + {:ok, app1} = + Repo.insert( + App.register_changeset(%App{}, %{ + client_name: "client1", + scopes: ["scope"], + redirect_uris: "url" + }) + ) + + {:ok, app2} = + Repo.insert( + App.register_changeset(%App{}, %{ + client_name: "client2", + scopes: ["scope"], + redirect_uris: "url" + }) + ) + + user = insert(:user) + + {:ok, auth1} = Authorization.create_authorization(app1, user) + {:ok, auth2} = Authorization.create_authorization(app2, user) + + {:ok, _token1} = Token.exchange_token(app1, auth1) + {:ok, _token2} = Token.exchange_token(app2, auth2) + + {tokens, _} = Token.delete_user_tokens(user) + + assert tokens == 2 + end +end diff --git a/test/pleroma/web/o_status/o_status_controller_test.exs b/test/pleroma/web/o_status/o_status_controller_test.exs @@ -0,0 +1,338 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OStatus.OStatusControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Config + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Endpoint + + require Pleroma.Constants + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup do: clear_config([:instance, :federating], true) + + describe "Mastodon compatibility routes" do + setup %{conn: conn} do + conn = put_req_header(conn, "accept", "text/html") + + {:ok, object} = + %{ + "type" => "Note", + "content" => "hey", + "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999", + "actor" => Endpoint.url() <> "/users/raymoo", + "to" => [Pleroma.Constants.as_public()] + } + |> Object.create() + + {:ok, activity, _} = + %{ + "id" => object.data["id"] <> "/activity", + "type" => "Create", + "object" => object.data["id"], + "actor" => object.data["actor"], + "to" => object.data["to"] + } + |> ActivityPub.persist(local: true) + + %{conn: conn, activity: activity} + end + + test "redirects to /notice/:id for html format", %{conn: conn, activity: activity} do + conn = get(conn, "/users/raymoo/statuses/999999999") + assert redirected_to(conn) == "/notice/#{activity.id}" + end + + test "redirects to /notice/:id for html format for activity", %{ + conn: conn, + activity: activity + } do + conn = get(conn, "/users/raymoo/statuses/999999999/activity") + assert redirected_to(conn) == "/notice/#{activity.id}" + end + end + + # Note: see ActivityPubControllerTest for JSON format tests + describe "GET /objects/:uuid (text/html)" do + setup %{conn: conn} do + conn = put_req_header(conn, "accept", "text/html") + %{conn: conn} + end + + test "redirects to /notice/id for html format", %{conn: conn} do + note_activity = insert(:note_activity) + object = Object.normalize(note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) + url = "/objects/#{uuid}" + + conn = get(conn, url) + assert redirected_to(conn) == "/notice/#{note_activity.id}" + end + + test "404s on private objects", %{conn: conn} do + note_activity = insert(:direct_note_activity) + object = Object.normalize(note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) + + conn + |> get("/objects/#{uuid}") + |> response(404) + end + + test "404s on non-existing objects", %{conn: conn} do + conn + |> get("/objects/123") + |> response(404) + end + end + + # Note: see ActivityPubControllerTest for JSON format tests + describe "GET /activities/:uuid (text/html)" do + setup %{conn: conn} do + conn = put_req_header(conn, "accept", "text/html") + %{conn: conn} + end + + test "redirects to /notice/id for html format", %{conn: conn} do + note_activity = insert(:note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) + + conn = get(conn, "/activities/#{uuid}") + assert redirected_to(conn) == "/notice/#{note_activity.id}" + end + + test "404s on private activities", %{conn: conn} do + note_activity = insert(:direct_note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) + + conn + |> get("/activities/#{uuid}") + |> response(404) + end + + test "404s on nonexistent activities", %{conn: conn} do + conn + |> get("/activities/123") + |> response(404) + end + end + + describe "GET notice/2" do + test "redirects to a proper object URL when json requested and the object is local", %{ + conn: conn + } do + note_activity = insert(:note_activity) + expected_redirect_url = Object.normalize(note_activity).data["id"] + + redirect_url = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/notice/#{note_activity.id}") + |> redirected_to() + + assert redirect_url == expected_redirect_url + end + + test "returns a 404 on remote notice when json requested", %{conn: conn} do + note_activity = insert(:note_activity, local: false) + + conn + |> put_req_header("accept", "application/activity+json") + |> get("/notice/#{note_activity.id}") + |> response(404) + end + + test "500s when actor not found", %{conn: conn} do + note_activity = insert(:note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + User.invalidate_cache(user) + Pleroma.Repo.delete(user) + + conn = + conn + |> get("/notice/#{note_activity.id}") + + assert response(conn, 500) == ~S({"error":"Something went wrong"}) + end + + test "render html for redirect for html format", %{conn: conn} do + note_activity = insert(:note_activity) + + resp = + conn + |> put_req_header("accept", "text/html") + |> get("/notice/#{note_activity.id}") + |> response(200) + + assert resp =~ + "<meta content=\"#{Pleroma.Web.base_url()}/notice/#{note_activity.id}\" property=\"og:url\">" + + user = insert(:user) + + {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id) + + assert like_activity.data["type"] == "Like" + + resp = + conn + |> put_req_header("accept", "text/html") + |> get("/notice/#{like_activity.id}") + |> response(200) + + assert resp =~ "<!--server-generated-meta-->" + end + + test "404s a private notice", %{conn: conn} do + note_activity = insert(:direct_note_activity) + url = "/notice/#{note_activity.id}" + + conn = + conn + |> get(url) + + assert response(conn, 404) + end + + test "404s a non-existing notice", %{conn: conn} do + url = "/notice/123" + + conn = + conn + |> get(url) + + assert response(conn, 404) + end + + test "it requires authentication if instance is NOT federating", %{ + conn: conn + } do + user = insert(:user) + note_activity = insert(:note_activity) + + conn = put_req_header(conn, "accept", "text/html") + + ensure_federating_or_authenticated(conn, "/notice/#{note_activity.id}", user) + end + end + + describe "GET /notice/:id/embed_player" do + setup do + note_activity = insert(:note_activity) + object = Pleroma.Object.normalize(note_activity) + + object_data = + Map.put(object.data, "attachment", [ + %{ + "url" => [ + %{ + "href" => + "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + } + ]) + + object + |> Ecto.Changeset.change(data: object_data) + |> Pleroma.Repo.update() + + %{note_activity: note_activity} + end + + test "renders embed player", %{conn: conn, note_activity: note_activity} do + conn = get(conn, "/notice/#{note_activity.id}/embed_player") + + assert Plug.Conn.get_resp_header(conn, "x-frame-options") == ["ALLOW"] + + assert Plug.Conn.get_resp_header( + conn, + "content-security-policy" + ) == [ + "default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;" + ] + + assert response(conn, 200) =~ + "<video controls loop><source src=\"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4\" type=\"video/mp4\">Your browser does not support video/mp4 playback.</video>" + end + + test "404s when activity isn't create", %{conn: conn} do + note_activity = insert(:note_activity, data_attrs: %{"type" => "Like"}) + + assert conn + |> get("/notice/#{note_activity.id}/embed_player") + |> response(404) + end + + test "404s when activity is direct message", %{conn: conn} do + note_activity = insert(:note_activity, data_attrs: %{"directMessage" => true}) + + assert conn + |> get("/notice/#{note_activity.id}/embed_player") + |> response(404) + end + + test "404s when attachment is empty", %{conn: conn} do + note_activity = insert(:note_activity) + object = Pleroma.Object.normalize(note_activity) + object_data = Map.put(object.data, "attachment", []) + + object + |> Ecto.Changeset.change(data: object_data) + |> Pleroma.Repo.update() + + assert conn + |> get("/notice/#{note_activity.id}/embed_player") + |> response(404) + end + + test "404s when attachment isn't audio or video", %{conn: conn} do + note_activity = insert(:note_activity) + object = Pleroma.Object.normalize(note_activity) + + object_data = + Map.put(object.data, "attachment", [ + %{ + "url" => [ + %{ + "href" => "https://peertube.moe/static/webseed/480.jpg", + "mediaType" => "image/jpg", + "type" => "Link" + } + ] + } + ]) + + object + |> Ecto.Changeset.change(data: object_data) + |> Pleroma.Repo.update() + + conn + |> get("/notice/#{note_activity.id}/embed_player") + |> response(404) + end + + test "it requires authentication if instance is NOT federating", %{ + conn: conn, + note_activity: note_activity + } do + user = insert(:user) + conn = put_req_header(conn, "accept", "text/html") + + ensure_federating_or_authenticated(conn, "/notice/#{note_activity.id}/embed_player", user) + end + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs @@ -0,0 +1,284 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Config + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + import Swoosh.TestAssertions + + describe "POST /api/v1/pleroma/accounts/confirmation_resend" do + setup do + {:ok, user} = + insert(:user) + |> User.confirmation_changeset(need_confirmation: true) + |> User.update_and_set_cache() + + assert user.confirmation_pending + + [user: user] + end + + setup do: clear_config([:instance, :account_activation_required], true) + + test "resend account confirmation email", %{conn: conn, user: user} do + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/accounts/confirmation_resend?email=#{user.email}") + |> json_response_and_validate_schema(:no_content) + + ObanHelpers.perform_all() + + email = Pleroma.Emails.UserEmail.account_confirmation_email(user) + notify_email = Config.get([:instance, :notify_email]) + instance_name = Config.get([:instance, :name]) + + assert_email_sent( + from: {instance_name, notify_email}, + to: {user.name, user.email}, + html_body: email.html_body + ) + end + + test "resend account confirmation email (with nickname)", %{conn: conn, user: user} do + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/accounts/confirmation_resend?nickname=#{user.nickname}") + |> json_response_and_validate_schema(:no_content) + + ObanHelpers.perform_all() + + email = Pleroma.Emails.UserEmail.account_confirmation_email(user) + notify_email = Config.get([:instance, :notify_email]) + instance_name = Config.get([:instance, :name]) + + assert_email_sent( + from: {instance_name, notify_email}, + to: {user.name, user.email}, + html_body: email.html_body + ) + end + end + + describe "getting favorites timeline of specified user" do + setup do + [current_user, user] = insert_pair(:user, hide_favorites: false) + %{user: current_user, conn: conn} = oauth_access(["read:favourites"], user: current_user) + [current_user: current_user, user: user, conn: conn] + end + + test "returns list of statuses favorited by specified user", %{ + conn: conn, + user: user + } do + [activity | _] = insert_pair(:note_activity) + CommonAPI.favorite(user, activity.id) + + response = + conn + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") + |> json_response_and_validate_schema(:ok) + + [like] = response + + assert length(response) == 1 + assert like["id"] == activity.id + end + + test "returns favorites for specified user_id when requester is not logged in", %{ + user: user + } do + activity = insert(:note_activity) + CommonAPI.favorite(user, activity.id) + + response = + build_conn() + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") + |> json_response_and_validate_schema(200) + + assert length(response) == 1 + end + + test "returns favorited DM only when user is logged in and he is one of recipients", %{ + current_user: current_user, + user: user + } do + {:ok, direct} = + CommonAPI.post(current_user, %{ + status: "Hi @#{user.nickname}!", + visibility: "direct" + }) + + CommonAPI.favorite(user, direct.id) + + for u <- [user, current_user] do + response = + build_conn() + |> assign(:user, u) + |> assign(:token, insert(:oauth_token, user: u, scopes: ["read:favourites"])) + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") + |> json_response_and_validate_schema(:ok) + + assert length(response) == 1 + end + + response = + build_conn() + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") + |> json_response_and_validate_schema(200) + + assert length(response) == 0 + end + + test "does not return others' favorited DM when user is not one of recipients", %{ + conn: conn, + user: user + } do + user_two = insert(:user) + + {:ok, direct} = + CommonAPI.post(user_two, %{ + status: "Hi @#{user.nickname}!", + visibility: "direct" + }) + + CommonAPI.favorite(user, direct.id) + + response = + conn + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response) + end + + test "paginates favorites using since_id and max_id", %{ + conn: conn, + user: user + } do + activities = insert_list(10, :note_activity) + + Enum.each(activities, fn activity -> + CommonAPI.favorite(user, activity.id) + end) + + third_activity = Enum.at(activities, 2) + seventh_activity = Enum.at(activities, 6) + + response = + conn + |> get( + "/api/v1/pleroma/accounts/#{user.id}/favourites?since_id=#{third_activity.id}&max_id=#{ + seventh_activity.id + }" + ) + |> json_response_and_validate_schema(:ok) + + assert length(response) == 3 + refute third_activity in response + refute seventh_activity in response + end + + test "limits favorites using limit parameter", %{ + conn: conn, + user: user + } do + 7 + |> insert_list(:note_activity) + |> Enum.each(fn activity -> + CommonAPI.favorite(user, activity.id) + end) + + response = + conn + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites?limit=3") + |> json_response_and_validate_schema(:ok) + + assert length(response) == 3 + end + + test "returns empty response when user does not have any favorited statuses", %{ + conn: conn, + user: user + } do + response = + conn + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response) + end + + test "returns 404 error when specified user is not exist", %{conn: conn} do + conn = get(conn, "/api/v1/pleroma/accounts/test/favourites") + + assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} + end + + test "returns 403 error when user has hidden own favorites", %{conn: conn} do + user = insert(:user, hide_favorites: true) + activity = insert(:note_activity) + CommonAPI.favorite(user, activity.id) + + conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites") + + assert json_response_and_validate_schema(conn, 403) == %{"error" => "Can't get favorites"} + end + + test "hides favorites for new users by default", %{conn: conn} do + user = insert(:user) + activity = insert(:note_activity) + CommonAPI.favorite(user, activity.id) + + assert user.hide_favorites + conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites") + + assert json_response_and_validate_schema(conn, 403) == %{"error" => "Can't get favorites"} + end + end + + describe "subscribing / unsubscribing" do + test "subscribing / unsubscribing to a user" do + %{user: user, conn: conn} = oauth_access(["follow"]) + subscription_target = insert(:user) + + ret_conn = + conn + |> assign(:user, user) + |> post("/api/v1/pleroma/accounts/#{subscription_target.id}/subscribe") + + assert %{"id" => _id, "subscribing" => true} = + json_response_and_validate_schema(ret_conn, 200) + + conn = post(conn, "/api/v1/pleroma/accounts/#{subscription_target.id}/unsubscribe") + + assert %{"id" => _id, "subscribing" => false} = json_response_and_validate_schema(conn, 200) + end + end + + describe "subscribing" do + test "returns 404 when subscription_target not found" do + %{conn: conn} = oauth_access(["write:follows"]) + + conn = post(conn, "/api/v1/pleroma/accounts/target_id/subscribe") + + assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404) + end + end + + describe "unsubscribing" do + test "returns 404 when subscription_target not found" do + %{conn: conn} = oauth_access(["follow"]) + + conn = post(conn, "/api/v1/pleroma/accounts/target_id/unsubscribe") + + assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404) + 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 @@ -0,0 +1,410 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only +defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "POST /api/v1/pleroma/chats/:id/messages/:message_id/read" do + setup do: oauth_access(["write:chats"]) + + test "it marks one message as read", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup") + {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2") + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + object = Object.normalize(create, false) + cm_ref = MessageReference.for_chat_and_object(chat, object) + + assert cm_ref.unread == true + + result = + conn + |> post("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}/read") + |> json_response_and_validate_schema(200) + + assert result["unread"] == false + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + assert cm_ref.unread == false + end + end + + describe "POST /api/v1/pleroma/chats/:id/read" do + setup do: oauth_access(["write:chats"]) + + test "given a `last_read_id`, it marks everything until then as read", %{ + conn: conn, + user: user + } do + other_user = insert(:user) + + {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup") + {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2") + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + object = Object.normalize(create, false) + cm_ref = MessageReference.for_chat_and_object(chat, object) + + assert cm_ref.unread == true + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/read", %{"last_read_id" => cm_ref.id}) + |> json_response_and_validate_schema(200) + + assert result["unread"] == 1 + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + assert cm_ref.unread == false + end + end + + describe "POST /api/v1/pleroma/chats/:id/messages" do + setup do: oauth_access(["write:chats"]) + + test "it posts a message to the chat", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "Hallo!!"}) + |> json_response_and_validate_schema(200) + + assert result["content"] == "Hallo!!" + assert result["chat_id"] == chat.id |> to_string() + end + + test "it fails if there is no content", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/messages") + |> json_response_and_validate_schema(400) + + assert %{"error" => "no_content"} == result + end + + test "it works with an attachment", %{conn: conn, user: user} do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{ + "media_id" => to_string(upload.id) + }) + |> json_response_and_validate_schema(200) + + assert result["attachment"] + end + + test "gets MRF reason when rejected", %{conn: conn, user: user} do + clear_config([:mrf_keyword, :reject], ["GNO"]) + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) + + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "GNO/Linux"}) + |> json_response_and_validate_schema(422) + + assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} == result + end + end + + describe "DELETE /api/v1/pleroma/chats/:id/messages/:message_id" do + setup do: oauth_access(["write:chats"]) + + test "it deletes a message from the chat", %{conn: conn, user: user} do + recipient = insert(:user) + + {:ok, message} = + CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend") + + {:ok, other_message} = CommonAPI.post_chat_message(recipient, user, "nico nico ni") + + object = Object.normalize(message, false) + + chat = Chat.get(user.id, recipient.ap_id) + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + # Deleting your own message removes the message and the reference + result = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] == cm_ref.id + refute MessageReference.get_by_id(cm_ref.id) + assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id) + + # Deleting other people's messages just removes the reference + object = Object.normalize(other_message, false) + cm_ref = MessageReference.for_chat_and_object(chat, object) + + result = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] == cm_ref.id + refute MessageReference.get_by_id(cm_ref.id) + assert Object.get_by_id(object.id) + end + end + + describe "GET /api/v1/pleroma/chats/:id/messages" do + setup do: oauth_access(["read:chats"]) + + test "it paginates", %{conn: conn, user: user} do + recipient = insert(:user) + + Enum.each(1..30, fn _ -> + {:ok, _} = CommonAPI.post_chat_message(user, recipient, "hey") + end) + + chat = Chat.get(user.id, recipient.ap_id) + + response = get(conn, "/api/v1/pleroma/chats/#{chat.id}/messages") + result = json_response_and_validate_schema(response, 200) + + [next, prev] = get_resp_header(response, "link") |> hd() |> String.split(", ") + api_endpoint = "/api/v1/pleroma/chats/" + + assert String.match?( + next, + ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&max_id=.*; rel=\"next\"$) + ) + + assert String.match?( + prev, + ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&min_id=.*; rel=\"prev\"$) + ) + + assert length(result) == 20 + + response = + get(conn, "/api/v1/pleroma/chats/#{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(", ") + + assert String.match?( + next, + ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&max_id=.*; rel=\"next\"$) + ) + + assert String.match?( + prev, + ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&max_id=.*&min_id=.*; rel=\"prev\"$) + ) + + assert length(result) == 10 + end + + test "it returns the messages for a given chat", %{conn: conn, user: user} do + other_user = insert(:user) + third_user = insert(:user) + + {:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey") + {:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey") + {:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?") + {:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?") + + chat = Chat.get(user.id, other_user.ap_id) + + result = + conn + |> get("/api/v1/pleroma/chats/#{chat.id}/messages") + |> json_response_and_validate_schema(200) + + result + |> Enum.each(fn message -> + assert message["chat_id"] == chat.id |> to_string() + end) + + assert length(result) == 3 + + # Trying to get the chat of a different user + conn + |> assign(:user, other_user) + |> get("/api/v1/pleroma/chats/#{chat.id}/messages") + |> json_response_and_validate_schema(404) + end + end + + describe "POST /api/v1/pleroma/chats/by-account-id/:id" do + setup do: oauth_access(["write:chats"]) + + test "it creates or returns a chat", %{conn: conn} do + other_user = insert(:user) + + result = + conn + |> post("/api/v1/pleroma/chats/by-account-id/#{other_user.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] + end + end + + describe "GET /api/v1/pleroma/chats/:id" do + setup do: oauth_access(["read:chats"]) + + test "it returns a chat", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> get("/api/v1/pleroma/chats/#{chat.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] == to_string(chat.id) + end + end + + describe "GET /api/v1/pleroma/chats" do + setup do: oauth_access(["read:chats"]) + + test "it does not return chats with deleted users", %{conn: conn, user: user} do + recipient = insert(:user) + {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) + + Pleroma.Repo.delete(recipient) + User.invalidate_cache(recipient) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + assert length(result) == 0 + end + + test "it does not return chats with users you blocked", %{conn: conn, user: user} do + recipient = insert(:user) + + {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + assert length(result) == 1 + + User.block(user, recipient) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + assert length(result) == 0 + end + + test "it returns all chats", %{conn: conn, user: user} do + Enum.each(1..30, fn _ -> + recipient = insert(:user) + {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) + end) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + assert length(result) == 30 + end + + test "it return a list of chats the current user is participating in, in descending order of updates", + %{conn: conn, user: user} do + har = insert(:user) + jafnhar = insert(:user) + tridi = insert(:user) + + {:ok, chat_1} = Chat.get_or_create(user.id, har.ap_id) + :timer.sleep(1000) + {:ok, _chat_2} = Chat.get_or_create(user.id, jafnhar.ap_id) + :timer.sleep(1000) + {:ok, chat_3} = Chat.get_or_create(user.id, tridi.ap_id) + :timer.sleep(1000) + + # bump the second one + {:ok, chat_2} = Chat.bump_or_create(user.id, jafnhar.ap_id) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + ids = Enum.map(result, & &1["id"]) + + assert ids == [ + chat_2.id |> to_string(), + chat_3.id |> to_string(), + chat_1.id |> to_string() + ] + end + + test "it is not affected by :restrict_unauthenticated setting (issue #1973)", %{ + conn: conn, + user: user + } do + clear_config([:restrict_unauthenticated, :profiles, :local], true) + clear_config([:restrict_unauthenticated, :profiles, :remote], true) + + user2 = insert(:user) + user3 = insert(:user, local: false) + + {:ok, _chat_12} = Chat.get_or_create(user.id, user2.ap_id) + {:ok, _chat_13} = Chat.get_or_create(user.id, user3.ap_id) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + account_ids = Enum.map(result, &get_in(&1, ["account", "id"])) + assert Enum.sort(account_ids) == Enum.sort([user2.id, user3.id]) + end + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/conversation_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/conversation_controller_test.exs @@ -0,0 +1,136 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ConversationControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Conversation.Participation + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "/api/v1/pleroma/conversations/:id" do + user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["read:statuses"]) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"}) + + [participation] = Participation.for_user(other_user) + + result = + conn + |> get("/api/v1/pleroma/conversations/#{participation.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] == participation.id |> to_string() + end + + test "/api/v1/pleroma/conversations/:id/statuses" do + user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["read:statuses"]) + third_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "Hi @#{third_user.nickname}!", visibility: "direct"}) + + {:ok, activity} = + CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"}) + + [participation] = Participation.for_user(other_user) + + {:ok, activity_two} = + CommonAPI.post(other_user, %{ + status: "Hi!", + in_reply_to_status_id: activity.id, + in_reply_to_conversation_id: participation.id + }) + + result = + conn + |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses") + |> json_response_and_validate_schema(200) + + assert length(result) == 2 + + id_one = activity.id + id_two = activity_two.id + assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result + + {:ok, %{id: id_three}} = + CommonAPI.post(other_user, %{ + status: "Bye!", + in_reply_to_status_id: activity.id, + in_reply_to_conversation_id: participation.id + }) + + assert [%{"id" => ^id_two}, %{"id" => ^id_three}] = + conn + |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?limit=2") + |> json_response_and_validate_schema(:ok) + + assert [%{"id" => ^id_three}] = + conn + |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?min_id=#{id_two}") + |> json_response_and_validate_schema(:ok) + end + + test "PATCH /api/v1/pleroma/conversations/:id" do + %{user: user, conn: conn} = oauth_access(["write:conversations"]) + other_user = insert(:user) + + {:ok, _activity} = CommonAPI.post(user, %{status: "Hi", visibility: "direct"}) + + [participation] = Participation.for_user(user) + + participation = Repo.preload(participation, :recipients) + + user = User.get_cached_by_id(user.id) + assert [user] == participation.recipients + assert other_user not in participation.recipients + + query = "recipients[]=#{user.id}&recipients[]=#{other_user.id}" + + result = + conn + |> patch("/api/v1/pleroma/conversations/#{participation.id}?#{query}") + |> json_response_and_validate_schema(200) + + assert result["id"] == participation.id |> to_string + + [participation] = Participation.for_user(user) + participation = Repo.preload(participation, :recipients) + + assert user in participation.recipients + assert other_user in participation.recipients + end + + test "POST /api/v1/pleroma/conversations/read" do + user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["write:conversations"]) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"}) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"}) + + [participation2, participation1] = Participation.for_user(other_user) + assert Participation.get(participation2.id).read == false + assert Participation.get(participation1.id).read == false + assert User.get_cached_by_id(other_user.id).unread_conversation_count == 2 + + [%{"unread" => false}, %{"unread" => false}] = + conn + |> post("/api/v1/pleroma/conversations/read", %{}) + |> json_response_and_validate_schema(200) + + [participation2, participation1] = Participation.for_user(other_user) + assert Participation.get(participation2.id).read == true + assert Participation.get(participation1.id).read == true + assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0 + end +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 @@ -0,0 +1,357 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.EmojiFileControllerTest do + use Pleroma.Web.ConnCase + + import Tesla.Mock + import Pleroma.Factory + + @emoji_path Path.join( + Pleroma.Config.get!([:instance, :static_dir]), + "emoji" + ) + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) + + setup do: clear_config([:instance, :public], true) + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + admin_conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + Pleroma.Emoji.reload() + {:ok, %{admin_conn: admin_conn}} + end + + describe "POST/PATCH/DELETE /api/pleroma/emoji/packs/files?name=:name" do + setup do + pack_file = "#{@emoji_path}/test_pack/pack.json" + original_content = File.read!(pack_file) + + on_exit(fn -> + File.write!(pack_file, original_content) + end) + + :ok + end + + test "upload zip file with emojies", %{admin_conn: admin_conn} do + on_exit(fn -> + [ + "128px/a_trusted_friend-128.png", + "auroraborealis.png", + "1000px/baby_in_a_box.png", + "1000px/bear.png", + "128px/bear-128.png" + ] + |> Enum.each(fn path -> File.rm_rf!("#{@emoji_path}/test_pack/#{path}") end) + end) + + resp = + admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ + file: %Plug.Upload{ + content_type: "application/zip", + filename: "emojis.zip", + path: Path.absname("test/fixtures/emojis.zip") + } + }) + |> json_response_and_validate_schema(200) + + assert resp == %{ + "a_trusted_friend-128" => "128px/a_trusted_friend-128.png", + "auroraborealis" => "auroraborealis.png", + "baby_in_a_box" => "1000px/baby_in_a_box.png", + "bear" => "1000px/bear.png", + "bear-128" => "128px/bear-128.png", + "blank" => "blank.png", + "blank2" => "blank2.png" + } + + Enum.each(Map.values(resp), fn path -> + assert File.exists?("#{@emoji_path}/test_pack/#{path}") + end) + end + + test "create shortcode exists", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(:conflict) == %{ + "error" => "An emoji with the \"blank\" shortcode already exists" + } + end + + test "don't rewrite old emoji", %{admin_conn: admin_conn} do + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir/") end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank3", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank2" => "blank2.png", + "blank3" => "dir/blank.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank", + new_shortcode: "blank2", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(:conflict) == %{ + "error" => + "New shortcode \"blank2\" is already used. If you want to override emoji use 'force' option" + } + end + + test "rewrite old emoji with force option", %{admin_conn: admin_conn} do + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir_2/") end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank3", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank2" => "blank2.png", + "blank3" => "dir/blank.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank3", + new_shortcode: "blank4", + new_filename: "dir_2/blank_3.png", + force: true + }) + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank2" => "blank2.png", + "blank4" => "dir_2/blank_3.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") + end + + test "with empty filename", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank2", + filename: "", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(422) == %{ + "error" => "pack name, shortcode or filename cannot be empty" + } + end + + test "add file with not loaded pack", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=not_loaded", %{ + shortcode: "blank3", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "pack \"not_loaded\" is not found" + } + end + + test "remove file with not loaded pack", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/packs/files?name=not_loaded&shortcode=blank3") + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "pack \"not_loaded\" is not found" + } + end + + test "remove file with empty shortcode", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/packs/files?name=not_loaded&shortcode=") + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "pack \"not_loaded\" is not found" + } + end + + test "update file with not loaded pack", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/files?name=not_loaded", %{ + shortcode: "blank4", + new_shortcode: "blank3", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "pack \"not_loaded\" is not found" + } + end + + test "new with shortcode as file with update", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank4", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank4" => "dir/blank.png", + "blank2" => "blank2.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank4", + new_shortcode: "blank3", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(200) == %{ + "blank3" => "dir_2/blank_3.png", + "blank" => "blank.png", + "blank2" => "blank2.png" + } + + refute File.exists?("#{@emoji_path}/test_pack/dir/") + assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") + + assert admin_conn + |> delete("/api/pleroma/emoji/packs/files?name=test_pack&shortcode=blank3") + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank2" => "blank2.png" + } + + refute File.exists?("#{@emoji_path}/test_pack/dir_2/") + + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir") end) + end + + test "new with shortcode from url", %{admin_conn: admin_conn} do + mock(fn + %{ + method: :get, + url: "https://test-blank/blank_url.png" + } -> + text(File.read!("#{@emoji_path}/test_pack/blank.png")) + end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank_url", + file: "https://test-blank/blank_url.png" + }) + |> json_response_and_validate_schema(200) == %{ + "blank_url" => "blank_url.png", + "blank" => "blank.png", + "blank2" => "blank2.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/blank_url.png") + + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/blank_url.png") end) + end + + test "new without shortcode", %{admin_conn: admin_conn} do + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/shortcode.png") end) + + 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_and_validate_schema(200) == %{ + "shortcode" => "shortcode.png", + "blank" => "blank.png", + "blank2" => "blank2.png" + } + end + + test "remove non existing shortcode in pack.json", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/packs/files?name=test_pack&shortcode=blank3") + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "Emoji \"blank3\" does not exist" + } + end + + test "update non existing emoji", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank3", + new_shortcode: "blank4", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "Emoji \"blank3\" does not exist" + } + end + + test "update with empty shortcode", %{admin_conn: admin_conn} do + assert %{ + "error" => "Missing field: new_shortcode." + } = + 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_and_validate_schema(:bad_request) + 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 @@ -0,0 +1,604 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do + use Pleroma.Web.ConnCase + + import Tesla.Mock + import Pleroma.Factory + + @emoji_path Path.join( + Pleroma.Config.get!([:instance, :static_dir]), + "emoji" + ) + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) + + setup do: clear_config([:instance, :public], true) + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + admin_conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + Pleroma.Emoji.reload() + {:ok, %{admin_conn: admin_conn}} + end + + test "GET /api/pleroma/emoji/packs when :public: false", %{conn: conn} do + Config.put([:instance, :public], false) + conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) + end + + test "GET /api/pleroma/emoji/packs", %{conn: conn} do + resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) + + assert resp["count"] == 4 + + assert resp["packs"] + |> Map.keys() + |> length() == 4 + + shared = resp["packs"]["test_pack"] + assert shared["files"] == %{"blank" => "blank.png", "blank2" => "blank2.png"} + assert Map.has_key?(shared["pack"], "download-sha256") + assert shared["pack"]["can-download"] + assert shared["pack"]["share-files"] + + non_shared = resp["packs"]["test_pack_nonshared"] + assert non_shared["pack"]["share-files"] == false + assert non_shared["pack"]["can-download"] == false + + resp = + conn + |> get("/api/pleroma/emoji/packs?page_size=1") + |> json_response_and_validate_schema(200) + + assert resp["count"] == 4 + + packs = Map.keys(resp["packs"]) + + assert length(packs) == 1 + + [pack1] = packs + + resp = + conn + |> get("/api/pleroma/emoji/packs?page_size=1&page=2") + |> json_response_and_validate_schema(200) + + assert resp["count"] == 4 + packs = Map.keys(resp["packs"]) + assert length(packs) == 1 + [pack2] = packs + + resp = + conn + |> get("/api/pleroma/emoji/packs?page_size=1&page=3") + |> json_response_and_validate_schema(200) + + assert resp["count"] == 4 + packs = Map.keys(resp["packs"]) + assert length(packs) == 1 + [pack3] = packs + + resp = + conn + |> get("/api/pleroma/emoji/packs?page_size=1&page=4") + |> json_response_and_validate_schema(200) + + assert resp["count"] == 4 + packs = Map.keys(resp["packs"]) + assert length(packs) == 1 + [pack4] = packs + assert [pack1, pack2, pack3, pack4] |> Enum.uniq() |> length() == 4 + end + + describe "GET /api/pleroma/emoji/packs/remote" do + test "shareable instance", %{admin_conn: admin_conn, conn: conn} do + resp = + conn + |> get("/api/pleroma/emoji/packs?page=2&page_size=1") + |> json_response_and_validate_schema(200) + + mock(fn + %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) + + %{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"} -> + json(resp) + end) + + assert admin_conn + |> get("/api/pleroma/emoji/packs/remote?url=https://example.com&page=2&page_size=1") + |> json_response_and_validate_schema(200) == resp + end + + test "non shareable instance", %{admin_conn: admin_conn} do + mock(fn + %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) + + %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> + json(%{metadata: %{features: []}}) + end) + + assert admin_conn + |> get("/api/pleroma/emoji/packs/remote?url=https://example.com") + |> json_response_and_validate_schema(500) == %{ + "error" => "The requested instance does not support sharing emoji packs" + } + end + end + + describe "GET /api/pleroma/emoji/packs/archive?name=:name" do + test "download shared pack", %{conn: conn} do + resp = + conn + |> get("/api/pleroma/emoji/packs/archive?name=test_pack") + |> response(200) + + {:ok, arch} = :zip.unzip(resp, [:memory]) + + assert Enum.find(arch, fn {n, _} -> n == 'pack.json' end) + assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end) + end + + test "non existing pack", %{conn: conn} do + assert conn + |> get("/api/pleroma/emoji/packs/archive?name=test_pack_for_import") + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "Pack test_pack_for_import does not exist" + } + end + + test "non downloadable pack", %{conn: conn} do + assert conn + |> get("/api/pleroma/emoji/packs/archive?name=test_pack_nonshared") + |> json_response_and_validate_schema(:forbidden) == %{ + "error" => + "Pack test_pack_nonshared cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing" + } + end + end + + describe "POST /api/pleroma/emoji/packs/download" do + test "shared pack from remote and non shared from fallback-src", %{ + admin_conn: admin_conn, + conn: conn + } do + mock(fn + %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) + + %{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/pack?name=test_pack" + } -> + conn + |> get("/api/pleroma/emoji/pack?name=test_pack") + |> json_response_and_validate_schema(200) + |> json() + + %{ + method: :get, + url: "https://example.com/api/pleroma/emoji/packs/archive?name=test_pack" + } -> + conn + |> get("/api/pleroma/emoji/packs/archive?name=test_pack") + |> response(200) + |> text() + + %{ + method: :get, + url: "https://example.com/api/pleroma/emoji/pack?name=test_pack_nonshared" + } -> + conn + |> get("/api/pleroma/emoji/pack?name=test_pack_nonshared") + |> json_response_and_validate_schema(200) + |> json() + + %{ + method: :get, + url: "https://nonshared-pack" + } -> + text(File.read!("#{@emoji_path}/test_pack_nonshared/nonshared.zip")) + end) + + assert admin_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_and_validate_schema(200) == "ok" + + assert File.exists?("#{@emoji_path}/test_pack2/pack.json") + assert File.exists?("#{@emoji_path}/test_pack2/blank.png") + + assert admin_conn + |> delete("/api/pleroma/emoji/pack?name=test_pack2") + |> json_response_and_validate_schema(200) == "ok" + + refute File.exists?("#{@emoji_path}/test_pack2") + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post( + "/api/pleroma/emoji/packs/download", + %{ + url: "https://example.com", + name: "test_pack_nonshared", + as: "test_pack_nonshared2" + } + ) + |> json_response_and_validate_schema(200) == "ok" + + assert File.exists?("#{@emoji_path}/test_pack_nonshared2/pack.json") + assert File.exists?("#{@emoji_path}/test_pack_nonshared2/blank.png") + + assert admin_conn + |> delete("/api/pleroma/emoji/pack?name=test_pack_nonshared2") + |> json_response_and_validate_schema(200) == "ok" + + refute File.exists?("#{@emoji_path}/test_pack_nonshared2") + end + + test "nonshareable instance", %{admin_conn: admin_conn} do + mock(fn + %{method: :get, url: "https://old-instance/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://old-instance/nodeinfo/2.1.json"}]}) + + %{method: :get, url: "https://old-instance/nodeinfo/2.1.json"} -> + json(%{metadata: %{features: []}}) + end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post( + "/api/pleroma/emoji/packs/download", + %{ + url: "https://old-instance", + name: "test_pack", + as: "test_pack2" + } + ) + |> json_response_and_validate_schema(500) == %{ + "error" => "The requested instance does not support sharing emoji packs" + } + end + + test "checksum fail", %{admin_conn: admin_conn} do + mock(fn + %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) + + %{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/pack?name=pack_bad_sha" + } -> + {: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" + } -> + %Tesla.Env{ + status: 200, + body: File.read!("test/instance_static/emoji/pack_bad_sha/pack_bad_sha.zip") + } + end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download", %{ + url: "https://example.com", + name: "pack_bad_sha", + as: "pack_bad_sha2" + }) + |> json_response_and_validate_schema(:internal_server_error) == %{ + "error" => "SHA256 for the pack doesn't match the one sent by the server" + } + end + + test "other error", %{admin_conn: admin_conn} do + mock(fn + %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) + + %{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/pack?name=test_pack" + } -> + {:ok, pack} = Pleroma.Emoji.Pack.load_pack("test_pack") + %Tesla.Env{status: 200, body: Jason.encode!(pack)} + end) + + assert admin_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_and_validate_schema(:internal_server_error) == %{ + "error" => + "The pack was not set as shared and there is no fallback src to download from" + } + end + end + + describe "PATCH /api/pleroma/emoji/pack?name=:name" do + setup do + pack_file = "#{@emoji_path}/test_pack/pack.json" + original_content = File.read!(pack_file) + + on_exit(fn -> + File.write!(pack_file, original_content) + end) + + {:ok, + pack_file: pack_file, + new_data: %{ + "license" => "Test license changed", + "homepage" => "https://pleroma.social", + "description" => "Test description", + "share-files" => false + }} + end + + test "for a pack without a fallback source", ctx do + assert ctx[:admin_conn] + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/pack?name=test_pack", %{ + "metadata" => ctx[:new_data] + }) + |> json_response_and_validate_schema(200) == ctx[:new_data] + + assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == ctx[:new_data] + end + + test "for a pack with a fallback source", ctx do + mock(fn + %{ + method: :get, + url: "https://nonshared-pack" + } -> + text(File.read!("#{@emoji_path}/test_pack_nonshared/nonshared.zip")) + end) + + new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack") + + new_data_with_sha = + Map.put( + new_data, + "fallback-src-sha256", + "1967BB4E42BCC34BCC12D57BE7811D3B7BE52F965BCE45C87BD377B9499CE11D" + ) + + assert ctx[:admin_conn] + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/pack?name=test_pack", %{metadata: new_data}) + |> json_response_and_validate_schema(200) == new_data_with_sha + + assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == new_data_with_sha + end + + test "when the fallback source doesn't have all the files", ctx do + mock(fn + %{ + method: :get, + url: "https://nonshared-pack" + } -> + {:ok, {'empty.zip', empty_arch}} = :zip.zip('empty.zip', [], [:memory]) + text(empty_arch) + end) + + new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack") + + assert ctx[:admin_conn] + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/pack?name=test_pack", %{metadata: new_data}) + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "The fallback archive does not have all files specified in pack.json" + } + end + end + + describe "POST/DELETE /api/pleroma/emoji/pack?name=:name" do + test "creating and deleting a pack", %{admin_conn: admin_conn} do + assert admin_conn + |> post("/api/pleroma/emoji/pack?name=test_created") + |> json_response_and_validate_schema(200) == "ok" + + assert File.exists?("#{@emoji_path}/test_created/pack.json") + + assert Jason.decode!(File.read!("#{@emoji_path}/test_created/pack.json")) == %{ + "pack" => %{}, + "files" => %{}, + "files_count" => 0 + } + + assert admin_conn + |> delete("/api/pleroma/emoji/pack?name=test_created") + |> json_response_and_validate_schema(200) == "ok" + + refute File.exists?("#{@emoji_path}/test_created/pack.json") + end + + test "if pack exists", %{admin_conn: admin_conn} do + path = Path.join(@emoji_path, "test_created") + File.mkdir(path) + pack_file = Jason.encode!(%{files: %{}, pack: %{}}) + File.write!(Path.join(path, "pack.json"), pack_file) + + assert admin_conn + |> post("/api/pleroma/emoji/pack?name=test_created") + |> json_response_and_validate_schema(:conflict) == %{ + "error" => "A pack named \"test_created\" already exists" + } + + on_exit(fn -> File.rm_rf(path) end) + end + + test "with empty name", %{admin_conn: admin_conn} do + assert admin_conn + |> post("/api/pleroma/emoji/pack?name= ") + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack name cannot be empty" + } + end + end + + test "deleting nonexisting pack", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/pack?name=non_existing") + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "Pack non_existing does not exist" + } + end + + test "deleting with empty name", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/pack?name= ") + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack name cannot be empty" + } + end + + test "filesystem import", %{admin_conn: admin_conn, conn: conn} do + on_exit(fn -> + File.rm!("#{@emoji_path}/test_pack_for_import/emoji.txt") + File.rm!("#{@emoji_path}/test_pack_for_import/pack.json") + end) + + resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) + + refute Map.has_key?(resp["packs"], "test_pack_for_import") + + assert admin_conn + |> get("/api/pleroma/emoji/packs/import") + |> json_response_and_validate_schema(200) == ["test_pack_for_import"] + + resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) + assert resp["packs"]["test_pack_for_import"]["files"] == %{"blank" => "blank.png"} + + File.rm!("#{@emoji_path}/test_pack_for_import/pack.json") + refute File.exists?("#{@emoji_path}/test_pack_for_import/pack.json") + + emoji_txt_content = """ + blank, blank.png, Fun + blank2, blank.png + foo, /emoji/test_pack_for_import/blank.png + bar + """ + + File.write!("#{@emoji_path}/test_pack_for_import/emoji.txt", emoji_txt_content) + + assert admin_conn + |> get("/api/pleroma/emoji/packs/import") + |> json_response_and_validate_schema(200) == ["test_pack_for_import"] + + resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) + + assert resp["packs"]["test_pack_for_import"]["files"] == %{ + "blank" => "blank.png", + "blank2" => "blank.png", + "foo" => "blank.png" + } + end + + describe "GET /api/pleroma/emoji/pack?name=:name" do + test "shows pack.json", %{conn: conn} do + assert %{ + "files" => files, + "files_count" => 2, + "pack" => %{ + "can-download" => true, + "description" => "Test description", + "download-sha256" => _, + "homepage" => "https://pleroma.social", + "license" => "Test license", + "share-files" => true + } + } = + conn + |> get("/api/pleroma/emoji/pack?name=test_pack") + |> json_response_and_validate_schema(200) + + assert files == %{"blank" => "blank.png", "blank2" => "blank2.png"} + + assert %{ + "files" => files, + "files_count" => 2 + } = + conn + |> get("/api/pleroma/emoji/pack?name=test_pack&page_size=1") + |> json_response_and_validate_schema(200) + + assert files |> Map.keys() |> length() == 1 + + assert %{ + "files" => files, + "files_count" => 2 + } = + conn + |> get("/api/pleroma/emoji/pack?name=test_pack&page_size=1&page=2") + |> json_response_and_validate_schema(200) + + assert files |> Map.keys() |> length() == 1 + end + + test "for pack name with special chars", %{conn: conn} do + assert %{ + "files" => files, + "files_count" => 1, + "pack" => %{ + "can-download" => true, + "description" => "Test description", + "download-sha256" => _, + "homepage" => "https://pleroma.social", + "license" => "Test license", + "share-files" => true + } + } = + conn + |> get("/api/pleroma/emoji/pack?name=blobs.gg") + |> json_response_and_validate_schema(200) + end + + test "non existing pack", %{conn: conn} do + assert conn + |> get("/api/pleroma/emoji/pack?name=non_existing") + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "Pack non_existing does not exist" + } + end + + test "error name", %{conn: conn} do + assert conn + |> get("/api/pleroma/emoji/pack?name= ") + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack name cannot be empty" + } + end + end +end 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 @@ -0,0 +1,149 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.Web.ConnCase + + alias Pleroma.Object + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + + 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/☕") + |> 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} + ] + + # Reacting with a non-emoji + assert conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) + |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/x") + |> json_response_and_validate_schema(400) + end + + test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + + ObanHelpers.perform_all() + + 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/☕") + + 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 + end + + test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + doomed_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + + result = + conn + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") + |> json_response_and_validate_schema(200) + + assert result == [] + + {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅") + + User.perform(:delete, doomed_user) + + result = + conn + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") + |> json_response_and_validate_schema(200) + + [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result + + assert represented_user["id"] == other_user.id + + result = + conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:statuses"])) + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") + |> json_response_and_validate_schema(200) + + assert [%{"name" => "🎅", "count" => 1, "accounts" => [_represented_user], "me" => true}] = + result + end + + test "GET /api/v1/pleroma/statuses/:id/reactions with :show_reactions disabled", %{conn: conn} do + clear_config([:instance, :show_reactions], false) + + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") + + result = + conn + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") + |> json_response_and_validate_schema(200) + + assert result == [] + end + + test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + + result = + conn + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅") + |> json_response_and_validate_schema(200) + + assert result == [] + + {: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}] = + conn + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅") + |> json_response_and_validate_schema(200) + + assert represented_user["id"] == other_user.id + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs @@ -0,0 +1,73 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.MascotControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.User + + test "mascot upload" do + %{conn: conn} = oauth_access(["write:accounts"]) + + non_image_file = %Plug.Upload{ + content_type: "audio/mpeg", + path: Path.absname("test/fixtures/sound.mp3"), + filename: "sound.mp3" + } + + ret_conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> put("/api/v1/pleroma/mascot", %{"file" => non_image_file}) + + assert json_response_and_validate_schema(ret_conn, 415) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> put("/api/v1/pleroma/mascot", %{"file" => file}) + + assert %{"id" => _, "type" => image} = json_response_and_validate_schema(conn, 200) + end + + test "mascot retrieving" do + %{user: user, conn: conn} = oauth_access(["read:accounts", "write:accounts"]) + + # When user hasn't set a mascot, we should just get pleroma tan back + ret_conn = get(conn, "/api/v1/pleroma/mascot") + + assert %{"url" => url} = json_response_and_validate_schema(ret_conn, 200) + assert url =~ "pleroma-fox-tan-smol" + + # When a user sets their mascot, we should get that back + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + ret_conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> put("/api/v1/pleroma/mascot", %{"file" => file}) + + assert json_response_and_validate_schema(ret_conn, 200) + + user = User.get_cached_by_id(user.id) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/pleroma/mascot") + + assert %{"url" => url, "type" => "image"} = json_response_and_validate_schema(conn, 200) + assert url =~ "an_image" + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/notification_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/notification_controller_test.exs @@ -0,0 +1,68 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.NotificationControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Notification + alias Pleroma.Repo + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "POST /api/v1/pleroma/notifications/read" do + setup do: oauth_access(["write:notifications"]) + + test "it marks a single notification as read", %{user: user1, conn: conn} do + user2 = insert(:user) + {:ok, activity1} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"}) + {:ok, activity2} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"}) + {: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) + + assert %{"pleroma" => %{"is_seen" => true}} = response + assert Repo.get(Notification, notification1.id).seen + refute Repo.get(Notification, notification2.id).seen + end + + test "it marks multiple notifications as read", %{user: user1, conn: conn} do + user2 = insert(:user) + {:ok, _activity1} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"}) + {:ok, _activity2} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"}) + {:ok, _activity3} = CommonAPI.post(user2, %{status: "HIE @#{user1.nickname}"}) + + [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) + + 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 + end + + test "it returns error when notification not found", %{conn: conn} do + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/notifications/read", %{ + id: 22_222_222_222_222 + }) + |> json_response_and_validate_schema(:bad_request) + + assert response == %{"error" => "Cannot get notification"} + end + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs @@ -0,0 +1,60 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.CommonAPI + + describe "POST /api/v1/pleroma/scrobble" do + test "works correctly" do + %{conn: conn} = oauth_access(["write"]) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/scrobble", %{ + "title" => "lain radio episode 1", + "artist" => "lain", + "album" => "lain radio", + "length" => "180000" + }) + + assert %{"title" => "lain radio episode 1"} = json_response_and_validate_schema(conn, 200) + end + end + + describe "GET /api/v1/pleroma/accounts/:id/scrobbles" do + test "works correctly" do + %{user: user, conn: conn} = oauth_access(["read"]) + + {:ok, _activity} = + CommonAPI.listen(user, %{ + title: "lain radio episode 1", + artist: "lain", + album: "lain radio" + }) + + {:ok, _activity} = + CommonAPI.listen(user, %{ + title: "lain radio episode 2", + artist: "lain", + album: "lain radio" + }) + + {:ok, _activity} = + CommonAPI.listen(user, %{ + title: "lain radio episode 3", + artist: "lain", + album: "lain radio" + }) + + conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/scrobbles") + + result = json_response_and_validate_schema(conn, 200) + + assert length(result) == 3 + end + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs @@ -0,0 +1,264 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + alias Pleroma.MFA.Settings + alias Pleroma.MFA.TOTP + + describe "GET /api/pleroma/accounts/mfa/settings" do + test "returns user mfa settings for new user", %{conn: conn} do + token = insert(:oauth_token, scopes: ["read", "follow"]) + token2 = insert(:oauth_token, scopes: ["write"]) + + assert conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/pleroma/accounts/mfa") + |> json_response(:ok) == %{ + "settings" => %{"enabled" => false, "totp" => false} + } + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> get("/api/pleroma/accounts/mfa") + |> json_response(403) == %{ + "error" => "Insufficient permissions: read:security." + } + end + + test "returns user mfa settings with enabled totp", %{conn: conn} do + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + enabled: true, + totp: %Settings.TOTP{secret: "XXX", delivery_type: "app", confirmed: true} + } + ) + + token = insert(:oauth_token, scopes: ["read", "follow"], user: user) + + assert conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/pleroma/accounts/mfa") + |> json_response(:ok) == %{ + "settings" => %{"enabled" => true, "totp" => true} + } + end + end + + describe "GET /api/pleroma/accounts/mfa/backup_codes" do + test "returns backup codes", %{conn: conn} do + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + backup_codes: ["1", "2", "3"], + totp: %Settings.TOTP{secret: "secret"} + } + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + token2 = insert(:oauth_token, scopes: ["read"]) + + response = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/pleroma/accounts/mfa/backup_codes") + |> json_response(:ok) + + assert [<<_::bytes-size(6)>>, <<_::bytes-size(6)>>] = response["codes"] + user = refresh_record(user) + mfa_settings = user.multi_factor_authentication_settings + assert mfa_settings.totp.secret == "secret" + refute mfa_settings.backup_codes == ["1", "2", "3"] + refute mfa_settings.backup_codes == [] + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> get("/api/pleroma/accounts/mfa/backup_codes") + |> json_response(403) == %{ + "error" => "Insufficient permissions: write:security." + } + end + end + + describe "GET /api/pleroma/accounts/mfa/setup/totp" do + test "return errors when method is invalid", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + + response = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/pleroma/accounts/mfa/setup/torf") + |> json_response(400) + + assert response == %{"error" => "undefined method"} + end + + test "returns key and provisioning_uri", %{conn: conn} do + user = + insert(:user, + multi_factor_authentication_settings: %Settings{backup_codes: ["1", "2", "3"]} + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + token2 = insert(:oauth_token, scopes: ["read"]) + + response = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/pleroma/accounts/mfa/setup/totp") + |> json_response(:ok) + + user = refresh_record(user) + mfa_settings = user.multi_factor_authentication_settings + secret = mfa_settings.totp.secret + refute mfa_settings.enabled + assert mfa_settings.backup_codes == ["1", "2", "3"] + + assert response == %{ + "key" => secret, + "provisioning_uri" => TOTP.provisioning_uri(secret, "#{user.email}") + } + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> get("/api/pleroma/accounts/mfa/setup/totp") + |> json_response(403) == %{ + "error" => "Insufficient permissions: write:security." + } + end + end + + describe "GET /api/pleroma/accounts/mfa/confirm/totp" do + test "returns success result", %{conn: conn} do + secret = TOTP.generate_secret() + code = TOTP.generate_token(secret) + + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + backup_codes: ["1", "2", "3"], + totp: %Settings.TOTP{secret: secret} + } + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + token2 = insert(:oauth_token, scopes: ["read"]) + + assert conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: code}) + |> json_response(:ok) + + settings = refresh_record(user).multi_factor_authentication_settings + assert settings.enabled + assert settings.totp.secret == secret + assert settings.totp.confirmed + assert settings.backup_codes == ["1", "2", "3"] + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: code}) + |> json_response(403) == %{ + "error" => "Insufficient permissions: write:security." + } + end + + test "returns error if password incorrect", %{conn: conn} do + secret = TOTP.generate_secret() + code = TOTP.generate_token(secret) + + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + backup_codes: ["1", "2", "3"], + totp: %Settings.TOTP{secret: secret} + } + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + + response = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "xxx", code: code}) + |> json_response(422) + + settings = refresh_record(user).multi_factor_authentication_settings + refute settings.enabled + refute settings.totp.confirmed + assert settings.backup_codes == ["1", "2", "3"] + assert response == %{"error" => "Invalid password."} + end + + test "returns error if code incorrect", %{conn: conn} do + secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + backup_codes: ["1", "2", "3"], + totp: %Settings.TOTP{secret: secret} + } + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + token2 = insert(:oauth_token, scopes: ["read"]) + + response = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: "code"}) + |> json_response(422) + + settings = refresh_record(user).multi_factor_authentication_settings + refute settings.enabled + refute settings.totp.confirmed + assert settings.backup_codes == ["1", "2", "3"] + assert response == %{"error" => "invalid_token"} + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: "code"}) + |> json_response(403) == %{ + "error" => "Insufficient permissions: write:security." + } + end + end + + describe "DELETE /api/pleroma/accounts/mfa/totp" do + test "returns success result", %{conn: conn} do + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + backup_codes: ["1", "2", "3"], + totp: %Settings.TOTP{secret: "secret"} + } + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + token2 = insert(:oauth_token, scopes: ["read"]) + + assert conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> delete("/api/pleroma/accounts/mfa/totp", %{password: "test"}) + |> json_response(:ok) + + settings = refresh_record(user).multi_factor_authentication_settings + refute settings.enabled + assert settings.totp.secret == nil + refute settings.totp.confirmed + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> delete("/api/pleroma/accounts/mfa/totp", %{password: "test"}) + |> json_response(403) == %{ + "error" => "Insufficient permissions: write:security." + } + end + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/user_import_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/user_import_controller_test.exs @@ -0,0 +1,235 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do + use Pleroma.Web.ConnCase + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Config + alias Pleroma.Tests.ObanHelpers + + import Pleroma.Factory + import Mock + + setup do + Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "POST /api/pleroma/follow_import" do + setup do: oauth_access(["follow"]) + + test "it returns HTTP 200", %{conn: conn} do + user2 = insert(:user) + + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/follow_import", %{"list" => "#{user2.ap_id}"}) + |> json_response_and_validate_schema(200) + end + + test "it imports follow lists from file", %{conn: conn} do + user2 = insert(:user) + + with_mocks([ + {File, [], + read!: fn "follow_list.txt" -> + "Account address,Show boosts\n#{user2.ap_id},true" + end} + ]) do + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/follow_import", %{ + "list" => %Plug.Upload{path: "follow_list.txt"} + }) + |> json_response_and_validate_schema(200) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == [user2] + end + end + + test "it imports new-style mastodon follow lists", %{conn: conn} do + user2 = insert(:user) + + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/follow_import", %{ + "list" => "Account address,Show boosts\n#{user2.ap_id},true" + }) + |> json_response_and_validate_schema(200) + + assert response == "job started" + end + + test "requires 'follow' or 'write:follows' permissions" do + token1 = insert(:oauth_token, scopes: ["read", "write"]) + token2 = insert(:oauth_token, scopes: ["follow"]) + token3 = insert(:oauth_token, scopes: ["something"]) + another_user = insert(:user) + + for token <- [token1, token2, token3] do + conn = + build_conn() + |> put_req_header("authorization", "Bearer #{token.token}") + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/follow_import", %{"list" => "#{another_user.ap_id}"}) + + if token == token3 do + assert %{"error" => "Insufficient permissions: follow | write:follows."} == + json_response(conn, 403) + else + assert json_response(conn, 200) + end + end + end + + test "it imports follows with different nickname variations", %{conn: conn} do + users = [user2, user3, user4, user5, user6] = insert_list(5, :user) + + identifiers = + [ + user2.ap_id, + user3.nickname, + " ", + "@" <> user4.nickname, + user5.nickname <> "@localhost", + "@" <> user6.nickname <> "@localhost" + ] + |> Enum.join("\n") + + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/follow_import", %{"list" => identifiers}) + |> json_response_and_validate_schema(200) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == users + end + end + + describe "POST /api/pleroma/blocks_import" do + # Note: "follow" or "write:blocks" permission is required + setup do: oauth_access(["write:blocks"]) + + test "it returns HTTP 200", %{conn: conn} do + user2 = insert(:user) + + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/blocks_import", %{"list" => "#{user2.ap_id}"}) + |> json_response_and_validate_schema(200) + end + + test "it imports blocks users from file", %{conn: conn} do + users = [user2, user3] = insert_list(2, :user) + + with_mocks([ + {File, [], read!: fn "blocks_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end} + ]) do + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/blocks_import", %{ + "list" => %Plug.Upload{path: "blocks_list.txt"} + }) + |> json_response_and_validate_schema(200) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == users + end + end + + test "it imports blocks with different nickname variations", %{conn: conn} do + users = [user2, user3, user4, user5, user6] = insert_list(5, :user) + + identifiers = + [ + user2.ap_id, + user3.nickname, + "@" <> user4.nickname, + user5.nickname <> "@localhost", + "@" <> user6.nickname <> "@localhost" + ] + |> Enum.join(" ") + + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/blocks_import", %{"list" => identifiers}) + |> json_response_and_validate_schema(200) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == users + end + end + + describe "POST /api/pleroma/mutes_import" do + # Note: "follow" or "write:mutes" permission is required + setup do: oauth_access(["write:mutes"]) + + test "it returns HTTP 200", %{user: user, conn: conn} do + user2 = insert(:user) + + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/mutes_import", %{"list" => "#{user2.ap_id}"}) + |> json_response_and_validate_schema(200) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == [user2] + assert Pleroma.User.mutes?(user, user2) + end + + test "it imports mutes users from file", %{user: user, conn: conn} do + users = [user2, user3] = insert_list(2, :user) + + with_mocks([ + {File, [], read!: fn "mutes_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end} + ]) do + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/mutes_import", %{ + "list" => %Plug.Upload{path: "mutes_list.txt"} + }) + |> json_response_and_validate_schema(200) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == users + assert Enum.all?(users, &Pleroma.User.mutes?(user, &1)) + end + end + + test "it imports mutes with different nickname variations", %{user: user, conn: conn} do + users = [user2, user3, user4, user5, user6] = insert_list(5, :user) + + identifiers = + [ + user2.ap_id, + user3.nickname, + "@" <> user4.nickname, + user5.nickname <> "@localhost", + "@" <> user6.nickname <> "@localhost" + ] + |> Enum.join(" ") + + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/mutes_import", %{"list" => identifiers}) + |> json_response_and_validate_schema(200) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == users + assert Enum.all?(users, &Pleroma.User.mutes?(user, &1)) + end + 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 @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do + use Pleroma.DataCase + + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView + + import Pleroma.Factory + + test "it displays a chat message" do + user = insert(:user) + recipient = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis :firefox:") + + chat = Chat.get(user.id, recipient.ap_id) + + object = Object.normalize(activity) + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + chat_message = MessageReferenceView.render("show.json", chat_message_reference: cm_ref) + + assert chat_message[:id] == cm_ref.id + assert chat_message[:content] == "kippis :firefox:" + assert chat_message[:account_id] == user.id + assert chat_message[:chat_id] + assert chat_message[:created_at] + assert chat_message[:unread] == false + assert match?([%{shortcode: "firefox"}], chat_message[:emojis]) + + clear_config([:rich_media, :enabled], true) + + Tesla.Mock.mock(fn + %{url: "https://example.com/ogp"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")} + end) + + {:ok, activity} = + CommonAPI.post_chat_message(recipient, user, "gkgkgk https://example.com/ogp", + media_id: upload.id + ) + + object = Object.normalize(activity) + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + chat_message_two = MessageReferenceView.render("show.json", chat_message_reference: cm_ref) + + assert chat_message_two[:id] == cm_ref.id + assert chat_message_two[:content] == object.data["content"] + assert chat_message_two[:account_id] == recipient.id + assert chat_message_two[:chat_id] == chat_message[:chat_id] + assert chat_message_two[:attachment] + assert chat_message_two[:unread] == true + assert chat_message_two[:card] + end +end diff --git a/test/pleroma/web/pleroma_api/views/chat_view_test.exs b/test/pleroma/web/pleroma_api/views/chat_view_test.exs @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ChatViewTest do + use Pleroma.DataCase + + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Object + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView + alias Pleroma.Web.PleromaAPI.ChatView + + import Pleroma.Factory + + test "it represents a chat" do + user = insert(:user) + recipient = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) + + represented_chat = ChatView.render("show.json", chat: chat) + + assert represented_chat == %{ + id: "#{chat.id}", + account: + AccountView.render("show.json", user: recipient, skip_visibility_check: true), + unread: 0, + last_message: nil, + updated_at: Utils.to_masto_date(chat.updated_at) + } + + {:ok, chat_message_creation} = CommonAPI.post_chat_message(user, recipient, "hello") + + chat_message = Object.normalize(chat_message_creation, false) + + {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) + + represented_chat = ChatView.render("show.json", chat: chat) + + cm_ref = MessageReference.for_chat_and_object(chat, chat_message) + + assert represented_chat[:last_message] == + MessageReferenceView.render("show.json", chat_message_reference: cm_ref) + end +end diff --git a/test/pleroma/web/pleroma_api/views/scrobble_view_test.exs b/test/pleroma/web/pleroma_api/views/scrobble_view_test.exs @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ScrobbleViewTest do + use Pleroma.DataCase + + alias Pleroma.Web.PleromaAPI.ScrobbleView + + import Pleroma.Factory + + test "successfully renders a Listen activity (pleroma extension)" do + listen_activity = insert(:listen) + + status = ScrobbleView.render("show.json", activity: listen_activity) + + assert status.length == listen_activity.data["object"]["length"] + assert status.title == listen_activity.data["object"]["title"] + end +end diff --git a/test/pleroma/web/plugs/admin_secret_authentication_plug_test.exs b/test/pleroma/web/plugs/admin_secret_authentication_plug_test.exs @@ -0,0 +1,75 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlugTest do + use Pleroma.Web.ConnCase + + import Mock + import Pleroma.Factory + + alias Pleroma.Web.Plugs.AdminSecretAuthenticationPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.PlugHelper + alias Pleroma.Web.Plugs.RateLimiter + + test "does nothing if a user is assigned", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + + ret_conn = + conn + |> AdminSecretAuthenticationPlug.call(%{}) + + assert conn == ret_conn + end + + describe "when secret set it assigns an admin user" do + setup do: clear_config([:admin_token]) + + setup_with_mocks([{RateLimiter, [:passthrough], []}]) do + :ok + end + + test "with `admin_token` query parameter", %{conn: conn} do + Pleroma.Config.put(:admin_token, "password123") + + conn = + %{conn | params: %{"admin_token" => "wrong_password"}} + |> AdminSecretAuthenticationPlug.call(%{}) + + refute conn.assigns[:user] + assert called(RateLimiter.call(conn, name: :authentication)) + + conn = + %{conn | params: %{"admin_token" => "password123"}} + |> AdminSecretAuthenticationPlug.call(%{}) + + assert conn.assigns[:user].is_admin + assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) + end + + test "with `x-admin-token` HTTP header", %{conn: conn} do + Pleroma.Config.put(:admin_token, "☕️") + + conn = + conn + |> put_req_header("x-admin-token", "🥛") + |> AdminSecretAuthenticationPlug.call(%{}) + + refute conn.assigns[:user] + assert called(RateLimiter.call(conn, name: :authentication)) + + conn = + conn + |> put_req_header("x-admin-token", "☕️") + |> AdminSecretAuthenticationPlug.call(%{}) + + assert conn.assigns[:user].is_admin + assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) + end + end +end diff --git a/test/pleroma/web/plugs/authentication_plug_test.exs b/test/pleroma/web/plugs/authentication_plug_test.exs @@ -0,0 +1,125 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.AuthenticationPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.User + alias Pleroma.Web.Plugs.AuthenticationPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.PlugHelper + + import ExUnit.CaptureLog + import Pleroma.Factory + + setup %{conn: conn} do + user = %User{ + id: 1, + name: "dude", + password_hash: Pbkdf2.hash_pwd_salt("guy") + } + + conn = + conn + |> assign(:auth_user, user) + + %{user: user, conn: conn} + end + + test "it does nothing if a user is assigned", %{conn: conn} do + conn = + conn + |> assign(:user, %User{}) + + ret_conn = + conn + |> AuthenticationPlug.call(%{}) + + assert ret_conn == conn + end + + test "with a correct password in the credentials, " <> + "it assigns the auth_user and marks OAuthScopesPlug as skipped", + %{conn: conn} do + conn = + conn + |> assign(:auth_credentials, %{password: "guy"}) + |> AuthenticationPlug.call(%{}) + + assert conn.assigns.user == conn.assigns.auth_user + assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) + end + + test "with a bcrypt hash, it updates to a pkbdf2 hash", %{conn: conn} do + user = insert(:user, password_hash: Bcrypt.hash_pwd_salt("123")) + assert "$2" <> _ = user.password_hash + + conn = + conn + |> assign(:auth_user, user) + |> assign(:auth_credentials, %{password: "123"}) + |> AuthenticationPlug.call(%{}) + + assert conn.assigns.user.id == conn.assigns.auth_user.id + assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) + + user = User.get_by_id(user.id) + 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 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 = + "$pbkdf2-sha512$160000$loXqbp8GYls43F0i6lEfIw$AY.Ep.2pGe57j2hAPY635sI/6w7l9Q9u9Bp02PkPmF3OrClDtJAI8bCiivPr53OKMF7ph6iHhN68Rom5nEfC2A" + + assert AuthenticationPlug.checkpw("test-password", hash) + 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" + + assert AuthenticationPlug.checkpw("password", hash) + refute AuthenticationPlug.checkpw("password1", hash) + end + + test "it returns false when hash invalid" do + hash = + "psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1" + + assert capture_log(fn -> + refute AuthenticationPlug.checkpw("password", hash) + end) =~ "[error] Password hash not recognized" + end + end +end diff --git a/test/pleroma/web/plugs/basic_auth_decoder_plug_test.exs b/test/pleroma/web/plugs/basic_auth_decoder_plug_test.exs @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.BasicAuthDecoderPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Web.Plugs.BasicAuthDecoderPlug + + defp basic_auth_enc(username, password) do + "Basic " <> Base.encode64("#{username}:#{password}") + end + + test "it puts the decoded credentials into the assigns", %{conn: conn} do + header = basic_auth_enc("moonman", "iloverobek") + + conn = + conn + |> put_req_header("authorization", header) + |> BasicAuthDecoderPlug.call(%{}) + + assert conn.assigns[:auth_credentials] == %{ + username: "moonman", + password: "iloverobek" + } + end + + test "without a authorization header it doesn't do anything", %{conn: conn} do + ret_conn = + conn + |> BasicAuthDecoderPlug.call(%{}) + + assert conn == ret_conn + end +end diff --git a/test/pleroma/web/plugs/cache_control_test.exs b/test/pleroma/web/plugs/cache_control_test.exs @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.CacheControlTest do + use Pleroma.Web.ConnCase + alias Plug.Conn + + test "Verify Cache-Control header on static assets", %{conn: conn} do + conn = get(conn, "/index.html") + + assert Conn.get_resp_header(conn, "cache-control") == ["public, no-cache"] + end + + test "Verify Cache-Control header on the API", %{conn: conn} do + conn = get(conn, "/api/v1/instance") + + assert Conn.get_resp_header(conn, "cache-control") == ["max-age=0, private, must-revalidate"] + end +end diff --git a/test/pleroma/web/plugs/cache_test.exs b/test/pleroma/web/plugs/cache_test.exs @@ -0,0 +1,186 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.CacheTest do + use ExUnit.Case, async: true + use Plug.Test + + alias Pleroma.Web.Plugs.Cache + + @miss_resp {200, + [ + {"cache-control", "max-age=0, private, must-revalidate"}, + {"content-type", "cofe/hot; charset=utf-8"}, + {"x-cache", "MISS from Pleroma"} + ], "cofe"} + + @hit_resp {200, + [ + {"cache-control", "max-age=0, private, must-revalidate"}, + {"content-type", "cofe/hot; charset=utf-8"}, + {"x-cache", "HIT from Pleroma"} + ], "cofe"} + + @ttl 5 + + setup do + Cachex.clear(:web_resp_cache) + :ok + end + + test "caches a response" do + assert @miss_resp == + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + + assert_raise(Plug.Conn.AlreadySentError, fn -> + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + end) + + assert @hit_resp == + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: nil}) + |> sent_resp() + end + + test "ttl is set" do + assert @miss_resp == + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: @ttl}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + + assert @hit_resp == + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: @ttl}) + |> sent_resp() + + :timer.sleep(@ttl + 1) + + assert @miss_resp == + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: @ttl}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + end + + test "set ttl via conn.assigns" do + assert @miss_resp == + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> assign(:cache_ttl, @ttl) + |> send_resp(:ok, "cofe") + |> sent_resp() + + assert @hit_resp == + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: nil}) + |> sent_resp() + + :timer.sleep(@ttl + 1) + + assert @miss_resp == + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + end + + test "ignore query string when `query_params` is false" do + assert @miss_resp == + conn(:get, "/?cofe") + |> Cache.call(%{query_params: false, ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + + assert @hit_resp == + conn(:get, "/?cofefe") + |> Cache.call(%{query_params: false, ttl: nil}) + |> sent_resp() + end + + test "take query string into account when `query_params` is true" do + assert @miss_resp == + conn(:get, "/?cofe") + |> Cache.call(%{query_params: true, ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + + assert @miss_resp == + conn(:get, "/?cofefe") + |> Cache.call(%{query_params: true, ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + end + + test "take specific query params into account when `query_params` is list" do + assert @miss_resp == + conn(:get, "/?a=1&b=2&c=3&foo=bar") + |> fetch_query_params() + |> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + + assert @hit_resp == + conn(:get, "/?bar=foo&c=3&b=2&a=1") + |> fetch_query_params() + |> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil}) + |> sent_resp() + + assert @miss_resp == + conn(:get, "/?bar=foo&c=3&b=2&a=2") + |> fetch_query_params() + |> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + end + + test "ignore not GET requests" do + expected = + {200, + [ + {"cache-control", "max-age=0, private, must-revalidate"}, + {"content-type", "cofe/hot; charset=utf-8"} + ], "cofe"} + + assert expected == + conn(:post, "/") + |> Cache.call(%{query_params: true, ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + end + + test "ignore non-successful responses" do + expected = + {418, + [ + {"cache-control", "max-age=0, private, must-revalidate"}, + {"content-type", "tea/iced; charset=utf-8"} + ], "🥤"} + + assert expected == + conn(:get, "/cofe") + |> Cache.call(%{query_params: true, ttl: nil}) + |> put_resp_content_type("tea/iced") + |> send_resp(:im_a_teapot, "🥤") + |> sent_resp() + end +end diff --git a/test/pleroma/web/plugs/ensure_authenticated_plug_test.exs b/test/pleroma/web/plugs/ensure_authenticated_plug_test.exs @@ -0,0 +1,96 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.EnsureAuthenticatedPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.User + alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug + + describe "without :if_func / :unless_func options" do + test "it halts if user is NOT assigned", %{conn: conn} do + conn = EnsureAuthenticatedPlug.call(conn, %{}) + + assert conn.status == 403 + assert conn.halted == true + end + + test "it continues if a user is assigned", %{conn: conn} do + conn = assign(conn, :user, %User{}) + ret_conn = EnsureAuthenticatedPlug.call(conn, %{}) + + refute ret_conn.halted + end + end + + test "it halts if user is assigned and MFA enabled", %{conn: conn} do + conn = + conn + |> assign(:user, %User{multi_factor_authentication_settings: %{enabled: true}}) + |> assign(:auth_credentials, %{password: "xd-42"}) + |> EnsureAuthenticatedPlug.call(%{}) + + assert conn.status == 403 + assert conn.halted == true + + assert conn.resp_body == + "{\"error\":\"Two-factor authentication enabled, you must use a access token.\"}" + end + + test "it continues if user is assigned and MFA disabled", %{conn: conn} do + conn = + conn + |> assign(:user, %User{multi_factor_authentication_settings: %{enabled: false}}) + |> assign(:auth_credentials, %{password: "xd-42"}) + |> EnsureAuthenticatedPlug.call(%{}) + + refute conn.status == 403 + refute conn.halted + end + + describe "with :if_func / :unless_func options" do + setup do + %{ + true_fn: fn _conn -> true end, + false_fn: fn _conn -> false end + } + end + + test "it continues if a user is assigned", %{conn: conn, true_fn: true_fn, false_fn: false_fn} do + conn = assign(conn, :user, %User{}) + refute EnsureAuthenticatedPlug.call(conn, if_func: true_fn).halted + refute EnsureAuthenticatedPlug.call(conn, if_func: false_fn).halted + refute EnsureAuthenticatedPlug.call(conn, unless_func: true_fn).halted + refute EnsureAuthenticatedPlug.call(conn, unless_func: false_fn).halted + end + + test "it continues if a user is NOT assigned but :if_func evaluates to `false`", + %{conn: conn, false_fn: false_fn} do + ret_conn = EnsureAuthenticatedPlug.call(conn, if_func: false_fn) + refute ret_conn.halted + end + + test "it continues if a user is NOT assigned but :unless_func evaluates to `true`", + %{conn: conn, true_fn: true_fn} do + ret_conn = EnsureAuthenticatedPlug.call(conn, unless_func: true_fn) + refute ret_conn.halted + end + + test "it halts if a user is NOT assigned and :if_func evaluates to `true`", + %{conn: conn, true_fn: true_fn} do + conn = EnsureAuthenticatedPlug.call(conn, if_func: true_fn) + + assert conn.status == 403 + assert conn.halted == true + end + + test "it halts if a user is NOT assigned and :unless_func evaluates to `false`", + %{conn: conn, false_fn: false_fn} do + conn = EnsureAuthenticatedPlug.call(conn, unless_func: false_fn) + + assert conn.status == 403 + assert conn.halted == true + end + end +end diff --git a/test/pleroma/web/plugs/ensure_public_or_authenticated_plug_test.exs b/test/pleroma/web/plugs/ensure_public_or_authenticated_plug_test.exs @@ -0,0 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Config + alias Pleroma.User + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug + + setup do: clear_config([:instance, :public]) + + test "it halts if not public and no user is assigned", %{conn: conn} do + Config.put([:instance, :public], false) + + conn = + conn + |> EnsurePublicOrAuthenticatedPlug.call(%{}) + + assert conn.status == 403 + assert conn.halted == true + end + + test "it continues if public", %{conn: conn} do + Config.put([:instance, :public], true) + + ret_conn = + conn + |> EnsurePublicOrAuthenticatedPlug.call(%{}) + + refute ret_conn.halted + end + + test "it continues if a user is assigned, even if not public", %{conn: conn} do + Config.put([:instance, :public], false) + + conn = + conn + |> assign(:user, %User{}) + + ret_conn = + conn + |> EnsurePublicOrAuthenticatedPlug.call(%{}) + + refute ret_conn.halted + end +end diff --git a/test/pleroma/web/plugs/ensure_user_key_plug_test.exs b/test/pleroma/web/plugs/ensure_user_key_plug_test.exs @@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.EnsureUserKeyPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Web.Plugs.EnsureUserKeyPlug + + test "if the conn has a user key set, it does nothing", %{conn: conn} do + conn = + conn + |> assign(:user, 1) + + ret_conn = + conn + |> EnsureUserKeyPlug.call(%{}) + + assert conn == ret_conn + end + + test "if the conn has no key set, it sets it to nil", %{conn: conn} do + conn = + conn + |> EnsureUserKeyPlug.call(%{}) + + assert Map.has_key?(conn.assigns, :user) + end +end diff --git a/test/pleroma/web/plugs/federating_plug_test.exs b/test/pleroma/web/plugs/federating_plug_test.exs @@ -0,0 +1,31 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.FederatingPlugTest do + use Pleroma.Web.ConnCase + + setup do: clear_config([:instance, :federating]) + + test "returns and halt the conn when federating is disabled" do + Pleroma.Config.put([:instance, :federating], false) + + conn = + build_conn() + |> Pleroma.Web.Plugs.FederatingPlug.call(%{}) + + assert conn.status == 404 + assert conn.halted + end + + test "does nothing when federating is enabled" do + Pleroma.Config.put([:instance, :federating], true) + + conn = + build_conn() + |> Pleroma.Web.Plugs.FederatingPlug.call(%{}) + + refute conn.status + refute conn.halted + end +end diff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do + use Pleroma.Web.ConnCase + + @dir "test/tmp/instance_static" + + setup do + File.mkdir_p!(@dir) + on_exit(fn -> File.rm_rf(@dir) end) + end + + setup do: clear_config([:instance, :static_dir], @dir) + + test "init will give a static plug config + the frontend type" do + opts = + [ + at: "/admin", + frontend_type: :admin + ] + |> Pleroma.Web.Plugs.FrontendStatic.init() + + assert opts[:at] == ["admin"] + assert opts[:frontend_type] == :admin + end + + test "overrides existing static files", %{conn: conn} do + name = "pelmora" + ref = "uguu" + + clear_config([:frontends, :primary], %{"name" => name, "ref" => ref}) + path = "#{@dir}/frontends/#{name}/#{ref}" + + File.mkdir_p!(path) + File.write!("#{path}/index.html", "from frontend plug") + + index = get(conn, "/") + assert html_response(index, 200) == "from frontend plug" + end + + test "overrides existing static files for the `pleroma/admin` path", %{conn: conn} do + name = "pelmora" + ref = "uguu" + + clear_config([:frontends, :admin], %{"name" => name, "ref" => ref}) + path = "#{@dir}/frontends/#{name}/#{ref}" + + File.mkdir_p!(path) + File.write!("#{path}/index.html", "from frontend plug") + + index = get(conn, "/pleroma/admin/") + assert html_response(index, 200) == "from frontend plug" + end +end diff --git a/test/pleroma/web/plugs/http_security_plug_test.exs b/test/pleroma/web/plugs/http_security_plug_test.exs @@ -0,0 +1,140 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Config + alias Plug.Conn + + describe "http security enabled" do + setup do: clear_config([:http_security, :enabled], true) + + test "it sends CSP headers when enabled", %{conn: conn} do + conn = get(conn, "/api/v1/instance") + + refute Conn.get_resp_header(conn, "x-xss-protection") == [] + refute Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] + refute Conn.get_resp_header(conn, "x-frame-options") == [] + refute Conn.get_resp_header(conn, "x-content-type-options") == [] + refute Conn.get_resp_header(conn, "x-download-options") == [] + refute Conn.get_resp_header(conn, "referrer-policy") == [] + refute Conn.get_resp_header(conn, "content-security-policy") == [] + end + + test "it sends STS headers when enabled", %{conn: conn} do + clear_config([:http_security, :sts], true) + + conn = get(conn, "/api/v1/instance") + + refute Conn.get_resp_header(conn, "strict-transport-security") == [] + refute Conn.get_resp_header(conn, "expect-ct") == [] + end + + test "it does not send STS headers when disabled", %{conn: conn} do + clear_config([:http_security, :sts], false) + + conn = get(conn, "/api/v1/instance") + + assert Conn.get_resp_header(conn, "strict-transport-security") == [] + assert Conn.get_resp_header(conn, "expect-ct") == [] + end + + test "referrer-policy header reflects configured value", %{conn: conn} do + resp = get(conn, "/api/v1/instance") + + assert Conn.get_resp_header(resp, "referrer-policy") == ["same-origin"] + + clear_config([:http_security, :referrer_policy], "no-referrer") + + resp = get(conn, "/api/v1/instance") + + assert Conn.get_resp_header(resp, "referrer-policy") == ["no-referrer"] + end + + test "it sends `report-to` & `report-uri` CSP response headers", %{conn: conn} do + conn = get(conn, "/api/v1/instance") + + [csp] = Conn.get_resp_header(conn, "content-security-policy") + + assert csp =~ ~r|report-uri https://endpoint.com;report-to csp-endpoint;| + + [reply_to] = Conn.get_resp_header(conn, "reply-to") + + assert reply_to == + "{\"endpoints\":[{\"url\":\"https://endpoint.com\"}],\"group\":\"csp-endpoint\",\"max-age\":10886400}" + end + + test "default values for img-src and media-src with disabled media proxy", %{conn: conn} do + conn = get(conn, "/api/v1/instance") + + [csp] = Conn.get_resp_header(conn, "content-security-policy") + assert csp =~ "media-src 'self' https:;" + assert csp =~ "img-src 'self' data: blob: https:;" + end + end + + describe "img-src and media-src" do + setup do + clear_config([:http_security, :enabled], true) + clear_config([:media_proxy, :enabled], true) + clear_config([:media_proxy, :proxy_opts, :redirect_on_failure], false) + end + + test "media_proxy with base_url", %{conn: conn} do + url = "https://example.com" + clear_config([:media_proxy, :base_url], url) + assert_media_img_src(conn, url) + end + + test "upload with base url", %{conn: conn} do + url = "https://example2.com" + clear_config([Pleroma.Upload, :base_url], url) + assert_media_img_src(conn, url) + end + + test "with S3 public endpoint", %{conn: conn} do + url = "https://example3.com" + clear_config([Pleroma.Uploaders.S3, :public_endpoint], url) + assert_media_img_src(conn, url) + end + + test "with captcha endpoint", %{conn: conn} do + clear_config([Pleroma.Captcha.Mock, :endpoint], "https://captcha.com") + assert_media_img_src(conn, "https://captcha.com") + end + + test "with media_proxy whitelist", %{conn: conn} do + clear_config([:media_proxy, :whitelist], ["https://example6.com", "https://example7.com"]) + assert_media_img_src(conn, "https://example7.com https://example6.com") + end + + # TODO: delete after removing support bare domains for media proxy whitelist + test "with media_proxy bare domains whitelist (deprecated)", %{conn: conn} do + clear_config([:media_proxy, :whitelist], ["example4.com", "example5.com"]) + assert_media_img_src(conn, "example5.com example4.com") + end + end + + defp assert_media_img_src(conn, url) do + conn = get(conn, "/api/v1/instance") + [csp] = Conn.get_resp_header(conn, "content-security-policy") + assert csp =~ "media-src 'self' #{url};" + assert csp =~ "img-src 'self' data: blob: #{url};" + end + + test "it does not send CSP headers when disabled", %{conn: conn} do + clear_config([:http_security, :enabled], false) + + conn = get(conn, "/api/v1/instance") + + assert Conn.get_resp_header(conn, "x-xss-protection") == [] + assert Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] + assert Conn.get_resp_header(conn, "x-frame-options") == [] + assert Conn.get_resp_header(conn, "x-content-type-options") == [] + assert Conn.get_resp_header(conn, "x-download-options") == [] + assert Conn.get_resp_header(conn, "referrer-policy") == [] + assert Conn.get_resp_header(conn, "content-security-policy") == [] + end +end diff --git a/test/pleroma/web/plugs/http_signature_plug_test.exs b/test/pleroma/web/plugs/http_signature_plug_test.exs @@ -0,0 +1,89 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do + use Pleroma.Web.ConnCase + alias Pleroma.Web.Plugs.HTTPSignaturePlug + + import Plug.Conn + import Phoenix.Controller, only: [put_format: 2] + import Mock + + test "it call HTTPSignatures to check validity if the actor sighed it" do + params = %{"actor" => "http://mastodon.example.org/users/admin"} + conn = build_conn(:get, "/doesntmattter", params) + + with_mock HTTPSignatures, validate_conn: fn _ -> true end do + conn = + conn + |> put_req_header( + "signature", + "keyId=\"http://mastodon.example.org/users/admin#main-key" + ) + |> put_format("activity+json") + |> HTTPSignaturePlug.call(%{}) + + assert conn.assigns.valid_signature == true + assert conn.halted == false + assert called(HTTPSignatures.validate_conn(:_)) + end + end + + describe "requires a signature when `authorized_fetch_mode` is enabled" do + setup do + Pleroma.Config.put([:activitypub, :authorized_fetch_mode], true) + + on_exit(fn -> + Pleroma.Config.put([:activitypub, :authorized_fetch_mode], false) + end) + + params = %{"actor" => "http://mastodon.example.org/users/admin"} + conn = build_conn(:get, "/doesntmattter", params) |> put_format("activity+json") + + [conn: conn] + end + + test "when signature header is present", %{conn: conn} do + with_mock HTTPSignatures, validate_conn: fn _ -> false end do + conn = + conn + |> put_req_header( + "signature", + "keyId=\"http://mastodon.example.org/users/admin#main-key" + ) + |> HTTPSignaturePlug.call(%{}) + + assert conn.assigns.valid_signature == false + assert conn.halted == true + assert conn.status == 401 + assert conn.state == :sent + assert conn.resp_body == "Request not signed" + assert called(HTTPSignatures.validate_conn(:_)) + end + + with_mock HTTPSignatures, validate_conn: fn _ -> true end do + conn = + conn + |> put_req_header( + "signature", + "keyId=\"http://mastodon.example.org/users/admin#main-key" + ) + |> HTTPSignaturePlug.call(%{}) + + assert conn.assigns.valid_signature == true + assert conn.halted == false + assert called(HTTPSignatures.validate_conn(:_)) + end + end + + test "halts the connection when `signature` header is not present", %{conn: conn} do + conn = HTTPSignaturePlug.call(conn, %{}) + assert conn.assigns[:valid_signature] == nil + assert conn.halted == true + assert conn.status == 401 + assert conn.state == :sent + assert conn.resp_body == "Request not signed" + end + end +end diff --git a/test/pleroma/web/plugs/idempotency_plug_test.exs b/test/pleroma/web/plugs/idempotency_plug_test.exs @@ -0,0 +1,110 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.IdempotencyPlugTest do + use ExUnit.Case, async: true + use Plug.Test + + alias Pleroma.Web.Plugs.IdempotencyPlug + alias Plug.Conn + + test "returns result from cache" do + key = "test1" + orig_request_id = "test1" + second_request_id = "test2" + body = "testing" + status = 200 + + :post + |> conn("/cofe") + |> put_req_header("idempotency-key", key) + |> Conn.put_resp_header("x-request-id", orig_request_id) + |> Conn.put_resp_content_type("application/json") + |> IdempotencyPlug.call([]) + |> Conn.send_resp(status, body) + + conn = + :post + |> conn("/cofe") + |> put_req_header("idempotency-key", key) + |> Conn.put_resp_header("x-request-id", second_request_id) + |> Conn.put_resp_content_type("application/json") + |> IdempotencyPlug.call([]) + + assert_raise Conn.AlreadySentError, fn -> + Conn.send_resp(conn, :im_a_teapot, "no cofe") + end + + assert conn.resp_body == body + assert conn.status == status + + assert [^second_request_id] = Conn.get_resp_header(conn, "x-request-id") + assert [^orig_request_id] = Conn.get_resp_header(conn, "x-original-request-id") + assert [^key] = Conn.get_resp_header(conn, "idempotency-key") + assert ["true"] = Conn.get_resp_header(conn, "idempotent-replayed") + assert ["application/json; charset=utf-8"] = Conn.get_resp_header(conn, "content-type") + end + + test "pass conn downstream if the cache not found" do + key = "test2" + orig_request_id = "test3" + body = "testing" + status = 200 + + conn = + :post + |> conn("/cofe") + |> put_req_header("idempotency-key", key) + |> Conn.put_resp_header("x-request-id", orig_request_id) + |> Conn.put_resp_content_type("application/json") + |> IdempotencyPlug.call([]) + |> Conn.send_resp(status, body) + + assert conn.resp_body == body + assert conn.status == status + + assert [] = Conn.get_resp_header(conn, "idempotent-replayed") + assert [^key] = Conn.get_resp_header(conn, "idempotency-key") + end + + test "passes conn downstream if idempotency is not present in headers" do + orig_request_id = "test4" + body = "testing" + status = 200 + + conn = + :post + |> conn("/cofe") + |> Conn.put_resp_header("x-request-id", orig_request_id) + |> Conn.put_resp_content_type("application/json") + |> IdempotencyPlug.call([]) + |> Conn.send_resp(status, body) + + assert [] = Conn.get_resp_header(conn, "idempotency-key") + end + + test "doesn't work with GET/DELETE" do + key = "test3" + body = "testing" + status = 200 + + conn = + :get + |> conn("/cofe") + |> put_req_header("idempotency-key", key) + |> IdempotencyPlug.call([]) + |> Conn.send_resp(status, body) + + assert [] = Conn.get_resp_header(conn, "idempotency-key") + + conn = + :delete + |> conn("/cofe") + |> put_req_header("idempotency-key", key) + |> IdempotencyPlug.call([]) + |> Conn.send_resp(status, body) + + assert [] = Conn.get_resp_header(conn, "idempotency-key") + end +end diff --git a/test/pleroma/web/plugs/instance_static_test.exs b/test/pleroma/web/plugs/instance_static_test.exs @@ -0,0 +1,65 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.InstanceStaticTest do + use Pleroma.Web.ConnCase + + @dir "test/tmp/instance_static" + + setup do + File.mkdir_p!(@dir) + on_exit(fn -> File.rm_rf(@dir) end) + end + + setup do: clear_config([:instance, :static_dir], @dir) + + test "overrides index" do + bundled_index = get(build_conn(), "/") + refute html_response(bundled_index, 200) == "hello world" + + File.write!(@dir <> "/index.html", "hello world") + + index = get(build_conn(), "/") + assert html_response(index, 200) == "hello world" + end + + test "also overrides frontend files", %{conn: conn} do + name = "pelmora" + ref = "uguu" + + clear_config([:frontends, :primary], %{"name" => name, "ref" => ref}) + + bundled_index = get(conn, "/") + refute html_response(bundled_index, 200) == "from frontend plug" + + path = "#{@dir}/frontends/#{name}/#{ref}" + File.mkdir_p!(path) + File.write!("#{path}/index.html", "from frontend plug") + + index = get(conn, "/") + assert html_response(index, 200) == "from frontend plug" + + File.write!(@dir <> "/index.html", "from instance static") + + index = get(conn, "/") + assert html_response(index, 200) == "from instance static" + end + + test "overrides any file in static/static" do + bundled_index = get(build_conn(), "/static/terms-of-service.html") + + assert html_response(bundled_index, 200) == + File.read!("priv/static/static/terms-of-service.html") + + File.mkdir!(@dir <> "/static") + File.write!(@dir <> "/static/terms-of-service.html", "plz be kind") + + index = get(build_conn(), "/static/terms-of-service.html") + assert html_response(index, 200) == "plz be kind" + + File.write!(@dir <> "/static/kaniini.html", "<h1>rabbit hugs as a service</h1>") + index = get(build_conn(), "/static/kaniini.html") + assert html_response(index, 200) == "<h1>rabbit hugs as a service</h1>" + end +end diff --git a/test/pleroma/web/plugs/legacy_authentication_plug_test.exs b/test/pleroma/web/plugs/legacy_authentication_plug_test.exs @@ -0,0 +1,82 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.LegacyAuthenticationPlugTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.User + alias Pleroma.Web.Plugs.LegacyAuthenticationPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.PlugHelper + + setup do + user = + insert(:user, + password: "password", + password_hash: + "$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1" + ) + + %{user: user} + end + + test "it does nothing if a user is assigned", %{conn: conn, user: user} do + conn = + conn + |> assign(:auth_credentials, %{username: "dude", password: "password"}) + |> assign(:auth_user, user) + |> assign(:user, %User{}) + + ret_conn = + conn + |> LegacyAuthenticationPlug.call(%{}) + + assert ret_conn == conn + end + + @tag :skip_on_mac + test "if `auth_user` is present and password is correct, " <> + "it authenticates the user, resets the password, marks OAuthScopesPlug as skipped", + %{ + conn: conn, + user: user + } do + conn = + conn + |> assign(:auth_credentials, %{username: "dude", password: "password"}) + |> assign(:auth_user, user) + + conn = LegacyAuthenticationPlug.call(conn, %{}) + + assert conn.assigns.user.id == user.id + assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) + end + + @tag :skip_on_mac + test "it does nothing if the password is wrong", %{ + conn: conn, + user: user + } do + conn = + conn + |> assign(:auth_credentials, %{username: "dude", password: "wrong_password"}) + |> assign(:auth_user, user) + + ret_conn = + conn + |> LegacyAuthenticationPlug.call(%{}) + + assert conn == ret_conn + end + + test "with no credentials or user it does nothing", %{conn: conn} do + ret_conn = + conn + |> LegacyAuthenticationPlug.call(%{}) + + assert ret_conn == conn + end +end diff --git a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do + use Pleroma.Web.ConnCase + alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug + + import Tesla.Mock + import Plug.Conn + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + defp set_signature(conn, key_id) do + conn + |> put_req_header("signature", "keyId=\"#{key_id}\"") + |> assign(:valid_signature, true) + end + + test "it successfully maps a valid identity with a valid signature" do + conn = + build_conn(:get, "/doesntmattter") + |> set_signature("http://mastodon.example.org/users/admin") + |> MappedSignatureToIdentityPlug.call(%{}) + + refute is_nil(conn.assigns.user) + end + + test "it successfully maps a valid identity with a valid signature with payload" do + conn = + build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) + |> set_signature("http://mastodon.example.org/users/admin") + |> MappedSignatureToIdentityPlug.call(%{}) + + refute is_nil(conn.assigns.user) + end + + test "it considers a mapped identity to be invalid when it mismatches a payload" do + conn = + build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) + |> set_signature("https://niu.moe/users/rye") + |> MappedSignatureToIdentityPlug.call(%{}) + + assert %{valid_signature: false} == conn.assigns + end + + @tag skip: "known breakage; the testsuite presently depends on it" + test "it considers a mapped identity to be invalid when the identity cannot be found" do + conn = + build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) + |> set_signature("http://niu.moe/users/rye") + |> MappedSignatureToIdentityPlug.call(%{}) + + assert %{valid_signature: false} == conn.assigns + end +end diff --git a/test/pleroma/web/plugs/o_auth_plug_test.exs b/test/pleroma/web/plugs/o_auth_plug_test.exs @@ -0,0 +1,80 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.OAuthPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Web.Plugs.OAuthPlug + import Pleroma.Factory + + @session_opts [ + store: :cookie, + key: "_test", + signing_salt: "cooldude" + ] + + setup %{conn: conn} do + user = insert(:user) + {:ok, %{token: token}} = Pleroma.Web.OAuth.Token.create(insert(:oauth_app), user) + %{user: user, token: token, conn: conn} + end + + test "with valid token(uppercase), it assigns the user", %{conn: conn} = opts do + conn = + conn + |> put_req_header("authorization", "BEARER #{opts[:token]}") + |> OAuthPlug.call(%{}) + + assert conn.assigns[:user] == opts[:user] + end + + test "with valid token(downcase), it assigns the user", %{conn: conn} = opts do + conn = + conn + |> put_req_header("authorization", "bearer #{opts[:token]}") + |> OAuthPlug.call(%{}) + + assert conn.assigns[:user] == opts[:user] + end + + test "with valid token(downcase) in url parameters, it assigns the user", opts do + conn = + :get + |> build_conn("/?access_token=#{opts[:token]}") + |> put_req_header("content-type", "application/json") + |> fetch_query_params() + |> OAuthPlug.call(%{}) + + assert conn.assigns[:user] == opts[:user] + end + + test "with valid token(downcase) in body parameters, it assigns the user", opts do + conn = + :post + |> build_conn("/api/v1/statuses", access_token: opts[:token], status: "test") + |> OAuthPlug.call(%{}) + + assert conn.assigns[:user] == opts[:user] + end + + test "with invalid token, it not assigns the user", %{conn: conn} do + conn = + conn + |> put_req_header("authorization", "bearer TTTTT") + |> OAuthPlug.call(%{}) + + refute conn.assigns[:user] + end + + test "when token is missed but token in session, it assigns the user", %{conn: conn} = opts do + conn = + conn + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + |> put_session(:oauth_token, opts[:token]) + |> OAuthPlug.call(%{}) + + assert conn.assigns[:user] == opts[:user] + end +end diff --git a/test/pleroma/web/plugs/o_auth_scopes_plug_test.exs b/test/pleroma/web/plugs/o_auth_scopes_plug_test.exs @@ -0,0 +1,210 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.OAuthScopesPlugTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Repo + alias Pleroma.Web.Plugs.OAuthScopesPlug + + import Mock + import Pleroma.Factory + + test "is not performed if marked as skipped", %{conn: conn} do + with_mock OAuthScopesPlug, [:passthrough], perform: &passthrough([&1, &2]) do + conn = + conn + |> OAuthScopesPlug.skip_plug() + |> OAuthScopesPlug.call(%{scopes: ["random_scope"]}) + + refute called(OAuthScopesPlug.perform(:_, :_)) + refute conn.halted + end + end + + test "if `token.scopes` fulfills specified 'any of' conditions, " <> + "proceeds with no op", + %{conn: conn} do + token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user) + + conn = + conn + |> assign(:user, token.user) + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: ["read"]}) + + refute conn.halted + assert conn.assigns[:user] + end + + test "if `token.scopes` fulfills specified 'all of' conditions, " <> + "proceeds with no op", + %{conn: conn} do + token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user) + + conn = + conn + |> assign(:user, token.user) + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: ["scope2", "scope3"], op: :&}) + + refute conn.halted + assert conn.assigns[:user] + end + + describe "with `fallback: :proceed_unauthenticated` option, " do + test "if `token.scopes` doesn't fulfill specified conditions, " <> + "clears :user and :token assigns", + %{conn: conn} do + user = insert(:user) + token1 = insert(:oauth_token, scopes: ["read", "write"], user: user) + + for token <- [token1, nil], op <- [:|, :&] do + ret_conn = + conn + |> assign(:user, user) + |> assign(:token, token) + |> OAuthScopesPlug.call(%{ + scopes: ["follow"], + op: op, + fallback: :proceed_unauthenticated + }) + + refute ret_conn.halted + refute ret_conn.assigns[:user] + refute ret_conn.assigns[:token] + end + end + end + + describe "without :fallback option, " do + test "if `token.scopes` does not fulfill specified 'any of' conditions, " <> + "returns 403 and halts", + %{conn: conn} do + for token <- [insert(:oauth_token, scopes: ["read", "write"]), nil] do + any_of_scopes = ["follow", "push"] + + ret_conn = + conn + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: any_of_scopes}) + + assert ret_conn.halted + assert 403 == ret_conn.status + + expected_error = "Insufficient permissions: #{Enum.join(any_of_scopes, " | ")}." + assert Jason.encode!(%{error: expected_error}) == ret_conn.resp_body + end + end + + test "if `token.scopes` does not fulfill specified 'all of' conditions, " <> + "returns 403 and halts", + %{conn: conn} do + for token <- [insert(:oauth_token, scopes: ["read", "write"]), nil] do + token_scopes = (token && token.scopes) || [] + all_of_scopes = ["write", "follow"] + + conn = + conn + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: all_of_scopes, op: :&}) + + assert conn.halted + assert 403 == conn.status + + expected_error = + "Insufficient permissions: #{Enum.join(all_of_scopes -- token_scopes, " & ")}." + + assert Jason.encode!(%{error: expected_error}) == conn.resp_body + end + end + end + + describe "with hierarchical scopes, " do + test "if `token.scopes` fulfills specified 'any of' conditions, " <> + "proceeds with no op", + %{conn: conn} do + token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user) + + conn = + conn + |> assign(:user, token.user) + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: ["read:something"]}) + + refute conn.halted + assert conn.assigns[:user] + end + + test "if `token.scopes` fulfills specified 'all of' conditions, " <> + "proceeds with no op", + %{conn: conn} do + token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user) + + conn = + conn + |> assign(:user, token.user) + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: ["scope1:subscope", "scope2:subscope"], op: :&}) + + refute conn.halted + assert conn.assigns[:user] + end + end + + describe "filter_descendants/2" do + test "filters scopes which directly match or are ancestors of supported scopes" do + f = fn scopes, supported_scopes -> + OAuthScopesPlug.filter_descendants(scopes, supported_scopes) + end + + assert f.(["read", "follow"], ["write", "read"]) == ["read"] + + assert f.(["read", "write:something", "follow"], ["write", "read"]) == + ["read", "write:something"] + + assert f.(["admin:read"], ["write", "read"]) == [] + + assert f.(["admin:read"], ["write", "admin"]) == ["admin:read"] + end + end + + describe "transform_scopes/2" do + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage]) + + setup do + {:ok, %{f: &OAuthScopesPlug.transform_scopes/2}} + end + + test "with :admin option, prefixes all requested scopes with `admin:` " <> + "and [optionally] keeps only prefixed scopes, " <> + "depending on `[:auth, :enforce_oauth_admin_scope_usage]` setting", + %{f: f} do + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false) + + assert f.(["read"], %{admin: true}) == ["admin:read", "read"] + + assert f.(["read", "write"], %{admin: true}) == [ + "admin:read", + "read", + "admin:write", + "write" + ] + + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true) + + assert f.(["read:accounts"], %{admin: true}) == ["admin:read:accounts"] + + assert f.(["read", "write:reports"], %{admin: true}) == [ + "admin:read", + "admin:write:reports" + ] + end + + test "with no supported options, returns unmodified scopes", %{f: f} do + assert f.(["read"], %{}) == ["read"] + assert f.(["read", "write"], %{}) == ["read", "write"] + end + end +end diff --git a/test/pleroma/web/plugs/plug_helper_test.exs b/test/pleroma/web/plugs/plug_helper_test.exs @@ -0,0 +1,91 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.PlugHelperTest do + @moduledoc "Tests for the functionality added via `use Pleroma.Web, :plug`" + + alias Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug + alias Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug + alias Pleroma.Web.Plugs.PlugHelper + + import Mock + + use Pleroma.Web.ConnCase + + describe "when plug is skipped, " do + setup_with_mocks( + [ + {ExpectPublicOrAuthenticatedCheckPlug, [:passthrough], []} + ], + %{conn: conn} + ) do + conn = ExpectPublicOrAuthenticatedCheckPlug.skip_plug(conn) + %{conn: conn} + end + + test "it neither adds plug to called plugs list nor calls `perform/2`, " <> + "regardless of :if_func / :unless_func options", + %{conn: conn} do + for opts <- [%{}, %{if_func: fn _ -> true end}, %{unless_func: fn _ -> false end}] do + ret_conn = ExpectPublicOrAuthenticatedCheckPlug.call(conn, opts) + + refute called(ExpectPublicOrAuthenticatedCheckPlug.perform(:_, :_)) + refute PlugHelper.plug_called?(ret_conn, ExpectPublicOrAuthenticatedCheckPlug) + end + end + end + + describe "when plug is NOT skipped, " do + setup_with_mocks([{ExpectAuthenticatedCheckPlug, [:passthrough], []}]) do + :ok + end + + test "with no pre-run checks, adds plug to called plugs list and calls `perform/2`", %{ + conn: conn + } do + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{}) + + assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) + assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + end + + test "when :if_func option is given, calls the plug only if provided function evals tru-ish", + %{conn: conn} do + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{if_func: fn _ -> false end}) + + refute called(ExpectAuthenticatedCheckPlug.perform(:_, :_)) + refute PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{if_func: fn _ -> true end}) + + assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) + assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + end + + test "if :unless_func option is given, calls the plug only if provided function evals falsy", + %{conn: conn} do + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{unless_func: fn _ -> true end}) + + refute called(ExpectAuthenticatedCheckPlug.perform(:_, :_)) + refute PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{unless_func: fn _ -> false end}) + + assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) + assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + end + + test "allows a plug to be called multiple times (even if it's in called plugs list)", %{ + conn: conn + } do + conn = ExpectAuthenticatedCheckPlug.call(conn, %{an_option: :value1}) + assert called(ExpectAuthenticatedCheckPlug.perform(conn, %{an_option: :value1})) + + assert PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) + + conn = ExpectAuthenticatedCheckPlug.call(conn, %{an_option: :value2}) + assert called(ExpectAuthenticatedCheckPlug.perform(conn, %{an_option: :value2})) + end + end +end diff --git a/test/pleroma/web/plugs/rate_limiter_test.exs b/test/pleroma/web/plugs/rate_limiter_test.exs @@ -0,0 +1,263 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.RateLimiterTest do + use Pleroma.Web.ConnCase + + alias Phoenix.ConnTest + alias Pleroma.Config + alias Pleroma.Web.Plugs.RateLimiter + alias Plug.Conn + + import Pleroma.Factory + import Pleroma.Tests.Helpers, only: [clear_config: 1, clear_config: 2] + + # Note: each example must work with separate buckets in order to prevent concurrency issues + setup do: clear_config([Pleroma.Web.Endpoint, :http, :ip]) + setup do: clear_config(:rate_limit) + + describe "config" do + @limiter_name :test_init + setup do: clear_config([Pleroma.Web.Plugs.RemoteIp, :enabled]) + + test "config is required for plug to work" do + Config.put([:rate_limit, @limiter_name], {1, 1}) + Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + + assert %{limits: {1, 1}, name: :test_init, opts: [name: :test_init]} == + [name: @limiter_name] + |> RateLimiter.init() + |> RateLimiter.action_settings() + + assert nil == + [name: :nonexisting_limiter] + |> RateLimiter.init() + |> RateLimiter.action_settings() + end + end + + test "it is disabled if it remote ip plug is enabled but no remote ip is found" do + assert RateLimiter.disabled?(Conn.assign(build_conn(), :remote_ip_found, false)) + end + + test "it is enabled if remote ip found" do + refute RateLimiter.disabled?(Conn.assign(build_conn(), :remote_ip_found, true)) + end + + test "it is enabled if remote_ip_found flag doesn't exist" do + refute RateLimiter.disabled?(build_conn()) + end + + test "it restricts based on config values" do + limiter_name = :test_plug_opts + scale = 80 + limit = 5 + + Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + Config.put([: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) + end + + conn = RateLimiter.call(conn, plug_opts) + assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests) + assert conn.halted + + Process.sleep(50) + + conn = build_conn(:get, "/") + + conn = RateLimiter.call(conn, plug_opts) + assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) + + refute conn.status == Conn.Status.code(:too_many_requests) + refute conn.resp_body + refute conn.halted + end + + describe "options" do + test "`bucket_name` option overrides default bucket name" do + limiter_name = :test_bucket_name + + Config.put([:rate_limit, limiter_name], {1000, 5}) + Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + + base_bucket_name = "#{limiter_name}:group1" + plug_opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name) + + conn = build_conn(:get, "/") + + RateLimiter.call(conn, plug_opts) + assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, plug_opts) + assert {:error, :not_found} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) + end + + test "`params` option allows different queries to be tracked independently" do + limiter_name = :test_params + Config.put([:rate_limit, limiter_name], {1000, 5}) + Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + + plug_opts = RateLimiter.init(name: limiter_name, params: ["id"]) + + conn = build_conn(:get, "/?id=1") + conn = Conn.fetch_query_params(conn) + conn_2 = build_conn(:get, "/?id=2") + + RateLimiter.call(conn, plug_opts) + assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) + assert {0, 5} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts) + end + + test "it supports combination of options modifying bucket name" do + limiter_name = :test_options_combo + Config.put([:rate_limit, limiter_name], {1000, 5}) + Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + + base_bucket_name = "#{limiter_name}:group1" + + plug_opts = + RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name, params: ["id"]) + + id = "100" + + conn = build_conn(:get, "/?id=#{id}") + conn = Conn.fetch_query_params(conn) + conn_2 = build_conn(:get, "/?id=#{101}") + + RateLimiter.call(conn, plug_opts) + assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, plug_opts) + assert {0, 5} = RateLimiter.inspect_bucket(conn_2, base_bucket_name, plug_opts) + end + end + + describe "unauthenticated users" do + test "are restricted based on remote IP" do + limiter_name = :test_unauthenticated + Config.put([:rate_limit, limiter_name], [{1000, 5}, {1, 10}]) + Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + + plug_opts = RateLimiter.init(name: limiter_name) + + conn = %{build_conn(:get, "/") | remote_ip: {127, 0, 0, 2}} + conn_2 = %{build_conn(:get, "/") | remote_ip: {127, 0, 0, 3}} + + for i <- 1..5 do + conn = RateLimiter.call(conn, plug_opts) + assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) + refute conn.halted + end + + conn = RateLimiter.call(conn, plug_opts) + + assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests) + assert conn.halted + + conn_2 = RateLimiter.call(conn_2, plug_opts) + assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts) + + refute conn_2.status == Conn.Status.code(:too_many_requests) + refute conn_2.resp_body + refute conn_2.halted + end + end + + describe "authenticated users" do + setup do + Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo) + + :ok + end + + test "can have limits separate from unauthenticated connections" do + limiter_name = :test_authenticated1 + + scale = 50 + limit = 5 + Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + Config.put([:rate_limit, limiter_name], [{1000, 1}, {scale, limit}]) + + plug_opts = RateLimiter.init(name: limiter_name) + + user = insert(:user) + conn = build_conn(:get, "/") |> assign(:user, user) + + for i <- 1..5 do + conn = RateLimiter.call(conn, plug_opts) + assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) + refute conn.halted + end + + conn = RateLimiter.call(conn, plug_opts) + + assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests) + assert conn.halted + end + + test "different users are counted independently" do + limiter_name = :test_authenticated2 + Config.put([:rate_limit, limiter_name], [{1, 10}, {1000, 5}]) + Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + + plug_opts = RateLimiter.init(name: limiter_name) + + user = insert(:user) + conn = build_conn(:get, "/") |> assign(:user, user) + + user_2 = insert(:user) + conn_2 = build_conn(:get, "/") |> assign(:user, user_2) + + for i <- 1..5 do + conn = RateLimiter.call(conn, plug_opts) + assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) + end + + conn = RateLimiter.call(conn, plug_opts) + assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests) + assert conn.halted + + conn_2 = RateLimiter.call(conn_2, plug_opts) + assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts) + refute conn_2.status == Conn.Status.code(:too_many_requests) + refute conn_2.resp_body + refute conn_2.halted + end + end + + test "doesn't crash due to a race condition when multiple requests are made at the same time and the bucket is not yet initialized" do + limiter_name = :test_race_condition + Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5}) + Pleroma.Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + + opts = RateLimiter.init(name: limiter_name) + + conn = build_conn(:get, "/") + conn_2 = build_conn(:get, "/") + + %Task{pid: pid1} = + task1 = + Task.async(fn -> + receive do + :process2_up -> + RateLimiter.call(conn, opts) + end + end) + + task2 = + Task.async(fn -> + send(pid1, :process2_up) + RateLimiter.call(conn_2, opts) + end) + + Task.await(task1) + Task.await(task2) + + refute {:err, :not_found} == RateLimiter.inspect_bucket(conn, limiter_name, opts) + end +end diff --git a/test/pleroma/web/plugs/remote_ip_test.exs b/test/pleroma/web/plugs/remote_ip_test.exs @@ -0,0 +1,108 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.RemoteIpTest do + use ExUnit.Case + use Plug.Test + + alias Pleroma.Web.Plugs.RemoteIp + + import Pleroma.Tests.Helpers, only: [clear_config: 2] + + setup do: + clear_config(RemoteIp, + enabled: true, + headers: ["x-forwarded-for"], + proxies: [], + reserved: [ + "127.0.0.0/8", + "::1/128", + "fc00::/7", + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16" + ] + ) + + test "disabled" do + Pleroma.Config.put(RemoteIp, enabled: false) + + %{remote_ip: remote_ip} = conn(:get, "/") + + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "1.1.1.1") + |> RemoteIp.call(nil) + + assert conn.remote_ip == remote_ip + end + + test "enabled" do + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "1.1.1.1") + |> RemoteIp.call(nil) + + assert conn.remote_ip == {1, 1, 1, 1} + end + + test "custom headers" do + Pleroma.Config.put(RemoteIp, enabled: true, headers: ["cf-connecting-ip"]) + + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "1.1.1.1") + |> RemoteIp.call(nil) + + refute conn.remote_ip == {1, 1, 1, 1} + + conn = + conn(:get, "/") + |> put_req_header("cf-connecting-ip", "1.1.1.1") + |> RemoteIp.call(nil) + + assert conn.remote_ip == {1, 1, 1, 1} + end + + test "custom proxies" do + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2") + |> RemoteIp.call(nil) + + refute conn.remote_ip == {1, 1, 1, 1} + + Pleroma.Config.put([RemoteIp, :proxies], ["173.245.48.0/20"]) + + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2") + |> RemoteIp.call(nil) + + assert conn.remote_ip == {1, 1, 1, 1} + end + + test "proxies set without CIDR format" do + Pleroma.Config.put([RemoteIp, :proxies], ["173.245.48.1"]) + + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1") + |> RemoteIp.call(nil) + + assert conn.remote_ip == {1, 1, 1, 1} + end + + test "proxies set `nonsensical` CIDR" do + Pleroma.Config.put([RemoteIp, :reserved], ["127.0.0.0/8"]) + Pleroma.Config.put([RemoteIp, :proxies], ["10.0.0.3/24"]) + + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "10.0.0.3, 1.1.1.1") + |> RemoteIp.call(nil) + + assert conn.remote_ip == {1, 1, 1, 1} + end +end diff --git a/test/pleroma/web/plugs/session_authentication_plug_test.exs b/test/pleroma/web/plugs/session_authentication_plug_test.exs @@ -0,0 +1,63 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.SessionAuthenticationPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.User + alias Pleroma.Web.Plugs.SessionAuthenticationPlug + + setup %{conn: conn} do + session_opts = [ + store: :cookie, + key: "_test", + signing_salt: "cooldude" + ] + + conn = + conn + |> Plug.Session.call(Plug.Session.init(session_opts)) + |> fetch_session + |> assign(:auth_user, %User{id: 1}) + + %{conn: conn} + end + + test "it does nothing if a user is assigned", %{conn: conn} do + conn = + conn + |> assign(:user, %User{}) + + ret_conn = + conn + |> SessionAuthenticationPlug.call(%{}) + + assert ret_conn == conn + end + + test "if the auth_user has the same id as the user_id in the session, it assigns the user", %{ + conn: conn + } do + conn = + conn + |> put_session(:user_id, conn.assigns.auth_user.id) + |> SessionAuthenticationPlug.call(%{}) + + assert conn.assigns.user == conn.assigns.auth_user + end + + test "if the auth_user has a different id as the user_id in the session, it does nothing", %{ + conn: conn + } do + conn = + conn + |> put_session(:user_id, -1) + + ret_conn = + conn + |> SessionAuthenticationPlug.call(%{}) + + assert ret_conn == conn + end +end diff --git a/test/pleroma/web/plugs/set_format_plug_test.exs b/test/pleroma/web/plugs/set_format_plug_test.exs @@ -0,0 +1,38 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.SetFormatPlugTest do + use ExUnit.Case, async: true + use Plug.Test + + alias Pleroma.Web.Plugs.SetFormatPlug + + test "set format from params" do + conn = + :get + |> conn("/cofe?_format=json") + |> SetFormatPlug.call([]) + + assert %{format: "json"} == conn.assigns + end + + test "set format from header" do + conn = + :get + |> conn("/cofe") + |> put_private(:phoenix_format, "xml") + |> SetFormatPlug.call([]) + + assert %{format: "xml"} == conn.assigns + end + + test "doesn't set format" do + conn = + :get + |> conn("/cofe") + |> SetFormatPlug.call([]) + + refute conn.assigns[:format] + end +end diff --git a/test/pleroma/web/plugs/set_locale_plug_test.exs b/test/pleroma/web/plugs/set_locale_plug_test.exs @@ -0,0 +1,46 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.SetLocalePlugTest do + use ExUnit.Case, async: true + use Plug.Test + + alias Pleroma.Web.Plugs.SetLocalePlug + alias Plug.Conn + + test "default locale is `en`" do + conn = + :get + |> conn("/cofe") + |> SetLocalePlug.call([]) + + assert "en" == Gettext.get_locale() + assert %{locale: "en"} == conn.assigns + end + + test "use supported locale from `accept-language`" do + conn = + :get + |> conn("/cofe") + |> Conn.put_req_header( + "accept-language", + "ru, fr-CH, fr;q=0.9, en;q=0.8, *;q=0.5" + ) + |> SetLocalePlug.call([]) + + assert "ru" == Gettext.get_locale() + assert %{locale: "ru"} == conn.assigns + end + + test "use default locale if locale from `accept-language` is not supported" do + conn = + :get + |> conn("/cofe") + |> Conn.put_req_header("accept-language", "tlh") + |> SetLocalePlug.call([]) + + assert "en" == Gettext.get_locale() + assert %{locale: "en"} == conn.assigns + end +end diff --git a/test/pleroma/web/plugs/set_user_session_id_plug_test.exs b/test/pleroma/web/plugs/set_user_session_id_plug_test.exs @@ -0,0 +1,45 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.SetUserSessionIdPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.User + alias Pleroma.Web.Plugs.SetUserSessionIdPlug + + setup %{conn: conn} do + session_opts = [ + store: :cookie, + key: "_test", + signing_salt: "cooldude" + ] + + conn = + conn + |> Plug.Session.call(Plug.Session.init(session_opts)) + |> fetch_session + + %{conn: conn} + end + + test "doesn't do anything if the user isn't set", %{conn: conn} do + ret_conn = + conn + |> SetUserSessionIdPlug.call(%{}) + + assert ret_conn == conn + end + + test "sets the user_id in the session to the user id of the user assign", %{conn: conn} do + Code.ensure_compiled(Pleroma.User) + + conn = + conn + |> assign(:user, %User{id: 1}) + |> SetUserSessionIdPlug.call(%{}) + + id = get_session(conn, :user_id) + assert id == 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 @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.UploadedMediaPlugTest do + use Pleroma.Web.ConnCase + alias Pleroma.Upload + + defp upload_file(context) do + Pleroma.DataCase.ensure_local_uploader(context) + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "nice_tf.jpg" + } + + {:ok, data} = Upload.store(file) + [%{"href" => attachment_url} | _] = data["url"] + [attachment_url: attachment_url] + end + + setup_all :upload_file + + test "does not send Content-Disposition header when name param is not set", %{ + attachment_url: attachment_url + } do + conn = get(build_conn(), attachment_url) + refute Enum.any?(conn.resp_headers, &(elem(&1, 0) == "content-disposition")) + end + + test "sends Content-Disposition header when name param is set", %{ + attachment_url: attachment_url + } do + conn = get(build_conn(), attachment_url <> "?name=\"cofe\".gif") + + assert Enum.any?( + conn.resp_headers, + &(&1 == {"content-disposition", "filename=\"\\\"cofe\\\".gif\""}) + ) + end +end diff --git a/test/pleroma/web/plugs/user_enabled_plug_test.exs b/test/pleroma/web/plugs/user_enabled_plug_test.exs @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.UserEnabledPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Web.Plugs.UserEnabledPlug + import Pleroma.Factory + + setup do: clear_config([:instance, :account_activation_required]) + + test "doesn't do anything if the user isn't set", %{conn: conn} do + ret_conn = + conn + |> UserEnabledPlug.call(%{}) + + assert ret_conn == conn + end + + test "with a user that's not confirmed and a config requiring confirmation, it removes that user", + %{conn: conn} do + Pleroma.Config.put([:instance, :account_activation_required], true) + + user = insert(:user, confirmation_pending: true) + + conn = + conn + |> assign(:user, user) + |> UserEnabledPlug.call(%{}) + + assert conn.assigns.user == nil + end + + test "with a user that is deactivated, it removes that user", %{conn: conn} do + user = insert(:user, deactivated: true) + + conn = + conn + |> assign(:user, user) + |> UserEnabledPlug.call(%{}) + + assert conn.assigns.user == nil + end + + test "with a user that is not deactivated, it does nothing", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + + ret_conn = + conn + |> UserEnabledPlug.call(%{}) + + assert conn == ret_conn + end +end diff --git a/test/pleroma/web/plugs/user_fetcher_plug_test.exs b/test/pleroma/web/plugs/user_fetcher_plug_test.exs @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.UserFetcherPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Web.Plugs.UserFetcherPlug + import Pleroma.Factory + + setup do + user = insert(:user) + %{user: user} + end + + test "if an auth_credentials assign is present, it tries to fetch the user and assigns it", %{ + conn: conn, + user: user + } do + conn = + conn + |> assign(:auth_credentials, %{ + username: user.nickname, + password: nil + }) + + conn = + conn + |> UserFetcherPlug.call(%{}) + + assert conn.assigns[:auth_user] == user + end + + test "without a credential assign it doesn't do anything", %{conn: conn} do + ret_conn = + conn + |> UserFetcherPlug.call(%{}) + + assert conn == ret_conn + end +end diff --git a/test/pleroma/web/plugs/user_is_admin_plug_test.exs b/test/pleroma/web/plugs/user_is_admin_plug_test.exs @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.UserIsAdminPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Web.Plugs.UserIsAdminPlug + 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 = UserIsAdminPlug.call(conn, %{}) + + assert conn == ret_conn + end + + test "denies a user that isn't an admin" do + user = insert(:user) + + conn = + build_conn() + |> assign(:user, user) + |> UserIsAdminPlug.call(%{}) + + assert conn.status == 403 + end + + test "denies when a user isn't set" do + conn = UserIsAdminPlug.call(build_conn(), %{}) + + assert conn.status == 403 + end +end diff --git a/test/pleroma/web/preload/providers/instance_test.exs b/test/pleroma/web/preload/providers/instance_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.InstanceTest do + use Pleroma.DataCase + alias Pleroma.Web.Preload.Providers.Instance + + setup do: {:ok, Instance.generate_terms(nil)} + + test "it renders the info", %{"/api/v1/instance" => info} do + assert %{ + description: description, + email: "admin@example.com", + registrations: true + } = info + + assert String.equivalent?(description, "Pleroma: An efficient and flexible fediverse server") + end + + test "it renders the panel", %{"/instance/panel.html" => panel} do + assert String.contains?( + panel, + "<p>Welcome to <a href=\"https://pleroma.social\" target=\"_blank\">Pleroma!</a></p>" + ) + end + + test "it works with overrides" do + clear_config([:instance, :static_dir], "test/fixtures/preload_static") + + %{"/instance/panel.html" => panel} = Instance.generate_terms(nil) + + assert String.contains?( + panel, + "HEY!" + ) + end + + test "it renders the node_info", %{"/nodeinfo/2.0.json" => nodeinfo} do + %{ + metadata: metadata, + version: "2.0" + } = nodeinfo + + assert metadata.private == false + assert metadata.suggestions == %{enabled: false} + end + + test "it renders the frontend configurations", %{ + "/api/pleroma/frontend_configurations" => fe_configs + } do + assert %{ + pleroma_fe: %{background: "/images/city.jpg", logo: "/static/logo.png"} + } = fe_configs + end +end diff --git a/test/pleroma/web/preload/providers/timeline_test.exs b/test/pleroma/web/preload/providers/timeline_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.TimelineTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Preload.Providers.Timelines + + @public_url "/api/v1/timelines/public" + + describe "unauthenticated timeliness when restricted" do + setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true) + setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true) + + test "return nothing" do + tl_data = Timelines.generate_terms(%{}) + + refute Map.has_key?(tl_data, "/api/v1/timelines/public") + end + end + + describe "unauthenticated timeliness when unrestricted" do + setup do: clear_config([:restrict_unauthenticated, :timelines, :local], false) + setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], false) + + setup do: {:ok, user: insert(:user)} + + test "returns the timeline when not restricted" do + assert Timelines.generate_terms(%{}) + |> Map.has_key?(@public_url) + end + + test "returns public items", %{user: user} do + {:ok, _} = CommonAPI.post(user, %{status: "it's post 1!"}) + {:ok, _} = CommonAPI.post(user, %{status: "it's post 2!"}) + {:ok, _} = CommonAPI.post(user, %{status: "it's post 3!"}) + + assert Timelines.generate_terms(%{}) + |> Map.fetch!(@public_url) + |> Enum.count() == 3 + end + + test "does not return non-public items", %{user: user} do + {:ok, _} = CommonAPI.post(user, %{status: "it's post 1!", visibility: "unlisted"}) + {:ok, _} = CommonAPI.post(user, %{status: "it's post 2!", visibility: "direct"}) + {:ok, _} = CommonAPI.post(user, %{status: "it's post 3!"}) + + assert Timelines.generate_terms(%{}) + |> Map.fetch!(@public_url) + |> Enum.count() == 1 + end + end +end diff --git a/test/pleroma/web/preload/providers/user_test.exs b/test/pleroma/web/preload/providers/user_test.exs @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.UserTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.Preload.Providers.User + + describe "returns empty when user doesn't exist" do + test "nil user specified" do + assert User.generate_terms(%{user: nil}) == %{} + end + + test "missing user specified" do + assert User.generate_terms(%{user: :not_a_user}) == %{} + end + end + + describe "specified user exists" do + setup do + user = insert(:user) + + terms = User.generate_terms(%{user: user}) + %{terms: terms, user: user} + end + + test "account is rendered", %{terms: terms, user: user} do + account = terms["/api/v1/accounts/#{user.id}"] + assert %{acct: user, username: user} = account + end + end +end diff --git a/test/pleroma/web/push/impl_test.exs b/test/pleroma/web/push/impl_test.exs @@ -0,0 +1,344 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Push.ImplTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Notification + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Push.Impl + alias Pleroma.Web.Push.Subscription + + setup do + Tesla.Mock.mock(fn + %{method: :post, url: "https://example.com/example/1234"} -> + %Tesla.Env{status: 200} + + %{method: :post, url: "https://example.com/example/not_found"} -> + %Tesla.Env{status: 400} + + %{method: :post, url: "https://example.com/example/bad"} -> + %Tesla.Env{status: 100} + end) + + :ok + end + + @sub %{ + endpoint: "https://example.com/example/1234", + keys: %{ + auth: "8eDyX_uCN0XRhSbY5hs7Hg==", + p256dh: + "BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=" + } + } + @api_key "BASgACIHpN1GYgzSRp" + @message "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." + + test "performs sending notifications" do + user = insert(:user) + user2 = insert(:user) + insert(:push_subscription, user: user, data: %{alerts: %{"mention" => true}}) + insert(:push_subscription, user: user2, data: %{alerts: %{"mention" => true}}) + + insert(:push_subscription, + user: user, + data: %{alerts: %{"follow" => true, "mention" => true}} + ) + + insert(:push_subscription, + user: user, + data: %{alerts: %{"follow" => true, "mention" => false}} + ) + + {:ok, activity} = CommonAPI.post(user, %{status: "<Lorem ipsum dolor sit amet."}) + + notif = + insert(:notification, + user: user, + activity: activity, + type: "mention" + ) + + assert Impl.perform(notif) == {:ok, [:ok, :ok]} + end + + @tag capture_log: true + test "returns error if notif does not match " do + assert Impl.perform(%{}) == {:error, :unknown_type} + end + + test "successful message sending" do + assert Impl.push_message(@message, @sub, @api_key, %Subscription{}) == :ok + end + + @tag capture_log: true + test "fail message sending" do + assert Impl.push_message( + @message, + Map.merge(@sub, %{endpoint: "https://example.com/example/bad"}), + @api_key, + %Subscription{} + ) == :error + end + + test "delete subscription if result send message between 400..500" do + subscription = insert(:push_subscription) + + assert Impl.push_message( + @message, + Map.merge(@sub, %{endpoint: "https://example.com/example/not_found"}), + @api_key, + subscription + ) == :ok + + refute Pleroma.Repo.get(Subscription, subscription.id) + end + + test "deletes subscription when token has been deleted" do + subscription = insert(:push_subscription) + + Pleroma.Repo.delete(subscription.token) + + refute Pleroma.Repo.get(Subscription, subscription.id) + end + + test "renders title and body for create activity" do + user = insert(:user, nickname: "Bob") + + {:ok, activity} = + CommonAPI.post(user, %{ + status: + "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." + }) + + object = Object.normalize(activity) + + assert Impl.format_body( + %{ + activity: activity + }, + user, + object + ) == + "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." + + assert Impl.format_title(%{activity: activity, type: "mention"}) == + "New Mention" + end + + test "renders title and body for follow activity" do + user = insert(:user, nickname: "Bob") + other_user = insert(:user) + {:ok, _, _, activity} = CommonAPI.follow(user, other_user) + object = Object.normalize(activity, false) + + assert Impl.format_body(%{activity: activity, type: "follow"}, user, object) == + "@Bob has followed you" + + assert Impl.format_title(%{activity: activity, type: "follow"}) == + "New Follower" + end + + test "renders title and body for announce activity" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: + "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." + }) + + {:ok, announce_activity} = CommonAPI.repeat(activity.id, user) + object = Object.normalize(activity) + + assert Impl.format_body(%{activity: announce_activity}, user, object) == + "@#{user.nickname} repeated: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." + + assert Impl.format_title(%{activity: announce_activity, type: "reblog"}) == + "New Repeat" + end + + test "renders title and body for like activity" do + user = insert(:user, nickname: "Bob") + + {:ok, activity} = + CommonAPI.post(user, %{ + status: + "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." + }) + + {:ok, activity} = CommonAPI.favorite(user, activity.id) + object = Object.normalize(activity) + + assert Impl.format_body(%{activity: activity, type: "favourite"}, user, object) == + "@Bob has favorited your post" + + assert Impl.format_title(%{activity: activity, type: "favourite"}) == + "New Favorite" + end + + test "renders title for create activity with direct visibility" do + user = insert(:user, nickname: "Bob") + + {:ok, activity} = + CommonAPI.post(user, %{ + visibility: "direct", + status: "This is just between you and me, pal" + }) + + assert Impl.format_title(%{activity: activity}) == + "New Direct Message" + end + + describe "build_content/3" do + test "builds content for chat messages" do + user = insert(:user) + recipient = insert(:user) + + {:ok, chat} = CommonAPI.post_chat_message(user, recipient, "hey") + object = Object.normalize(chat, false) + [notification] = Notification.for_user(recipient) + + res = Impl.build_content(notification, user, object) + + assert res == %{ + body: "@#{user.nickname}: hey", + title: "New Chat Message" + } + end + + test "builds content for chat messages with no content" do + user = insert(:user) + recipient = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + + {:ok, chat} = CommonAPI.post_chat_message(user, recipient, nil, media_id: upload.id) + object = Object.normalize(chat, false) + [notification] = Notification.for_user(recipient) + + res = Impl.build_content(notification, user, object) + + assert res == %{ + body: "@#{user.nickname}: (Attachment)", + title: "New Chat Message" + } + end + + test "hides contents of notifications when option enabled" do + user = insert(:user, nickname: "Bob") + + user2 = + insert(:user, nickname: "Rob", notification_settings: %{hide_notification_contents: true}) + + {:ok, activity} = + CommonAPI.post(user, %{ + visibility: "direct", + status: "<Lorem ipsum dolor sit amet." + }) + + notif = insert(:notification, user: user2, activity: activity) + + actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) + object = Object.normalize(activity) + + assert Impl.build_content(notif, actor, object) == %{ + body: "New Direct Message" + } + + {:ok, activity} = + CommonAPI.post(user, %{ + visibility: "public", + status: "<Lorem ipsum dolor sit amet." + }) + + notif = insert(:notification, user: user2, activity: activity, type: "mention") + + actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) + object = Object.normalize(activity) + + assert Impl.build_content(notif, actor, object) == %{ + body: "New Mention" + } + + {:ok, activity} = CommonAPI.favorite(user, activity.id) + + notif = insert(:notification, user: user2, activity: activity, type: "favourite") + + actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) + object = Object.normalize(activity) + + assert Impl.build_content(notif, actor, object) == %{ + body: "New Favorite" + } + end + + test "returns regular content when hiding contents option disabled" do + user = insert(:user, nickname: "Bob") + + user2 = + insert(:user, nickname: "Rob", notification_settings: %{hide_notification_contents: false}) + + {:ok, activity} = + CommonAPI.post(user, %{ + visibility: "direct", + status: + "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." + }) + + notif = insert(:notification, user: user2, activity: activity) + + actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) + object = Object.normalize(activity) + + assert Impl.build_content(notif, actor, object) == %{ + body: + "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...", + title: "New Direct Message" + } + + {:ok, activity} = + CommonAPI.post(user, %{ + visibility: "public", + status: + "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." + }) + + notif = insert(:notification, user: user2, activity: activity, type: "mention") + + actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) + object = Object.normalize(activity) + + assert Impl.build_content(notif, actor, object) == %{ + body: + "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...", + title: "New Mention" + } + + {:ok, activity} = CommonAPI.favorite(user, activity.id) + + notif = insert(:notification, user: user2, activity: activity, type: "favourite") + + actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) + object = Object.normalize(activity) + + assert Impl.build_content(notif, actor, object) == %{ + body: "@Bob has favorited your post", + title: "New Favorite" + } + end + end +end diff --git a/test/pleroma/web/rel_me_test.exs b/test/pleroma/web/rel_me_test.exs @@ -0,0 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RelMeTest do + use ExUnit.Case + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "parse/1" do + hrefs = ["https://social.example.org/users/lain"] + + assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/null") == {:ok, []} + + assert {:ok, %Tesla.Env{status: 404}} = + Pleroma.Web.RelMe.parse("http://example.com/rel_me/error") + + assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/link") == {:ok, hrefs} + assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/anchor") == {:ok, hrefs} + assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/anchor_nofollow") == {:ok, hrefs} + end + + test "maybe_put_rel_me/2" do + profile_urls = ["https://social.example.org/users/lain"] + attr = "me" + fallback = nil + + assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/null", profile_urls) == + fallback + + assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/error", profile_urls) == + fallback + + assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/anchor", profile_urls) == + attr + + assert Pleroma.Web.RelMe.maybe_put_rel_me( + "http://example.com/rel_me/anchor_nofollow", + profile_urls + ) == attr + + assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/link", profile_urls) == + attr + end +end diff --git a/test/pleroma/web/rich_media/helpers_test.exs b/test/pleroma/web/rich_media/helpers_test.exs @@ -0,0 +1,85 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.HelpersTest do + use Pleroma.DataCase + + alias Pleroma.Config + 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" + }) + + Config.put([: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" + }) + + Config.put([: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" + }) + + Config.put([: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"}) + + Config.put([: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 @@ -0,0 +1,82 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do + use ExUnit.Case, async: true + + test "s3 signed url is parsed correct for expiration time" do + url = "https://pleroma.social/amz" + + {:ok, timestamp} = + Timex.now() + |> DateTime.truncate(:second) + |> Timex.format("{ISO:Basic:Z}") + + # in seconds + valid_till = 30 + + metadata = construct_metadata(timestamp, valid_till, url) + + 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) + end + + test "s3 signed url is parsed and correct ttl is set for rich media" do + url = "https://pleroma.social/amz" + + {:ok, timestamp} = + Timex.now() + |> DateTime.truncate(:second) + |> Timex.format("{ISO:Basic:Z}") + + # in seconds + valid_till = 30 + + metadata = construct_metadata(timestamp, valid_till, url) + + body = """ + <meta name="twitter:card" content="Pleroma" /> + <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)}" /> + """ + + Tesla.Mock.mock(fn + %{ + method: :get, + url: "https://pleroma.social/amz" + } -> + %Tesla.Env{status: 200, body: body} + end) + + Cachex.put(:rich_media_cache, url, metadata) + + Pleroma.Web.RichMedia.Parser.set_ttl_based_on_image(metadata, url) + + {:ok, cache_ttl} = Cachex.ttl(:rich_media_cache, url) + + # 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) + end + + defp construct_s3_url(timestamp, valid_till) do + "https://pleroma.s3.ap-southeast-1.amazonaws.com/sachin%20%281%29%20_a%20-%25%2Aasdasd%20BNN%20bnnn%20.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIBLWWK6RGDQXDLJQ%2F20190716%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=#{ + timestamp + }&X-Amz-Expires=#{valid_till}&X-Amz-Signature=04ffd6b98634f4b1bbabc62e0fac4879093cd54a6eed24fe8eb38e8369526bbf&X-Amz-SignedHeaders=host" + end + + defp construct_metadata(timestamp, valid_till, url) do + %{ + 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_test.exs b/test/pleroma/web/rich_media/parser_test.exs @@ -0,0 +1,176 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.ParserTest do + use ExUnit.Case, async: true + + 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) + + :ok + end + + test "returns error when no metadata present" do + assert {:error, _} = Parser.parse("http://example.com/empty") + end + + test "doesn't just add a title" do + assert {:error, {:invalid_metadata, _}} = Parser.parse("http://example.com/non-ogp") + end + + test "parses ogp" do + assert Parser.parse("http://example.com/ogp") == + {:ok, + %{ + "image" => "http://ia.media-imdb.com/images/rock.jpg", + "title" => "The Rock", + "description" => + "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", + "type" => "video.movie", + "url" => "http://example.com/ogp" + }} + end + + test "falls back to <title> when ogp:title is missing" do + assert Parser.parse("http://example.com/ogp-missing-title") == + {:ok, + %{ + "image" => "http://ia.media-imdb.com/images/rock.jpg", + "title" => "The Rock (1996)", + "description" => + "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", + "type" => "video.movie", + "url" => "http://example.com/ogp-missing-title" + }} + end + + test "parses twitter card" do + assert Parser.parse("http://example.com/twitter-card") == + {:ok, + %{ + "card" => "summary", + "site" => "@flickr", + "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" + }} + end + + test "parses OEmbed" do + assert Parser.parse("http://example.com/oembed") == + {:ok, + %{ + "author_name" => "‮‭‬bees‬", + "author_url" => "https://www.flickr.com/photos/bees/", + "cache_age" => 3600, + "flickr_type" => "photo", + "height" => "768", + "html" => + "<a data-flickr-embed=\"true\" href=\"https://www.flickr.com/photos/bees/2362225867/\" title=\"Bacon Lollys by ‮‭‬bees‬, 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>", + "license" => "All Rights Reserved", + "license_id" => 0, + "provider_name" => "Flickr", + "provider_url" => "https://www.flickr.com/", + "thumbnail_height" => 150, + "thumbnail_url" => + "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg", + "thumbnail_width" => 150, + "title" => "Bacon Lollys", + "type" => "photo", + "url" => "http://example.com/oembed", + "version" => "1.0", + "web_page" => "https://www.flickr.com/photos/bees/2362225867/", + "web_page_short_url" => "https://flic.kr/p/4AK2sc", + "width" => "1024" + }} + end + + test "rejects invalid OGP data" do + assert {:error, _} = Parser.parse("http://example.com/malformed") + end + + test "returns error if getting page was not successful" do + assert {:error, :overload} = Parser.parse("http://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") + 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") + end +end diff --git a/test/pleroma/web/rich_media/parsers/twitter_card_test.exs b/test/pleroma/web/rich_media/parsers/twitter_card_test.exs @@ -0,0 +1,127 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do + use ExUnit.Case, async: true + alias Pleroma.Web.RichMedia.Parsers.TwitterCard + + test "returns error when html not contains twitter card" do + assert TwitterCard.parse([{"html", [], [{"head", [], []}, {"body", [], []}]}], %{}) == %{} + end + + test "parses twitter card with only name attributes" do + html = + File.read!("test/fixtures/nypd-facial-recognition-children-teenagers3.html") + |> Floki.parse_document!() + + assert TwitterCard.parse(html, %{}) == + %{ + "app:id:googleplay" => "com.nytimes.android", + "app:name:googleplay" => "NYTimes", + "app:url:googleplay" => "nytimes://reader/id/100000006583622", + "site" => nil, + "description" => + "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", + "image" => + "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg", + "type" => "article", + "url" => + "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", + "title" => + "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database." + } + end + + test "parses twitter card with only property attributes" do + html = + File.read!("test/fixtures/nypd-facial-recognition-children-teenagers2.html") + |> Floki.parse_document!() + + assert TwitterCard.parse(html, %{}) == + %{ + "card" => "summary_large_image", + "description" => + "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", + "image" => + "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", + "image:alt" => "", + "title" => + "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", + "url" => + "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", + "type" => "article" + } + end + + test "parses twitter card with name & property attributes" do + html = + File.read!("test/fixtures/nypd-facial-recognition-children-teenagers.html") + |> Floki.parse_document!() + + assert TwitterCard.parse(html, %{}) == + %{ + "app:id:googleplay" => "com.nytimes.android", + "app:name:googleplay" => "NYTimes", + "app:url:googleplay" => "nytimes://reader/id/100000006583622", + "card" => "summary_large_image", + "description" => + "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", + "image" => + "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", + "image:alt" => "", + "site" => nil, + "title" => + "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", + "url" => + "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", + "type" => "article" + } + end + + test "respect only first title tag on the page" do + image_path = + "https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzLzkwYzgyMzI4LThlMDUtNGRiNS05MDg3LTUzMGUxZTM5N2RmMmVkOTM5ZDM4MGM4OTIx" <> + "YTQ5MF9EQVIgZXhodW1hdGlvbiBvZiBNYXJnYXJldCBDb3JiaW4gZ3JhdmUgMTkyNi5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9" <> + "yaWVudCJdLFsicCIsInRodW1iIiwiNjAweD4iXV0/DAR%20exhumation%20of%20Margaret%20Corbin%20grave%201926.jpg" + + html = + File.read!("test/fixtures/margaret-corbin-grave-west-point.html") |> Floki.parse_document!() + + assert TwitterCard.parse(html, %{}) == + %{ + "site" => "@atlasobscura", + "title" => "The Missing Grave of Margaret Corbin, Revolutionary War Veteran", + "card" => "summary_large_image", + "image" => image_path, + "description" => + "She's the only woman veteran honored with a monument at West Point. But where was she buried?", + "site_name" => "Atlas Obscura", + "type" => "article", + "url" => "http://www.atlasobscura.com/articles/margaret-corbin-grave-west-point" + } + end + + test "takes first founded title in html head if there is html markup error" do + html = + File.read!("test/fixtures/nypd-facial-recognition-children-teenagers4.html") + |> Floki.parse_document!() + + assert TwitterCard.parse(html, %{}) == + %{ + "site" => nil, + "title" => + "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", + "app:id:googleplay" => "com.nytimes.android", + "app:name:googleplay" => "NYTimes", + "app:url:googleplay" => "nytimes://reader/id/100000006583622", + "description" => + "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", + "image" => + "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg", + "type" => "article", + "url" => + "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" + } + end +end diff --git a/test/pleroma/web/static_fe/static_fe_controller_test.exs b/test/pleroma/web/static_fe/static_fe_controller_test.exs @@ -0,0 +1,196 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + setup_all do: clear_config([:static_fe, :enabled], true) + setup do: clear_config([:instance, :federating], true) + + setup %{conn: conn} do + conn = put_req_header(conn, "accept", "text/html") + user = insert(:user) + + %{conn: conn, user: user} + end + + describe "user profile html" do + test "just the profile as HTML", %{conn: conn, user: user} do + conn = get(conn, "/users/#{user.nickname}") + + assert html_response(conn, 200) =~ user.nickname + end + + test "404 when user not found", %{conn: conn} do + conn = get(conn, "/users/limpopo") + + assert html_response(conn, 404) =~ "not found" + end + + test "profile does not include private messages", %{conn: conn, user: user} do + CommonAPI.post(user, %{status: "public"}) + CommonAPI.post(user, %{status: "private", visibility: "private"}) + + conn = get(conn, "/users/#{user.nickname}") + + html = html_response(conn, 200) + + assert html =~ ">public<" + refute html =~ ">private<" + end + + test "pagination", %{conn: conn, user: user} do + Enum.map(1..30, fn i -> CommonAPI.post(user, %{status: "test#{i}"}) end) + + conn = get(conn, "/users/#{user.nickname}") + + html = html_response(conn, 200) + + assert html =~ ">test30<" + assert html =~ ">test11<" + refute html =~ ">test10<" + refute html =~ ">test1<" + end + + test "pagination, page 2", %{conn: conn, user: user} do + activities = Enum.map(1..30, fn i -> CommonAPI.post(user, %{status: "test#{i}"}) end) + {:ok, a11} = Enum.at(activities, 11) + + conn = get(conn, "/users/#{user.nickname}?max_id=#{a11.id}") + + html = html_response(conn, 200) + + assert html =~ ">test1<" + assert html =~ ">test10<" + refute html =~ ">test20<" + refute html =~ ">test29<" + end + + test "it requires authentication if instance is NOT federating", %{conn: conn, user: user} do + ensure_federating_or_authenticated(conn, "/users/#{user.nickname}", user) + end + end + + describe "notice html" do + test "single notice page", %{conn: conn, user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"}) + + conn = get(conn, "/notice/#{activity.id}") + + html = html_response(conn, 200) + assert html =~ "<header>" + assert html =~ user.nickname + assert html =~ "testing a thing!" + end + + test "redirects to json if requested", %{conn: conn, user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"}) + + conn = + conn + |> put_req_header( + "accept", + "Accept: application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\", text/html" + ) + |> get("/notice/#{activity.id}") + + assert redirected_to(conn, 302) =~ activity.data["object"] + end + + test "filters HTML tags", %{conn: conn} do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "<script>alert('xss')</script>"}) + + conn = + conn + |> put_req_header("accept", "text/html") + |> get("/notice/#{activity.id}") + + html = html_response(conn, 200) + assert html =~ ~s[&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;] + end + + test "shows the whole thread", %{conn: conn, user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "space: the final frontier"}) + + CommonAPI.post(user, %{ + status: "these are the voyages or something", + in_reply_to_status_id: activity.id + }) + + conn = get(conn, "/notice/#{activity.id}") + + html = html_response(conn, 200) + assert html =~ "the final frontier" + assert html =~ "voyages" + end + + test "redirect by AP object ID", %{conn: conn, user: user} do + {:ok, %Activity{data: %{"object" => object_url}}} = + CommonAPI.post(user, %{status: "beam me up"}) + + conn = get(conn, URI.parse(object_url).path) + + assert html_response(conn, 302) =~ "redirected" + end + + test "redirect by activity ID", %{conn: conn, user: user} do + {:ok, %Activity{data: %{"id" => id}}} = + CommonAPI.post(user, %{status: "I'm a doctor, not a devops!"}) + + conn = get(conn, URI.parse(id).path) + + assert html_response(conn, 302) =~ "redirected" + end + + test "404 when notice not found", %{conn: conn} do + conn = get(conn, "/notice/88c9c317") + + assert html_response(conn, 404) =~ "not found" + end + + test "404 for private status", %{conn: conn, user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "don't show me!", visibility: "private"}) + + conn = get(conn, "/notice/#{activity.id}") + + assert html_response(conn, 404) =~ "not found" + end + + test "302 for remote cached status", %{conn: conn, user: user} do + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => user.follower_address, + "cc" => "https://www.w3.org/ns/activitystreams#Public", + "type" => "Create", + "object" => %{ + "content" => "blah blah blah", + "type" => "Note", + "attributedTo" => user.ap_id, + "inReplyTo" => nil + }, + "actor" => user.ap_id + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + conn = get(conn, "/notice/#{activity.id}") + + assert html_response(conn, 302) =~ "redirected" + end + + test "it requires authentication if instance is NOT federating", %{conn: conn, user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"}) + + ensure_federating_or_authenticated(conn, "/notice/#{activity.id}", user) + end + end +end diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs @@ -0,0 +1,767 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.StreamerTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Conversation.Participation + alias Pleroma.List + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Streamer + alias Pleroma.Web.StreamerView + + @moduletag needs_streamer: true, capture_log: true + + setup do: clear_config([:instance, :skip_thread_containment]) + + describe "get_topic/_ (unauthenticated)" do + test "allows public" do + assert {:ok, "public"} = Streamer.get_topic("public", nil, nil) + assert {:ok, "public:local"} = Streamer.get_topic("public:local", nil, nil) + assert {:ok, "public:media"} = Streamer.get_topic("public:media", nil, nil) + assert {:ok, "public:local:media"} = Streamer.get_topic("public:local:media", nil, nil) + end + + test "allows hashtag streams" do + assert {:ok, "hashtag:cofe"} = Streamer.get_topic("hashtag", nil, nil, %{"tag" => "cofe"}) + end + + test "disallows user streams" do + assert {:error, _} = Streamer.get_topic("user", nil, nil) + assert {:error, _} = Streamer.get_topic("user:notification", nil, nil) + assert {:error, _} = Streamer.get_topic("direct", nil, nil) + end + + test "disallows list streams" do + assert {:error, _} = Streamer.get_topic("list", nil, nil, %{"list" => 42}) + end + end + + describe "get_topic/_ (authenticated)" do + setup do: oauth_access(["read"]) + + test "allows public streams (regardless of OAuth token scopes)", %{ + user: user, + token: read_oauth_token + } do + with oauth_token <- [nil, read_oauth_token] do + assert {:ok, "public"} = Streamer.get_topic("public", user, oauth_token) + assert {:ok, "public:local"} = Streamer.get_topic("public:local", user, oauth_token) + assert {:ok, "public:media"} = Streamer.get_topic("public:media", user, oauth_token) + + assert {:ok, "public:local:media"} = + Streamer.get_topic("public:local:media", user, oauth_token) + end + end + + test "allows user streams (with proper OAuth token scopes)", %{ + user: user, + token: read_oauth_token + } do + %{token: read_notifications_token} = oauth_access(["read:notifications"], user: user) + %{token: read_statuses_token} = oauth_access(["read:statuses"], user: user) + %{token: badly_scoped_token} = oauth_access(["irrelevant:scope"], user: user) + + expected_user_topic = "user:#{user.id}" + expected_notification_topic = "user:notification:#{user.id}" + expected_direct_topic = "direct:#{user.id}" + expected_pleroma_chat_topic = "user:pleroma_chat:#{user.id}" + + for valid_user_token <- [read_oauth_token, read_statuses_token] do + assert {:ok, ^expected_user_topic} = Streamer.get_topic("user", user, valid_user_token) + + assert {:ok, ^expected_direct_topic} = + Streamer.get_topic("direct", user, valid_user_token) + + assert {:ok, ^expected_pleroma_chat_topic} = + Streamer.get_topic("user:pleroma_chat", user, valid_user_token) + end + + for invalid_user_token <- [read_notifications_token, badly_scoped_token], + user_topic <- ["user", "direct", "user:pleroma_chat"] do + assert {:error, :unauthorized} = Streamer.get_topic(user_topic, user, invalid_user_token) + end + + for valid_notification_token <- [read_oauth_token, read_notifications_token] do + assert {:ok, ^expected_notification_topic} = + Streamer.get_topic("user:notification", user, valid_notification_token) + end + + for invalid_notification_token <- [read_statuses_token, badly_scoped_token] do + assert {:error, :unauthorized} = + Streamer.get_topic("user:notification", user, invalid_notification_token) + end + end + + test "allows hashtag streams (regardless of OAuth token scopes)", %{ + user: user, + token: read_oauth_token + } do + for oauth_token <- [nil, read_oauth_token] do + assert {:ok, "hashtag:cofe"} = + Streamer.get_topic("hashtag", user, oauth_token, %{"tag" => "cofe"}) + end + end + + test "disallows registering to another user's stream", %{user: user, token: read_oauth_token} do + another_user = insert(:user) + assert {:error, _} = Streamer.get_topic("user:#{another_user.id}", user, read_oauth_token) + + assert {:error, _} = + Streamer.get_topic("user:notification:#{another_user.id}", user, read_oauth_token) + + assert {:error, _} = Streamer.get_topic("direct:#{another_user.id}", user, read_oauth_token) + end + + test "allows list stream that are owned by the user (with `read` or `read:lists` scopes)", %{ + user: user, + token: read_oauth_token + } do + %{token: read_lists_token} = oauth_access(["read:lists"], user: user) + %{token: invalid_token} = oauth_access(["irrelevant:scope"], user: user) + {:ok, list} = List.create("Test", user) + + assert {:error, _} = Streamer.get_topic("list:#{list.id}", user, read_oauth_token) + + for valid_token <- [read_oauth_token, read_lists_token] do + assert {:ok, _} = Streamer.get_topic("list", user, valid_token, %{"list" => list.id}) + end + + assert {:error, _} = Streamer.get_topic("list", user, invalid_token, %{"list" => list.id}) + end + + test "disallows list stream that are not owned by the user", %{user: user, token: oauth_token} do + another_user = insert(:user) + {:ok, list} = List.create("Test", another_user) + + assert {:error, _} = Streamer.get_topic("list:#{list.id}", user, oauth_token) + assert {:error, _} = Streamer.get_topic("list", user, oauth_token, %{"list" => list.id}) + end + end + + describe "user streams" do + setup do + %{user: user, token: token} = oauth_access(["read"]) + notify = insert(:notification, user: user, activity: build(:note_activity)) + {:ok, %{user: user, notify: notify, token: token}} + end + + test "it streams the user's post in the 'user' stream", %{user: user, token: oauth_token} do + Streamer.get_topic_and_add_socket("user", user, oauth_token) + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + + assert_receive {:render_with_user, _, _, ^activity} + refute Streamer.filtered_by_user?(user, activity) + end + + test "it streams boosts of the user in the 'user' stream", %{user: user, token: oauth_token} do + Streamer.get_topic_and_add_socket("user", user, oauth_token) + + other_user = insert(:user) + {: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} + refute Streamer.filtered_by_user?(user, announce) + end + + test "it does not stream announces of the user's own posts in the 'user' stream", %{ + user: user, + token: oauth_token + } do + Streamer.get_topic_and_add_socket("user", user, oauth_token) + + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, announce} = CommonAPI.repeat(activity.id, other_user) + + assert Streamer.filtered_by_user?(user, announce) + end + + test "it does stream notifications announces of the user's own posts in the 'user' stream", %{ + user: user, + token: oauth_token + } do + Streamer.get_topic_and_add_socket("user", user, oauth_token) + + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, announce} = CommonAPI.repeat(activity.id, other_user) + + notification = + Pleroma.Notification + |> Repo.get_by(%{user_id: user.id, activity_id: announce.id}) + |> Repo.preload(:activity) + + refute Streamer.filtered_by_user?(user, notification) + end + + test "it streams boosts of mastodon user in the 'user' stream", %{ + user: user, + token: oauth_token + } do + Streamer.get_topic_and_add_socket("user", user, oauth_token) + + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) + + data = + File.read!("test/fixtures/mastodon-announce.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("actor", user.ap_id) + + {: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} + refute Streamer.filtered_by_user?(user, announce) + end + + test "it sends notify to in the 'user' stream", %{ + user: user, + token: oauth_token, + notify: notify + } do + Streamer.get_topic_and_add_socket("user", user, oauth_token) + Streamer.stream("user", notify) + + assert_receive {:render_with_user, _, _, ^notify} + refute Streamer.filtered_by_user?(user, notify) + end + + test "it sends notify to in the 'user:notification' stream", %{ + user: user, + token: oauth_token, + notify: notify + } do + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) + Streamer.stream("user:notification", notify) + + assert_receive {:render_with_user, _, _, ^notify} + refute Streamer.filtered_by_user?(user, notify) + end + + test "it sends chat messages to the 'user:pleroma_chat' stream", %{ + user: user, + token: oauth_token + } do + other_user = insert(:user) + + {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno") + object = Object.normalize(create_activity, false) + chat = Chat.get(user.id, other_user.ap_id) + cm_ref = MessageReference.for_chat_and_object(chat, object) + cm_ref = %{cm_ref | chat: chat, object: object} + + 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}) + + assert text =~ "hey cirno" + assert_receive {:text, ^text} + end + + test "it sends chat messages to the 'user' stream", %{user: user, token: oauth_token} do + other_user = insert(:user) + + {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno") + object = Object.normalize(create_activity, false) + chat = Chat.get(user.id, other_user.ap_id) + cm_ref = MessageReference.for_chat_and_object(chat, object) + cm_ref = %{cm_ref | chat: chat, object: object} + + 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}) + + assert text =~ "hey cirno" + assert_receive {:text, ^text} + end + + test "it sends chat message notifications to the 'user:notification' stream", %{ + user: user, + token: oauth_token + } do + other_user = insert(:user) + + {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey") + + notify = + Repo.get_by(Pleroma.Notification, user_id: user.id, activity_id: create_activity.id) + |> Repo.preload(:activity) + + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) + Streamer.stream("user:notification", notify) + + assert_receive {:render_with_user, _, _, ^notify} + refute Streamer.filtered_by_user?(user, notify) + end + + test "it doesn't send notify to the 'user:notification' stream when a user is blocked", %{ + user: user, + token: oauth_token + } do + blocked = insert(:user) + {:ok, _user_relationship} = User.block(user, blocked) + + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) + + {:ok, activity} = CommonAPI.post(user, %{status: ":("}) + {:ok, _} = CommonAPI.favorite(blocked, activity.id) + + refute_receive _ + end + + test "it doesn't send notify to the 'user:notification' stream when a thread is muted", %{ + user: user, + token: oauth_token + } do + user2 = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) + {:ok, _} = CommonAPI.add_mute(user, activity) + + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) + + {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) + + refute_receive _ + assert Streamer.filtered_by_user?(user, favorite_activity) + end + + test "it sends favorite to 'user:notification' stream'", %{ + user: user, + token: oauth_token + } do + user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"}) + + {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) + 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 notif.activity.id == favorite_activity.id + refute Streamer.filtered_by_user?(user, notif) + end + + test "it doesn't send the 'user:notification' stream' when a domain is blocked", %{ + user: user, + token: oauth_token + } do + user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"}) + + {:ok, user} = User.block_domain(user, "hecking-lewd-place.com") + {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) + {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) + + refute_receive _ + assert Streamer.filtered_by_user?(user, favorite_activity) + end + + test "it sends follow activities to the 'user:notification' stream", %{ + user: user, + token: oauth_token + } do + user_url = user.ap_id + user2 = insert(:user) + + body = + File.read!("test/fixtures/users_mock/localhost.json") + |> String.replace("{{nickname}}", user.nickname) + |> Jason.encode!() + + Tesla.Mock.mock_global(fn + %{method: :get, url: ^user_url} -> + %Tesla.Env{status: 200, body: body} + end) + + 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 notif.activity.id == follow_activity.id + refute Streamer.filtered_by_user?(user, notif) + end + end + + describe "public streams" do + test "it sends to public (authenticated)" do + %{user: user, token: oauth_token} = oauth_access(["read"]) + other_user = insert(:user) + + Streamer.get_topic_and_add_socket("public", user, oauth_token) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "Test"}) + assert_receive {:render_with_user, _, _, ^activity} + refute Streamer.filtered_by_user?(other_user, activity) + end + + test "it sends to public (unauthenticated)" do + user = insert(:user) + + Streamer.get_topic_and_add_socket("public", nil, nil) + + {:ok, activity} = CommonAPI.post(user, %{status: "Test"}) + activity_id = activity.id + assert_receive {:text, event} + assert %{"event" => "update", "payload" => payload} = Jason.decode!(event) + assert %{"id" => ^activity_id} = Jason.decode!(payload) + + {:ok, _} = CommonAPI.delete(activity.id, user) + assert_receive {:text, event} + assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event) + end + + test "handles deletions" do + %{user: user, token: oauth_token} = oauth_access(["read"]) + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(other_user, %{status: "Test"}) + + Streamer.get_topic_and_add_socket("public", user, oauth_token) + + {:ok, _} = CommonAPI.delete(activity.id, other_user) + activity_id = activity.id + assert_receive {:text, event} + assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event) + end + end + + describe "thread_containment/2" do + test "it filters to user if recipients invalid and thread containment is enabled" do + Pleroma.Config.put([:instance, :skip_thread_containment], false) + author = insert(:user) + %{user: user, token: oauth_token} = oauth_access(["read"]) + User.follow(user, author, :follow_accept) + + activity = + insert(:note_activity, + note: + insert(:note, + user: author, + data: %{"to" => ["TEST-FFF"]} + ) + ) + + Streamer.get_topic_and_add_socket("public", user, oauth_token) + Streamer.stream("public", activity) + assert_receive {:render_with_user, _, _, ^activity} + assert Streamer.filtered_by_user?(user, activity) + end + + test "it sends message if recipients invalid and thread containment is disabled" do + Pleroma.Config.put([:instance, :skip_thread_containment], true) + author = insert(:user) + %{user: user, token: oauth_token} = oauth_access(["read"]) + User.follow(user, author, :follow_accept) + + activity = + insert(:note_activity, + note: + insert(:note, + user: author, + data: %{"to" => ["TEST-FFF"]} + ) + ) + + Streamer.get_topic_and_add_socket("public", user, oauth_token) + Streamer.stream("public", activity) + + assert_receive {:render_with_user, _, _, ^activity} + refute Streamer.filtered_by_user?(user, activity) + end + + test "it sends message if recipients invalid and thread containment is enabled but user's thread containment is disabled" do + Pleroma.Config.put([:instance, :skip_thread_containment], false) + author = insert(:user) + user = insert(:user, skip_thread_containment: true) + %{token: oauth_token} = oauth_access(["read"], user: user) + User.follow(user, author, :follow_accept) + + activity = + insert(:note_activity, + note: + insert(:note, + user: author, + data: %{"to" => ["TEST-FFF"]} + ) + ) + + Streamer.get_topic_and_add_socket("public", user, oauth_token) + Streamer.stream("public", activity) + + assert_receive {:render_with_user, _, _, ^activity} + refute Streamer.filtered_by_user?(user, activity) + end + end + + describe "blocks" do + setup do: oauth_access(["read"]) + + test "it filters messages involving blocked users", %{user: user, token: oauth_token} do + blocked_user = insert(:user) + {:ok, _user_relationship} = User.block(user, blocked_user) + + 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 Streamer.filtered_by_user?(user, activity) + end + + test "it filters messages transitively involving blocked users", %{ + user: blocker, + token: blocker_token + } do + blockee = insert(:user) + friend = insert(:user) + + Streamer.get_topic_and_add_socket("public", blocker, blocker_token) + + {:ok, _user_relationship} = User.block(blocker, blockee) + + {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey! @#{blockee.nickname}"}) + + 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 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 Streamer.filtered_by_user?(blocker, activity_three) + end + end + + describe "lists" do + setup do: oauth_access(["read"]) + + test "it doesn't send unwanted DMs to list", %{user: user_a, token: user_a_token} do + user_b = insert(:user) + user_c = insert(:user) + + {:ok, user_a} = User.follow(user_a, user_b) + + {:ok, list} = List.create("Test", user_a) + {:ok, list} = List.follow(list, user_b) + + Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) + + {:ok, _activity} = + CommonAPI.post(user_b, %{ + status: "@#{user_c.nickname} Test", + visibility: "direct" + }) + + refute_receive _ + end + + test "it doesn't send unwanted private posts to list", %{user: user_a, token: user_a_token} do + user_b = insert(:user) + + {:ok, list} = List.create("Test", user_a) + {:ok, list} = List.follow(list, user_b) + + Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) + + {:ok, _activity} = + CommonAPI.post(user_b, %{ + status: "Test", + visibility: "private" + }) + + refute_receive _ + end + + test "it sends wanted private posts to list", %{user: user_a, token: user_a_token} do + user_b = insert(:user) + + {:ok, user_a} = User.follow(user_a, user_b) + + {:ok, list} = List.create("Test", user_a) + {:ok, list} = List.follow(list, user_b) + + Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) + + {:ok, activity} = + CommonAPI.post(user_b, %{ + status: "Test", + visibility: "private" + }) + + assert_receive {:render_with_user, _, _, ^activity} + refute Streamer.filtered_by_user?(user_a, activity) + end + end + + describe "muted reblogs" do + setup do: oauth_access(["read"]) + + test "it filters muted reblogs", %{user: user1, token: user1_token} do + user2 = insert(:user) + user3 = insert(:user) + CommonAPI.follow(user1, user2) + CommonAPI.hide_reblogs(user1, user2) + + {:ok, create_activity} = CommonAPI.post(user3, %{status: "I'm kawen"}) + + 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 Streamer.filtered_by_user?(user1, announce_activity) + end + + test "it filters reblog notification for reblog-muted actors", %{ + user: user1, + token: user1_token + } do + user2 = insert(:user) + CommonAPI.follow(user1, user2) + CommonAPI.hide_reblogs(user1, user2) + + {:ok, create_activity} = CommonAPI.post(user1, %{status: "I'm kawen"}) + 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 Streamer.filtered_by_user?(user1, notif) + end + + test "it send non-reblog notification for reblog-muted actors", %{ + user: user1, + token: user1_token + } do + user2 = insert(:user) + CommonAPI.follow(user1, user2) + CommonAPI.hide_reblogs(user1, user2) + + {:ok, create_activity} = CommonAPI.post(user1, %{status: "I'm kawen"}) + 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} + refute Streamer.filtered_by_user?(user1, notif) + end + end + + describe "muted threads" do + test "it filters posts from muted threads" do + user = insert(:user) + %{user: user2, token: user2_token} = oauth_access(["read"]) + Streamer.get_topic_and_add_socket("user", user2, user2_token) + + {:ok, user2, user, _activity} = CommonAPI.follow(user2, user) + {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) + {:ok, _} = CommonAPI.add_mute(user2, activity) + + assert_receive {:render_with_user, _, _, ^activity} + assert Streamer.filtered_by_user?(user2, activity) + end + end + + describe "direct streams" do + setup do: oauth_access(["read"]) + + test "it sends conversation update to the 'direct' stream", %{user: user, token: oauth_token} do + another_user = insert(:user) + + Streamer.get_topic_and_add_socket("direct", user, oauth_token) + + {:ok, _create_activity} = + CommonAPI.post(another_user, %{ + status: "hey @#{user.nickname}", + visibility: "direct" + }) + + assert_receive {:text, received_event} + + assert %{"event" => "conversation", "payload" => received_payload} = + Jason.decode!(received_event) + + assert %{"last_status" => last_status} = Jason.decode!(received_payload) + [participation] = Participation.for_user(user) + assert last_status["pleroma"]["direct_conversation_id"] == participation.id + end + + test "it doesn't send conversation update to the 'direct' stream when the last message in the conversation is deleted", + %{user: user, token: oauth_token} do + another_user = insert(:user) + + Streamer.get_topic_and_add_socket("direct", user, oauth_token) + + {:ok, create_activity} = + CommonAPI.post(another_user, %{ + status: "hi @#{user.nickname}", + visibility: "direct" + }) + + create_activity_id = create_activity.id + assert_receive {:render_with_user, _, _, ^create_activity} + assert_receive {:text, received_conversation1} + assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) + + {:ok, _} = CommonAPI.delete(create_activity_id, another_user) + + assert_receive {:text, received_event} + + assert %{"event" => "delete", "payload" => ^create_activity_id} = + Jason.decode!(received_event) + + refute_receive _ + end + + test "it sends conversation update to the 'direct' stream when a message is deleted", %{ + user: user, + token: oauth_token + } do + another_user = insert(:user) + Streamer.get_topic_and_add_socket("direct", user, oauth_token) + + {:ok, create_activity} = + CommonAPI.post(another_user, %{ + status: "hi @#{user.nickname}", + visibility: "direct" + }) + + {:ok, create_activity2} = + CommonAPI.post(another_user, %{ + status: "hi @#{user.nickname} 2", + in_reply_to_status_id: create_activity.id, + visibility: "direct" + }) + + 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} + assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) + + {:ok, _} = CommonAPI.delete(create_activity2.id, another_user) + + assert_receive {:text, received_event} + assert %{"event" => "delete", "payload" => _} = Jason.decode!(received_event) + + assert_receive {:text, received_event} + + assert %{"event" => "conversation", "payload" => received_payload} = + Jason.decode!(received_event) + + assert %{"last_status" => last_status} = Jason.decode!(received_payload) + assert last_status["id"] == to_string(create_activity.id) + end + end +end diff --git a/test/pleroma/web/twitter_api/controller_test.exs b/test/pleroma/web/twitter_api/controller_test.exs @@ -0,0 +1,138 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.ControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Builders.ActivityBuilder + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.OAuth.Token + + import Pleroma.Factory + + describe "POST /api/qvitter/statuses/notifications/read" do + test "without valid credentials", %{conn: conn} do + conn = post(conn, "/api/qvitter/statuses/notifications/read", %{"latest_id" => 1_234_567}) + assert json_response(conn, 403) == %{"error" => "Invalid credentials."} + end + + test "with credentials, without any params" do + %{conn: conn} = oauth_access(["write:notifications"]) + + conn = post(conn, "/api/qvitter/statuses/notifications/read") + + assert json_response(conn, 400) == %{ + "error" => "You need to specify latest_id", + "request" => "/api/qvitter/statuses/notifications/read" + } + end + + test "with credentials, with params" do + %{user: current_user, conn: conn} = + oauth_access(["read:notifications", "write:notifications"]) + + other_user = insert(:user) + + {:ok, _activity} = + ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: other_user}) + + response_conn = + conn + |> assign(:user, current_user) + |> get("/api/v1/notifications") + + [notification] = response = json_response(response_conn, 200) + + assert length(response) == 1 + + assert notification["pleroma"]["is_seen"] == false + + response_conn = + conn + |> assign(:user, current_user) + |> post("/api/qvitter/statuses/notifications/read", %{"latest_id" => notification["id"]}) + + [notification] = response = json_response(response_conn, 200) + + assert length(response) == 1 + + assert notification["pleroma"]["is_seen"] == true + end + end + + describe "GET /api/account/confirm_email/:id/:token" do + setup do + {:ok, user} = + insert(:user) + |> User.confirmation_changeset(need_confirmation: true) + |> Repo.update() + + assert user.confirmation_pending + + [user: user] + end + + test "it redirects to root url", %{conn: conn, user: user} do + conn = get(conn, "/api/account/confirm_email/#{user.id}/#{user.confirmation_token}") + + assert 302 == conn.status + end + + test "it confirms the user account", %{conn: conn, user: user} do + get(conn, "/api/account/confirm_email/#{user.id}/#{user.confirmation_token}") + + user = User.get_cached_by_id(user.id) + + refute user.confirmation_pending + refute user.confirmation_token + end + + test "it returns 500 if user cannot be found by id", %{conn: conn, user: user} do + conn = get(conn, "/api/account/confirm_email/0/#{user.confirmation_token}") + + assert 500 == conn.status + end + + test "it returns 500 if token is invalid", %{conn: conn, user: user} do + conn = get(conn, "/api/account/confirm_email/#{user.id}/wrong_token") + + assert 500 == conn.status + end + end + + describe "GET /api/oauth_tokens" do + setup do + token = insert(:oauth_token) |> Repo.preload(:user) + + %{token: token} + end + + test "renders list", %{token: token} do + response = + build_conn() + |> assign(:user, token.user) + |> get("/api/oauth_tokens") + + keys = + json_response(response, 200) + |> hd() + |> Map.keys() + + assert keys -- ["id", "app_name", "valid_until"] == [] + end + + test "revoke token", %{token: token} do + response = + build_conn() + |> assign(:user, token.user) + |> delete("/api/oauth_tokens/#{token.id}") + + tokens = Token.get_user_tokens(token.user) + + assert tokens == [] + assert response.status == 201 + end + end +end diff --git a/test/pleroma/web/twitter_api/password_controller_test.exs b/test/pleroma/web/twitter_api/password_controller_test.exs @@ -0,0 +1,81 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.PasswordResetToken + alias Pleroma.User + alias Pleroma.Web.OAuth.Token + import Pleroma.Factory + + describe "GET /api/pleroma/password_reset/token" do + test "it returns error when token invalid", %{conn: conn} do + response = + conn + |> get("/api/pleroma/password_reset/token") + |> html_response(:ok) + + assert response =~ "<h2>Invalid Token</h2>" + end + + test "it shows password reset form", %{conn: conn} do + user = insert(:user) + {:ok, token} = PasswordResetToken.create_token(user) + + response = + conn + |> get("/api/pleroma/password_reset/#{token.token}") + |> html_response(:ok) + + assert response =~ "<h2>Password Reset for #{user.nickname}</h2>" + end + end + + describe "POST /api/pleroma/password_reset" do + test "it returns HTTP 200", %{conn: conn} do + user = insert(:user) + {:ok, token} = PasswordResetToken.create_token(user) + {:ok, _access_token} = Token.create(insert(:oauth_app), user, %{}) + + params = %{ + "password" => "test", + password_confirmation: "test", + token: token.token + } + + response = + conn + |> assign(:user, user) + |> post("/api/pleroma/password_reset", %{data: params}) + |> html_response(:ok) + + assert response =~ "<h2>Password changed!</h2>" + + user = refresh_record(user) + assert Pbkdf2.verify_pass("test", user.password_hash) + assert Enum.empty?(Token.get_user_tokens(user)) + end + + test "it sets password_reset_pending to false", %{conn: conn} do + user = insert(:user, password_reset_pending: true) + + {:ok, token} = PasswordResetToken.create_token(user) + {:ok, _access_token} = Token.create(insert(:oauth_app), user, %{}) + + params = %{ + "password" => "test", + password_confirmation: "test", + token: token.token + } + + conn + |> assign(:user, user) + |> post("/api/pleroma/password_reset", %{data: params}) + |> html_response(:ok) + + assert User.get_by_id(user.id).password_reset_pending == false + 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 @@ -0,0 +1,350 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Config + alias Pleroma.MFA + alias Pleroma.MFA.TOTP + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import ExUnit.CaptureLog + import Pleroma.Factory + import Ecto.Query + + setup do + Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup_all do: clear_config([:instance, :federating], true) + setup do: clear_config([:instance]) + setup do: clear_config([:frontend_configurations, :pleroma_fe]) + setup do: clear_config([:user, :deny_follow_blocked]) + + describe "GET /ostatus_subscribe - remote_follow/2" do + test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} do + assert conn + |> get( + remote_follow_path(conn, :follow, %{ + acct: "https://mastodon.social/users/emelie/statuses/101849165031453009" + }) + ) + |> redirected_to() =~ "/notice/" + end + + test "show follow account page if the `acct` is a account link", %{conn: conn} do + response = + conn + |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) + |> html_response(200) + + assert response =~ "Log in to follow" + end + + test "show follow page if the `acct` is a account link", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) + |> html_response(200) + + assert response =~ "Remote follow" + end + + test "show follow page with error when user cannot fecth by `acct` link", %{conn: conn} do + user = insert(:user) + + assert capture_log(fn -> + response = + conn + |> assign(:user, user) + |> get( + remote_follow_path(conn, :follow, %{ + acct: "https://mastodon.social/users/not_found" + }) + ) + |> html_response(200) + + assert response =~ "Error fetching user" + end) =~ "Object has been deleted" + end + end + + describe "POST /ostatus_subscribe - do_follow/2 with assigned user " do + test "required `follow | write:follows` scope", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + read_token = insert(:oauth_token, user: user, scopes: ["read"]) + + assert capture_log(fn -> + response = + conn + |> assign(:user, user) + |> assign(:token, read_token) + |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Error following account" + end) =~ "Insufficient permissions: follow | write:follows." + end + + test "follows user", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + + conn = + conn + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"])) + |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + + assert redirected_to(conn) == "/users/#{user2.id}" + end + + test "returns error when user is deactivated", %{conn: conn} do + user = insert(:user, deactivated: true) + user2 = insert(:user) + + response = + conn + |> assign(:user, user) + |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns error when user is blocked", %{conn: conn} do + Pleroma.Config.put([:user, :deny_follow_blocked], true) + user = insert(:user) + user2 = insert(:user) + + {:ok, _user_block} = Pleroma.User.block(user2, user) + + response = + conn + |> assign(:user, user) + |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns error when followee not found", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => "jimm"}}) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns success result when user already in followers", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + {:ok, _, _, _} = CommonAPI.follow(user, user2) + + conn = + conn + |> assign(:user, refresh_record(user)) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"])) + |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + + assert redirected_to(conn) == "/users/#{user2.id}" + end + end + + describe "POST /ostatus_subscribe - follow/2 with enabled Two-Factor Auth " do + test "render the MFA login form", %{conn: conn} do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + user2 = insert(:user) + + response = + conn + |> post(remote_follow_path(conn, :do_follow), %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} + }) + |> response(200) + + mfa_token = Pleroma.Repo.one(from(q in Pleroma.MFA.Token, where: q.user_id == ^user.id)) + + assert response =~ "Two-factor authentication" + assert response =~ "Authentication code" + assert response =~ mfa_token.token + refute user2.follower_address in User.following(user) + end + + test "returns error when password is incorrect", %{conn: conn} do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + user2 = insert(:user) + + response = + conn + |> post(remote_follow_path(conn, :do_follow), %{ + "authorization" => %{"name" => user.nickname, "password" => "test1", "id" => user2.id} + }) + |> response(200) + + assert response =~ "Wrong username or password" + refute user2.follower_address in User.following(user) + end + + test "follows", %{conn: conn} do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + {:ok, %{token: token}} = MFA.Token.create(user) + + user2 = insert(:user) + otp_token = TOTP.generate_token(otp_secret) + + conn = + conn + |> post( + remote_follow_path(conn, :do_follow), + %{ + "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id} + } + ) + + assert redirected_to(conn) == "/users/#{user2.id}" + assert user2.follower_address in User.following(user) + end + + test "returns error when auth code is incorrect", %{conn: conn} do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + {:ok, %{token: token}} = MFA.Token.create(user) + + user2 = insert(:user) + otp_token = TOTP.generate_token(TOTP.generate_secret()) + + response = + conn + |> post( + remote_follow_path(conn, :do_follow), + %{ + "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id} + } + ) + |> response(200) + + assert response =~ "Wrong authentication code" + refute user2.follower_address in User.following(user) + end + end + + describe "POST /ostatus_subscribe - follow/2 without assigned user " do + test "follows", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + + conn = + conn + |> post(remote_follow_path(conn, :do_follow), %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} + }) + + assert redirected_to(conn) == "/users/#{user2.id}" + assert user2.follower_address in User.following(user) + end + + test "returns error when followee not found", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post(remote_follow_path(conn, :do_follow), %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => "jimm"} + }) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns error when login invalid", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post(remote_follow_path(conn, :do_follow), %{ + "authorization" => %{"name" => "jimm", "password" => "test", "id" => user.id} + }) + |> response(200) + + assert response =~ "Wrong username or password" + end + + test "returns error when password invalid", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + + response = + conn + |> post(remote_follow_path(conn, :do_follow), %{ + "authorization" => %{"name" => user.nickname, "password" => "42", "id" => user2.id} + }) + |> response(200) + + assert response =~ "Wrong username or password" + end + + test "returns error when user is blocked", %{conn: conn} do + Pleroma.Config.put([:user, :deny_follow_blocked], true) + user = insert(:user) + user2 = insert(:user) + {:ok, _user_block} = Pleroma.User.block(user2, user) + + response = + conn + |> post(remote_follow_path(conn, :do_follow), %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} + }) + |> response(200) + + assert response =~ "Error following account" + end + end +end diff --git a/test/pleroma/web/twitter_api/twitter_api_test.exs b/test/pleroma/web/twitter_api/twitter_api_test.exs @@ -0,0 +1,432 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.UserInviteToken + alias Pleroma.Web.TwitterAPI.TwitterAPI + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "it registers a new user and returns the user." do + data = %{ + :username => "lain", + :email => "lain@wired.jp", + :fullname => "lain iwakura", + :password => "bear", + :confirm => "bear" + } + + {:ok, user} = TwitterAPI.register_user(data) + + assert user == User.get_cached_by_nickname("lain") + end + + test "it registers a new user with empty string in bio and returns the user" do + data = %{ + :username => "lain", + :email => "lain@wired.jp", + :fullname => "lain iwakura", + :bio => "", + :password => "bear", + :confirm => "bear" + } + + {:ok, user} = TwitterAPI.register_user(data) + + assert user == User.get_cached_by_nickname("lain") + end + + test "it sends confirmation email if :account_activation_required is specified in instance config" do + setting = Pleroma.Config.get([:instance, :account_activation_required]) + + unless setting do + Pleroma.Config.put([:instance, :account_activation_required], true) + on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end) + end + + data = %{ + :username => "lain", + :email => "lain@wired.jp", + :fullname => "lain iwakura", + :bio => "", + :password => "bear", + :confirm => "bear" + } + + {:ok, user} = TwitterAPI.register_user(data) + ObanHelpers.perform_all() + + assert user.confirmation_pending + + email = Pleroma.Emails.UserEmail.account_confirmation_email(user) + + notify_email = Pleroma.Config.get([:instance, :notify_email]) + instance_name = Pleroma.Config.get([:instance, :name]) + + Swoosh.TestAssertions.assert_email_sent( + from: {instance_name, notify_email}, + to: {user.name, user.email}, + html_body: email.html_body + ) + end + + test "it sends an admin email if :account_approval_required is specified in instance config" do + admin = insert(:user, is_admin: true) + setting = Pleroma.Config.get([:instance, :account_approval_required]) + + unless setting do + Pleroma.Config.put([:instance, :account_approval_required], true) + on_exit(fn -> Pleroma.Config.put([:instance, :account_approval_required], setting) end) + end + + data = %{ + :username => "lain", + :email => "lain@wired.jp", + :fullname => "lain iwakura", + :bio => "", + :password => "bear", + :confirm => "bear", + :reason => "I love anime" + } + + {:ok, user} = TwitterAPI.register_user(data) + ObanHelpers.perform_all() + + assert user.approval_pending + + email = Pleroma.Emails.AdminEmail.new_unapproved_registration(admin, user) + + notify_email = Pleroma.Config.get([:instance, :notify_email]) + instance_name = Pleroma.Config.get([:instance, :name]) + + Swoosh.TestAssertions.assert_email_sent( + from: {instance_name, notify_email}, + to: {admin.name, admin.email}, + html_body: email.html_body + ) + end + + test "it registers a new user and parses mentions in the bio" do + data1 = %{ + :username => "john", + :email => "john@gmail.com", + :fullname => "John Doe", + :bio => "test", + :password => "bear", + :confirm => "bear" + } + + {:ok, user1} = TwitterAPI.register_user(data1) + + data2 = %{ + :username => "lain", + :email => "lain@wired.jp", + :fullname => "lain iwakura", + :bio => "@john test", + :password => "bear", + :confirm => "bear" + } + + {:ok, user2} = TwitterAPI.register_user(data2) + + expected_text = + ~s(<span class="h-card"><a class="u-url mention" data-user="#{user1.id}" href="#{ + user1.ap_id + }" rel="ugc">@<span>john</span></a></span> test) + + assert user2.bio == expected_text + end + + describe "register with one time token" do + setup do: clear_config([:instance, :registrations_open], false) + + test "returns user on success" do + {:ok, invite} = UserInviteToken.create_invite() + + data = %{ + :username => "vinny", + :email => "pasta@pizza.vs", + :fullname => "Vinny Vinesauce", + :bio => "streamer", + :password => "hiptofbees", + :confirm => "hiptofbees", + :token => invite.token + } + + {:ok, user} = TwitterAPI.register_user(data) + + assert user == User.get_cached_by_nickname("vinny") + + invite = Repo.get_by(UserInviteToken, token: invite.token) + assert invite.used == true + end + + test "returns error on invalid token" do + data = %{ + :username => "GrimReaper", + :email => "death@reapers.afterlife", + :fullname => "Reaper Grim", + :bio => "Your time has come", + :password => "scythe", + :confirm => "scythe", + :token => "DudeLetMeInImAFairy" + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Invalid token" + refute User.get_cached_by_nickname("GrimReaper") + end + + test "returns error on expired token" do + {:ok, invite} = UserInviteToken.create_invite() + UserInviteToken.update_invite!(invite, used: true) + + data = %{ + :username => "GrimReaper", + :email => "death@reapers.afterlife", + :fullname => "Reaper Grim", + :bio => "Your time has come", + :password => "scythe", + :confirm => "scythe", + :token => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_cached_by_nickname("GrimReaper") + end + end + + describe "registers with date limited token" do + setup do: clear_config([:instance, :registrations_open], false) + + setup do + data = %{ + :username => "vinny", + :email => "pasta@pizza.vs", + :fullname => "Vinny Vinesauce", + :bio => "streamer", + :password => "hiptofbees", + :confirm => "hiptofbees" + } + + check_fn = fn invite -> + data = Map.put(data, :token, invite.token) + {:ok, user} = TwitterAPI.register_user(data) + + assert user == User.get_cached_by_nickname("vinny") + end + + {:ok, data: data, check_fn: check_fn} + end + + test "returns user on success", %{check_fn: check_fn} do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today()}) + + check_fn.(invite) + + invite = Repo.get_by(UserInviteToken, token: invite.token) + + refute invite.used + end + + test "returns user on token which expired tomorrow", %{check_fn: check_fn} do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), 1)}) + + check_fn.(invite) + + invite = Repo.get_by(UserInviteToken, token: invite.token) + + refute invite.used + end + + test "returns an error on overdue date", %{data: data} do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1)}) + + data = Map.put(data, "token", invite.token) + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_cached_by_nickname("vinny") + invite = Repo.get_by(UserInviteToken, token: invite.token) + + refute invite.used + end + end + + describe "registers with reusable token" do + setup do: clear_config([:instance, :registrations_open], false) + + test "returns user on success, after him registration fails" do + {:ok, invite} = UserInviteToken.create_invite(%{max_use: 100}) + + UserInviteToken.update_invite!(invite, uses: 99) + + data = %{ + :username => "vinny", + :email => "pasta@pizza.vs", + :fullname => "Vinny Vinesauce", + :bio => "streamer", + :password => "hiptofbees", + :confirm => "hiptofbees", + :token => invite.token + } + + {:ok, user} = TwitterAPI.register_user(data) + assert user == User.get_cached_by_nickname("vinny") + + invite = Repo.get_by(UserInviteToken, token: invite.token) + assert invite.used == true + + data = %{ + :username => "GrimReaper", + :email => "death@reapers.afterlife", + :fullname => "Reaper Grim", + :bio => "Your time has come", + :password => "scythe", + :confirm => "scythe", + :token => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_cached_by_nickname("GrimReaper") + end + end + + describe "registers with reusable date limited token" do + setup do: clear_config([:instance, :registrations_open], false) + + test "returns user on success" do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100}) + + data = %{ + :username => "vinny", + :email => "pasta@pizza.vs", + :fullname => "Vinny Vinesauce", + :bio => "streamer", + :password => "hiptofbees", + :confirm => "hiptofbees", + :token => invite.token + } + + {:ok, user} = TwitterAPI.register_user(data) + assert user == User.get_cached_by_nickname("vinny") + + invite = Repo.get_by(UserInviteToken, token: invite.token) + refute invite.used + end + + test "error after max uses" do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100}) + + UserInviteToken.update_invite!(invite, uses: 99) + + data = %{ + :username => "vinny", + :email => "pasta@pizza.vs", + :fullname => "Vinny Vinesauce", + :bio => "streamer", + :password => "hiptofbees", + :confirm => "hiptofbees", + :token => invite.token + } + + {:ok, user} = TwitterAPI.register_user(data) + assert user == User.get_cached_by_nickname("vinny") + + invite = Repo.get_by(UserInviteToken, token: invite.token) + assert invite.used == true + + data = %{ + :username => "GrimReaper", + :email => "death@reapers.afterlife", + :fullname => "Reaper Grim", + :bio => "Your time has come", + :password => "scythe", + :confirm => "scythe", + :token => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_cached_by_nickname("GrimReaper") + end + + test "returns error on overdue date" do + {:ok, invite} = + UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100}) + + data = %{ + :username => "GrimReaper", + :email => "death@reapers.afterlife", + :fullname => "Reaper Grim", + :bio => "Your time has come", + :password => "scythe", + :confirm => "scythe", + :token => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_cached_by_nickname("GrimReaper") + end + + test "returns error on with overdue date and after max" do + {:ok, invite} = + UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100}) + + UserInviteToken.update_invite!(invite, uses: 100) + + data = %{ + :username => "GrimReaper", + :email => "death@reapers.afterlife", + :fullname => "Reaper Grim", + :bio => "Your time has come", + :password => "scythe", + :confirm => "scythe", + :token => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_cached_by_nickname("GrimReaper") + end + end + + test "it returns the error on registration problems" do + data = %{ + :username => "lain", + :email => "lain@wired.jp", + :fullname => "lain iwakura", + :bio => "close the world." + } + + {:error, error} = TwitterAPI.register_user(data) + + assert is_binary(error) + refute User.get_cached_by_nickname("lain") + end + + setup do + Supervisor.terminate_child(Pleroma.Supervisor, Cachex) + Supervisor.restart_child(Pleroma.Supervisor, Cachex) + :ok + end +end diff --git a/test/pleroma/web/twitter_api/util_controller_test.exs b/test/pleroma/web/twitter_api/util_controller_test.exs @@ -0,0 +1,437 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do + use Pleroma.Web.ConnCase + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Config + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + + import Pleroma.Factory + import Mock + + setup do + Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup do: clear_config([:instance]) + setup do: clear_config([:frontend_configurations, :pleroma_fe]) + + describe "PUT /api/pleroma/notification_settings" do + setup do: oauth_access(["write:accounts"]) + + test "it updates notification settings", %{user: user, conn: conn} do + conn + |> put("/api/pleroma/notification_settings", %{ + "block_from_strangers" => true, + "bar" => 1 + }) + |> json_response(:ok) + + user = refresh_record(user) + + assert %Pleroma.User.NotificationSetting{ + block_from_strangers: true, + hide_notification_contents: false + } == user.notification_settings + end + + test "it updates notification settings to enable hiding contents", %{user: user, conn: conn} do + conn + |> put("/api/pleroma/notification_settings", %{"hide_notification_contents" => "1"}) + |> json_response(:ok) + + user = refresh_record(user) + + assert %Pleroma.User.NotificationSetting{ + block_from_strangers: false, + hide_notification_contents: true + } == user.notification_settings + end + end + + describe "GET /api/pleroma/frontend_configurations" do + test "returns everything in :pleroma, :frontend_configurations", %{conn: conn} do + config = [ + frontend_a: %{ + x: 1, + y: 2 + }, + frontend_b: %{ + z: 3 + } + ] + + Config.put(:frontend_configurations, config) + + response = + conn + |> get("/api/pleroma/frontend_configurations") + |> json_response(:ok) + + assert response == Jason.encode!(config |> Enum.into(%{})) |> Jason.decode!() + end + end + + describe "/api/pleroma/emoji" do + test "returns json with custom emoji with tags", %{conn: conn} do + emoji = + conn + |> get("/api/pleroma/emoji") + |> json_response(200) + + assert Enum.all?(emoji, fn + {_key, + %{ + "image_url" => url, + "tags" => tags + }} -> + is_binary(url) and is_list(tags) + end) + end + end + + describe "GET /api/pleroma/healthcheck" do + setup do: clear_config([:instance, :healthcheck]) + + test "returns 503 when healthcheck disabled", %{conn: conn} do + Config.put([:instance, :healthcheck], false) + + response = + conn + |> get("/api/pleroma/healthcheck") + |> json_response(503) + + assert response == %{} + end + + test "returns 200 when healthcheck enabled and all ok", %{conn: conn} do + Config.put([:instance, :healthcheck], true) + + with_mock Pleroma.Healthcheck, + system_info: fn -> %Pleroma.Healthcheck{healthy: true} end do + response = + conn + |> get("/api/pleroma/healthcheck") + |> json_response(200) + + assert %{ + "active" => _, + "healthy" => true, + "idle" => _, + "memory_used" => _, + "pool_size" => _ + } = response + end + end + + test "returns 503 when healthcheck enabled and health is false", %{conn: conn} do + Config.put([:instance, :healthcheck], true) + + with_mock Pleroma.Healthcheck, + system_info: fn -> %Pleroma.Healthcheck{healthy: false} end do + response = + conn + |> get("/api/pleroma/healthcheck") + |> json_response(503) + + assert %{ + "active" => _, + "healthy" => false, + "idle" => _, + "memory_used" => _, + "pool_size" => _ + } = response + end + end + end + + describe "POST /api/pleroma/disable_account" do + setup do: oauth_access(["write:accounts"]) + + test "with valid permissions and password, it disables the account", %{conn: conn, user: user} do + response = + conn + |> post("/api/pleroma/disable_account", %{"password" => "test"}) + |> json_response(:ok) + + assert response == %{"status" => "success"} + ObanHelpers.perform_all() + + user = User.get_cached_by_id(user.id) + + assert user.deactivated == true + end + + test "with valid permissions and invalid password, it returns an error", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post("/api/pleroma/disable_account", %{"password" => "test1"}) + |> json_response(:ok) + + assert response == %{"error" => "Invalid password."} + user = User.get_cached_by_id(user.id) + + refute user.deactivated + end + end + + describe "POST /main/ostatus - remote_subscribe/2" do + setup do: clear_config([:instance, :federating], true) + + test "renders subscribe form", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post("/main/ostatus", %{"nickname" => user.nickname, "profile" => ""}) + |> response(:ok) + + refute response =~ "Could not find user" + assert response =~ "Remotely follow #{user.nickname}" + end + + test "renders subscribe form with error when user not found", %{conn: conn} do + response = + conn + |> post("/main/ostatus", %{"nickname" => "nickname", "profile" => ""}) + |> response(:ok) + + assert response =~ "Could not find user" + refute response =~ "Remotely follow" + end + + test "it redirect to webfinger url", %{conn: conn} do + user = insert(:user) + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + conn = + conn + |> post("/main/ostatus", %{ + "user" => %{"nickname" => user.nickname, "profile" => user2.ap_id} + }) + + assert redirected_to(conn) == + "https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}" + end + + test "it renders form with error when user not found", %{conn: conn} do + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + response = + conn + |> post("/main/ostatus", %{"user" => %{"nickname" => "jimm", "profile" => user2.ap_id}}) + |> response(:ok) + + assert response =~ "Something went wrong." + end + end + + test "it returns new captcha", %{conn: conn} do + with_mock Pleroma.Captcha, + new: fn -> "test_captcha" end do + resp = + conn + |> get("/api/pleroma/captcha") + |> response(200) + + assert resp == "\"test_captcha\"" + assert called(Pleroma.Captcha.new()) + end + end + + describe "POST /api/pleroma/change_email" do + setup do: oauth_access(["write:accounts"]) + + test "without permissions", %{conn: conn} do + conn = + conn + |> assign(:token, nil) + |> post("/api/pleroma/change_email") + + assert json_response(conn, 403) == %{"error" => "Insufficient permissions: write:accounts."} + end + + test "with proper permissions and invalid password", %{conn: conn} do + conn = + post(conn, "/api/pleroma/change_email", %{ + "password" => "hi", + "email" => "test@test.com" + }) + + assert json_response(conn, 200) == %{"error" => "Invalid password."} + end + + test "with proper permissions, valid password and invalid email", %{ + conn: conn + } do + conn = + post(conn, "/api/pleroma/change_email", %{ + "password" => "test", + "email" => "foobar" + }) + + assert json_response(conn, 200) == %{"error" => "Email has invalid format."} + end + + test "with proper permissions, valid password and no email", %{ + conn: conn + } do + conn = + post(conn, "/api/pleroma/change_email", %{ + "password" => "test" + }) + + assert json_response(conn, 200) == %{"error" => "Email can't be blank."} + end + + test "with proper permissions, valid password and blank email", %{ + conn: conn + } do + conn = + post(conn, "/api/pleroma/change_email", %{ + "password" => "test", + "email" => "" + }) + + assert json_response(conn, 200) == %{"error" => "Email can't be blank."} + end + + test "with proper permissions, valid password and non unique email", %{ + conn: conn + } do + user = insert(:user) + + conn = + post(conn, "/api/pleroma/change_email", %{ + "password" => "test", + "email" => user.email + }) + + assert json_response(conn, 200) == %{"error" => "Email has already been taken."} + end + + test "with proper permissions, valid password and valid email", %{ + conn: conn + } do + conn = + post(conn, "/api/pleroma/change_email", %{ + "password" => "test", + "email" => "cofe@foobar.com" + }) + + assert json_response(conn, 200) == %{"status" => "success"} + end + end + + describe "POST /api/pleroma/change_password" do + setup do: oauth_access(["write:accounts"]) + + test "without permissions", %{conn: conn} do + conn = + conn + |> assign(:token, nil) + |> post("/api/pleroma/change_password") + + assert json_response(conn, 403) == %{"error" => "Insufficient permissions: write:accounts."} + end + + test "with proper permissions and invalid password", %{conn: conn} do + conn = + post(conn, "/api/pleroma/change_password", %{ + "password" => "hi", + "new_password" => "newpass", + "new_password_confirmation" => "newpass" + }) + + assert json_response(conn, 200) == %{"error" => "Invalid password."} + end + + test "with proper permissions, valid password and new password and confirmation not matching", + %{ + conn: conn + } do + conn = + post(conn, "/api/pleroma/change_password", %{ + "password" => "test", + "new_password" => "newpass", + "new_password_confirmation" => "notnewpass" + }) + + assert json_response(conn, 200) == %{ + "error" => "New password does not match confirmation." + } + end + + test "with proper permissions, valid password and invalid new password", %{ + conn: conn + } do + conn = + post(conn, "/api/pleroma/change_password", %{ + "password" => "test", + "new_password" => "", + "new_password_confirmation" => "" + }) + + assert json_response(conn, 200) == %{ + "error" => "New password can't be blank." + } + end + + test "with proper permissions, valid password and matching new password and confirmation", %{ + conn: conn, + user: user + } do + conn = + post(conn, "/api/pleroma/change_password", %{ + "password" => "test", + "new_password" => "newpass", + "new_password_confirmation" => "newpass" + }) + + assert json_response(conn, 200) == %{"status" => "success"} + fetched_user = User.get_cached_by_id(user.id) + assert Pbkdf2.verify_pass("newpass", fetched_user.password_hash) == true + end + end + + describe "POST /api/pleroma/delete_account" do + setup do: oauth_access(["write:accounts"]) + + test "without permissions", %{conn: conn} do + conn = + conn + |> assign(:token, nil) + |> post("/api/pleroma/delete_account") + + assert json_response(conn, 403) == + %{"error" => "Insufficient permissions: write:accounts."} + end + + test "with proper permissions and wrong or missing password", %{conn: conn} do + for params <- [%{"password" => "hi"}, %{}] do + ret_conn = post(conn, "/api/pleroma/delete_account", params) + + assert json_response(ret_conn, 200) == %{"error" => "Invalid password."} + end + end + + test "with proper permissions and valid password", %{conn: conn, user: user} do + conn = post(conn, "/api/pleroma/delete_account", %{"password" => "test"}) + ObanHelpers.perform_all() + assert json_response(conn, 200) == %{"status" => "success"} + + user = User.get_by_id(user.id) + assert user.deactivated == true + assert user.name == nil + assert user.bio == "" + assert user.password_hash == nil + end + end +end diff --git a/test/pleroma/web/uploader_controller_test.exs b/test/pleroma/web/uploader_controller_test.exs @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.UploaderControllerTest do + use Pleroma.Web.ConnCase + alias Pleroma.Uploaders.Uploader + + describe "callback/2" do + test "it returns 400 response when process callback isn't alive", %{conn: conn} do + res = + conn + |> post(uploader_path(conn, :callback, "test-path")) + + assert res.status == 400 + assert res.resp_body == "{\"error\":\"bad request\"}" + end + + test "it returns success result", %{conn: conn} do + task = + Task.async(fn -> + receive do + {Uploader, pid, conn, _params} -> + conn = + conn + |> put_status(:ok) + |> Phoenix.Controller.json(%{upload_path: "test-path"}) + + send(pid, {Uploader, conn}) + end + end) + + :global.register_name({Uploader, "test-path"}, task.pid) + + res = + conn + |> post(uploader_path(conn, :callback, "test-path")) + |> json_response(200) + + assert res == %{"upload_path" => "test-path"} + end + end +end diff --git a/test/pleroma/web/views/error_view_test.exs b/test/pleroma/web/views/error_view_test.exs @@ -0,0 +1,36 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ErrorViewTest do + use Pleroma.Web.ConnCase, async: true + import ExUnit.CaptureLog + + # Bring render/3 and render_to_string/3 for testing custom views + import Phoenix.View + + test "renders 404.json" do + assert render(Pleroma.Web.ErrorView, "404.json", []) == %{errors: %{detail: "Page not found"}} + end + + test "render 500.json" do + assert capture_log(fn -> + assert render(Pleroma.Web.ErrorView, "500.json", []) == + %{errors: %{detail: "Internal server error", reason: "nil"}} + end) =~ "[error] Internal server error: nil" + end + + test "render any other" do + assert capture_log(fn -> + assert render(Pleroma.Web.ErrorView, "505.json", []) == + %{errors: %{detail: "Internal server error", reason: "nil"}} + end) =~ "[error] Internal server error: nil" + end + + test "render 500.json with reason" do + assert capture_log(fn -> + assert render(Pleroma.Web.ErrorView, "500.json", reason: "test reason") == + %{errors: %{detail: "Internal server error", reason: "\"test reason\""}} + end) =~ "[error] Internal server error: \"test reason\"" + 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 @@ -0,0 +1,94 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do + use Pleroma.Web.ConnCase + + import ExUnit.CaptureLog + import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup_all do: clear_config([:instance, :federating], true) + + test "GET host-meta" do + response = + build_conn() + |> get("/.well-known/host-meta") + + 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.base_url() + }/.well-known/webfinger?resource={uri}" type="application/xrd+xml" /></XRD>) + end + + test "Webfinger JRD" do + user = insert(:user) + + response = + build_conn() + |> put_req_header("accept", "application/jrd+json") + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") + + assert json_response(response, 200)["subject"] == "acct:#{user.nickname}@localhost" + end + + test "it returns 404 when user isn't found (JSON)" do + result = + build_conn() + |> put_req_header("accept", "application/jrd+json") + |> get("/.well-known/webfinger?resource=acct:jimm@localhost") + |> json_response(404) + + assert result == "Couldn't find user" + end + + test "Webfinger XML" do + user = insert(:user) + + response = + build_conn() + |> put_req_header("accept", "application/xrd+xml") + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") + + assert response(response, 200) + end + + test "it returns 404 when user isn't found (XML)" do + result = + build_conn() + |> put_req_header("accept", "application/xrd+xml") + |> get("/.well-known/webfinger?resource=acct:jimm@localhost") + |> response(404) + + assert result == "Couldn't find user" + end + + test "Sends a 404 when invalid format" do + user = insert(:user) + + assert capture_log(fn -> + assert_raise Phoenix.NotAcceptableError, fn -> + build_conn() + |> put_req_header("accept", "text/html") + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") + end + end) =~ "no supported media type in accept header" + end + + test "Sends a 400 when resource param is missing" do + response = + build_conn() + |> put_req_header("accept", "application/xrd+xml,application/jrd+json") + |> get("/.well-known/webfinger") + + assert response(response, 400) + end +end diff --git a/test/pleroma/web/web_finger_test.exs b/test/pleroma/web/web_finger_test.exs @@ -0,0 +1,116 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.WebFingerTest do + use Pleroma.DataCase + alias Pleroma.Web.WebFinger + import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "host meta" do + test "returns a link to the xml lrdd" do + host_info = WebFinger.host_meta() + + assert String.contains?(host_info, Pleroma.Web.base_url()) + end + end + + describe "incoming webfinger request" do + test "works for fqns" do + user = insert(:user) + + {:ok, result} = + WebFinger.webfinger("#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", "XML") + + assert is_binary(result) + end + + test "works for ap_ids" do + user = insert(:user) + + {:ok, result} = WebFinger.webfinger(user.ap_id, "XML") + assert is_binary(result) + end + end + + describe "fingering" do + test "returns error for nonsensical input" do + assert {:error, _} = WebFinger.finger("bliblablu") + assert {:error, _} = WebFinger.finger("pleroma.social") + end + + test "returns error when fails parse xml or json" do + user = "invalid_content@social.heldscal.la" + assert {:error, %Jason.DecodeError{}} = WebFinger.finger(user) + end + + test "returns the ActivityPub actor URI for an ActivityPub user" do + user = "framasoft@framatube.org" + + {:ok, _data} = WebFinger.finger(user) + end + + test "returns the ActivityPub actor URI 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" + end + + test "it work for AP-only user" do + user = "kpherox@mstdn.jp" + + {:ok, data} = WebFinger.finger(user) + + assert data["magic_key"] == nil + assert data["salmon"] == nil + + assert data["topic"] == nil + assert data["subject"] == "acct:kPherox@mstdn.jp" + assert data["ap_id"] == "https://mstdn.jp/users/kPherox" + 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") + + assert template == "https://social.heldscal.la/.well-known/webfinger?resource={uri}" + end + + test "it gets the xrd endpoint for hubzilla" do + {:ok, template} = WebFinger.find_lrdd_template("macgirvin.com") + + assert template == "https://macgirvin.com/xrd/?uri={uri}" + end + + 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}" + end + + test "it works with idna domains as nickname" do + nickname = "lain@" <> to_string(:idna.encode("zetsubou.みんな")) + + {:ok, _data} = WebFinger.finger(nickname) + end + + test "it works with idna domains as link" do + ap_id = "https://" <> to_string(:idna.encode("zetsubou.みんな")) <> "/users/lain" + {:ok, _data} = WebFinger.finger(ap_id) + end + end +end diff --git a/test/pleroma/workers/cron/digest_emails_worker_test.exs b/test/pleroma/workers/cron/digest_emails_worker_test.exs @@ -0,0 +1,54 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.Cron.DigestEmailsWorkerTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + setup do: clear_config([:email_notifications, :digest]) + + setup do + Pleroma.Config.put([:email_notifications, :digest], %{ + active: true, + inactivity_threshold: 7, + interval: 7 + }) + + user = insert(:user) + + date = + Timex.now() + |> Timex.shift(days: -10) + |> Timex.to_naive_datetime() + + user2 = insert(:user, last_digest_emailed_at: date) + {:ok, _} = User.switch_email_notifications(user2, "digest", true) + CommonAPI.post(user, %{status: "hey @#{user2.nickname}!"}) + + {:ok, user2: user2} + end + + test "it sends digest emails", %{user2: user2} do + Pleroma.Workers.Cron.DigestEmailsWorker.perform(%Oban.Job{}) + # Performing job(s) enqueued at previous step + ObanHelpers.perform_all() + + assert_received {:email, email} + assert email.to == [{user2.name, user2.email}] + assert email.subject == "Your digest from #{Pleroma.Config.get(:instance)[:name]}" + end + + test "it doesn't fail when a user has no email", %{user2: user2} do + {:ok, _} = user2 |> Ecto.Changeset.change(%{email: nil}) |> Pleroma.Repo.update() + + Pleroma.Workers.Cron.DigestEmailsWorker.perform(%Oban.Job{}) + # Performing job(s) enqueued at previous step + ObanHelpers.perform_all() + end +end diff --git a/test/pleroma/workers/cron/new_users_digest_worker_test.exs b/test/pleroma/workers/cron/new_users_digest_worker_test.exs @@ -0,0 +1,45 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.Cron.NewUsersDigestWorkerTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Tests.ObanHelpers + alias Pleroma.Web.CommonAPI + alias Pleroma.Workers.Cron.NewUsersDigestWorker + + test "it sends new users digest emails" do + yesterday = NaiveDateTime.utc_now() |> Timex.shift(days: -1) + admin = insert(:user, %{is_admin: true}) + user = insert(:user, %{inserted_at: yesterday}) + user2 = insert(:user, %{inserted_at: yesterday}) + CommonAPI.post(user, %{status: "cofe"}) + + NewUsersDigestWorker.perform(%Oban.Job{}) + ObanHelpers.perform_all() + + assert_received {:email, email} + assert email.to == [{admin.name, admin.email}] + assert email.subject == "#{Pleroma.Config.get([:instance, :name])} New Users" + + refute email.html_body =~ admin.nickname + assert email.html_body =~ user.nickname + assert email.html_body =~ user2.nickname + assert email.html_body =~ "cofe" + assert email.html_body =~ "#{Pleroma.Web.Endpoint.url()}/static/logo.png" + end + + test "it doesn't fail when admin has no email" do + yesterday = NaiveDateTime.utc_now() |> Timex.shift(days: -1) + insert(:user, %{is_admin: true, email: nil}) + insert(:user, %{inserted_at: yesterday}) + user = insert(:user, %{inserted_at: yesterday}) + + CommonAPI.post(user, %{status: "cofe"}) + + NewUsersDigestWorker.perform(%Oban.Job{}) + ObanHelpers.perform_all() + end +end diff --git a/test/pleroma/workers/purge_expired_activity_test.exs b/test/pleroma/workers/purge_expired_activity_test.exs @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.PurgeExpiredActivityTest do + use Pleroma.DataCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Workers.PurgeExpiredActivity + + test "enqueue job" do + activity = insert(:note_activity) + + assert {:ok, _} = + PurgeExpiredActivity.enqueue(%{ + activity_id: activity.id, + expires_at: DateTime.add(DateTime.utc_now(), 3601) + }) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity.id} + ) + + assert {:ok, _} = + perform_job(Pleroma.Workers.PurgeExpiredActivity, %{activity_id: activity.id}) + + assert %Oban.Job{} = Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) + end + + test "error if user was not found" do + activity = insert(:note_activity) + + assert {:ok, _} = + PurgeExpiredActivity.enqueue(%{ + activity_id: activity.id, + expires_at: DateTime.add(DateTime.utc_now(), 3601) + }) + + user = Pleroma.User.get_by_ap_id(activity.actor) + Pleroma.Repo.delete(user) + + assert {:error, :user_not_found} = + perform_job(Pleroma.Workers.PurgeExpiredActivity, %{activity_id: activity.id}) + end + + test "error if actiivity was not found" do + assert {:ok, _} = + PurgeExpiredActivity.enqueue(%{ + activity_id: "some_id", + expires_at: DateTime.add(DateTime.utc_now(), 3601) + }) + + assert {:error, :activity_not_found} = + perform_job(Pleroma.Workers.PurgeExpiredActivity, %{activity_id: "some_if"}) + end +end diff --git a/test/pleroma/workers/purge_expired_token_test.exs b/test/pleroma/workers/purge_expired_token_test.exs @@ -0,0 +1,51 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.PurgeExpiredTokenTest do + use Pleroma.DataCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + setup do: clear_config([:oauth2, :clean_expired_tokens], true) + + test "purges expired oauth token" do + user = insert(:user) + app = insert(:oauth_app) + + {:ok, %{id: id}} = Pleroma.Web.OAuth.Token.create(app, user) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredToken, + args: %{token_id: id, mod: Pleroma.Web.OAuth.Token} + ) + + assert {:ok, %{id: ^id}} = + perform_job(Pleroma.Workers.PurgeExpiredToken, %{ + token_id: id, + mod: Pleroma.Web.OAuth.Token + }) + + assert Repo.aggregate(Pleroma.Web.OAuth.Token, :count, :id) == 0 + end + + test "purges expired mfa token" do + authorization = insert(:oauth_authorization) + + {:ok, %{id: id}} = Pleroma.MFA.Token.create(authorization.user, authorization) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredToken, + args: %{token_id: id, mod: Pleroma.MFA.Token} + ) + + assert {:ok, %{id: ^id}} = + perform_job(Pleroma.Workers.PurgeExpiredToken, %{ + token_id: id, + mod: Pleroma.MFA.Token + }) + + assert Repo.aggregate(Pleroma.MFA.Token, :count, :id) == 0 + end +end diff --git a/test/pleroma/workers/scheduled_activity_worker_test.exs b/test/pleroma/workers/scheduled_activity_worker_test.exs @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.ScheduledActivityWorkerTest do + use Pleroma.DataCase + + alias Pleroma.ScheduledActivity + alias Pleroma.Workers.ScheduledActivityWorker + + import Pleroma.Factory + import ExUnit.CaptureLog + + setup do: clear_config([ScheduledActivity, :enabled]) + + test "creates a status from the scheduled activity" do + Pleroma.Config.put([ScheduledActivity, :enabled], true) + user = insert(:user) + + naive_datetime = + NaiveDateTime.add( + NaiveDateTime.utc_now(), + -:timer.minutes(2), + :millisecond + ) + + scheduled_activity = + insert( + :scheduled_activity, + scheduled_at: naive_datetime, + user: user, + params: %{status: "hi"} + ) + + ScheduledActivityWorker.perform(%Oban.Job{args: %{"activity_id" => scheduled_activity.id}}) + + refute Repo.get(ScheduledActivity, scheduled_activity.id) + activity = Repo.all(Pleroma.Activity) |> Enum.find(&(&1.actor == user.ap_id)) + assert Pleroma.Object.normalize(activity).data["content"] == "hi" + end + + test "adds log message if ScheduledActivity isn't find" do + Pleroma.Config.put([ScheduledActivity, :enabled], true) + + assert capture_log([level: :error], fn -> + ScheduledActivityWorker.perform(%Oban.Job{args: %{"activity_id" => 42}}) + end) =~ "Couldn't find scheduled activity" + end +end diff --git a/test/pleroma/xml_builder_test.exs b/test/pleroma/xml_builder_test.exs @@ -0,0 +1,65 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.XmlBuilderTest do + use Pleroma.DataCase + alias Pleroma.XmlBuilder + + test "Build a basic xml string from a tuple" do + data = {:feed, %{xmlns: "http://www.w3.org/2005/Atom"}, "Some content"} + + expected_xml = "<feed xmlns=\"http://www.w3.org/2005/Atom\">Some content</feed>" + + assert XmlBuilder.to_xml(data) == expected_xml + end + + test "returns a complete document" do + data = {:feed, %{xmlns: "http://www.w3.org/2005/Atom"}, "Some content"} + + expected_xml = + "<?xml version=\"1.0\" encoding=\"UTF-8\"?><feed xmlns=\"http://www.w3.org/2005/Atom\">Some content</feed>" + + assert XmlBuilder.to_doc(data) == expected_xml + end + + test "Works without attributes" do + data = { + :feed, + "Some content" + } + + expected_xml = "<feed>Some content</feed>" + + assert XmlBuilder.to_xml(data) == expected_xml + end + + test "It works with nested tuples" do + data = { + :feed, + [ + {:guy, "brush"}, + {:lament, %{configuration: "puzzle"}, "pinhead"} + ] + } + + expected_xml = + ~s[<feed><guy>brush</guy><lament configuration="puzzle">pinhead</lament></feed>] + + assert XmlBuilder.to_xml(data) == expected_xml + end + + test "Represents NaiveDateTime as iso8601" do + assert XmlBuilder.to_xml(~N[2000-01-01 13:13:33]) == "2000-01-01T13:13:33" + end + + test "Uses self-closing tags when no content is giving" do + data = { + :link, + %{rel: "self"} + } + + expected_xml = ~s[<link rel="self" />] + assert XmlBuilder.to_xml(data) == expected_xml + end +end diff --git a/test/plugs/admin_secret_authentication_plug_test.exs b/test/plugs/admin_secret_authentication_plug_test.exs @@ -1,75 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.AdminSecretAuthenticationPlugTest do - use Pleroma.Web.ConnCase - - import Mock - import Pleroma.Factory - - alias Pleroma.Plugs.AdminSecretAuthenticationPlug - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.PlugHelper - alias Pleroma.Plugs.RateLimiter - - test "does nothing if a user is assigned", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - - ret_conn = - conn - |> AdminSecretAuthenticationPlug.call(%{}) - - assert conn == ret_conn - end - - describe "when secret set it assigns an admin user" do - setup do: clear_config([:admin_token]) - - setup_with_mocks([{RateLimiter, [:passthrough], []}]) do - :ok - end - - test "with `admin_token` query parameter", %{conn: conn} do - Pleroma.Config.put(:admin_token, "password123") - - conn = - %{conn | params: %{"admin_token" => "wrong_password"}} - |> AdminSecretAuthenticationPlug.call(%{}) - - refute conn.assigns[:user] - assert called(RateLimiter.call(conn, name: :authentication)) - - conn = - %{conn | params: %{"admin_token" => "password123"}} - |> AdminSecretAuthenticationPlug.call(%{}) - - assert conn.assigns[:user].is_admin - assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) - end - - test "with `x-admin-token` HTTP header", %{conn: conn} do - Pleroma.Config.put(:admin_token, "☕️") - - conn = - conn - |> put_req_header("x-admin-token", "🥛") - |> AdminSecretAuthenticationPlug.call(%{}) - - refute conn.assigns[:user] - assert called(RateLimiter.call(conn, name: :authentication)) - - conn = - conn - |> put_req_header("x-admin-token", "☕️") - |> AdminSecretAuthenticationPlug.call(%{}) - - assert conn.assigns[:user].is_admin - assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) - end - end -end diff --git a/test/plugs/authentication_plug_test.exs b/test/plugs/authentication_plug_test.exs @@ -1,125 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.AuthenticationPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.AuthenticationPlug - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.PlugHelper - alias Pleroma.User - - import ExUnit.CaptureLog - import Pleroma.Factory - - setup %{conn: conn} do - user = %User{ - id: 1, - name: "dude", - password_hash: Pbkdf2.hash_pwd_salt("guy") - } - - conn = - conn - |> assign(:auth_user, user) - - %{user: user, conn: conn} - end - - test "it does nothing if a user is assigned", %{conn: conn} do - conn = - conn - |> assign(:user, %User{}) - - ret_conn = - conn - |> AuthenticationPlug.call(%{}) - - assert ret_conn == conn - end - - test "with a correct password in the credentials, " <> - "it assigns the auth_user and marks OAuthScopesPlug as skipped", - %{conn: conn} do - conn = - conn - |> assign(:auth_credentials, %{password: "guy"}) - |> AuthenticationPlug.call(%{}) - - assert conn.assigns.user == conn.assigns.auth_user - assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) - end - - test "with a bcrypt hash, it updates to a pkbdf2 hash", %{conn: conn} do - user = insert(:user, password_hash: Bcrypt.hash_pwd_salt("123")) - assert "$2" <> _ = user.password_hash - - conn = - conn - |> assign(:auth_user, user) - |> assign(:auth_credentials, %{password: "123"}) - |> AuthenticationPlug.call(%{}) - - assert conn.assigns.user.id == conn.assigns.auth_user.id - assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) - - user = User.get_by_id(user.id) - 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 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 = - "$pbkdf2-sha512$160000$loXqbp8GYls43F0i6lEfIw$AY.Ep.2pGe57j2hAPY635sI/6w7l9Q9u9Bp02PkPmF3OrClDtJAI8bCiivPr53OKMF7ph6iHhN68Rom5nEfC2A" - - assert AuthenticationPlug.checkpw("test-password", hash) - 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" - - assert AuthenticationPlug.checkpw("password", hash) - refute AuthenticationPlug.checkpw("password1", hash) - end - - test "it returns false when hash invalid" do - hash = - "psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1" - - assert capture_log(fn -> - refute Pleroma.Plugs.AuthenticationPlug.checkpw("password", hash) - end) =~ "[error] Password hash not recognized" - end - end -end diff --git a/test/plugs/basic_auth_decoder_plug_test.exs b/test/plugs/basic_auth_decoder_plug_test.exs @@ -1,35 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.BasicAuthDecoderPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.BasicAuthDecoderPlug - - defp basic_auth_enc(username, password) do - "Basic " <> Base.encode64("#{username}:#{password}") - end - - test "it puts the decoded credentials into the assigns", %{conn: conn} do - header = basic_auth_enc("moonman", "iloverobek") - - conn = - conn - |> put_req_header("authorization", header) - |> BasicAuthDecoderPlug.call(%{}) - - assert conn.assigns[:auth_credentials] == %{ - username: "moonman", - password: "iloverobek" - } - end - - test "without a authorization header it doesn't do anything", %{conn: conn} do - ret_conn = - conn - |> BasicAuthDecoderPlug.call(%{}) - - assert conn == ret_conn - end -end diff --git a/test/plugs/cache_control_test.exs b/test/plugs/cache_control_test.exs @@ -1,20 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.CacheControlTest do - use Pleroma.Web.ConnCase - alias Plug.Conn - - test "Verify Cache-Control header on static assets", %{conn: conn} do - conn = get(conn, "/index.html") - - assert Conn.get_resp_header(conn, "cache-control") == ["public, no-cache"] - end - - test "Verify Cache-Control header on the API", %{conn: conn} do - conn = get(conn, "/api/v1/instance") - - assert Conn.get_resp_header(conn, "cache-control") == ["max-age=0, private, must-revalidate"] - end -end diff --git a/test/plugs/cache_test.exs b/test/plugs/cache_test.exs @@ -1,186 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.CacheTest do - use ExUnit.Case, async: true - use Plug.Test - - alias Pleroma.Plugs.Cache - - @miss_resp {200, - [ - {"cache-control", "max-age=0, private, must-revalidate"}, - {"content-type", "cofe/hot; charset=utf-8"}, - {"x-cache", "MISS from Pleroma"} - ], "cofe"} - - @hit_resp {200, - [ - {"cache-control", "max-age=0, private, must-revalidate"}, - {"content-type", "cofe/hot; charset=utf-8"}, - {"x-cache", "HIT from Pleroma"} - ], "cofe"} - - @ttl 5 - - setup do - Cachex.clear(:web_resp_cache) - :ok - end - - test "caches a response" do - assert @miss_resp == - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - - assert_raise(Plug.Conn.AlreadySentError, fn -> - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - end) - - assert @hit_resp == - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: nil}) - |> sent_resp() - end - - test "ttl is set" do - assert @miss_resp == - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: @ttl}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - - assert @hit_resp == - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: @ttl}) - |> sent_resp() - - :timer.sleep(@ttl + 1) - - assert @miss_resp == - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: @ttl}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - end - - test "set ttl via conn.assigns" do - assert @miss_resp == - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> assign(:cache_ttl, @ttl) - |> send_resp(:ok, "cofe") - |> sent_resp() - - assert @hit_resp == - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: nil}) - |> sent_resp() - - :timer.sleep(@ttl + 1) - - assert @miss_resp == - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - end - - test "ignore query string when `query_params` is false" do - assert @miss_resp == - conn(:get, "/?cofe") - |> Cache.call(%{query_params: false, ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - - assert @hit_resp == - conn(:get, "/?cofefe") - |> Cache.call(%{query_params: false, ttl: nil}) - |> sent_resp() - end - - test "take query string into account when `query_params` is true" do - assert @miss_resp == - conn(:get, "/?cofe") - |> Cache.call(%{query_params: true, ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - - assert @miss_resp == - conn(:get, "/?cofefe") - |> Cache.call(%{query_params: true, ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - end - - test "take specific query params into account when `query_params` is list" do - assert @miss_resp == - conn(:get, "/?a=1&b=2&c=3&foo=bar") - |> fetch_query_params() - |> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - - assert @hit_resp == - conn(:get, "/?bar=foo&c=3&b=2&a=1") - |> fetch_query_params() - |> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil}) - |> sent_resp() - - assert @miss_resp == - conn(:get, "/?bar=foo&c=3&b=2&a=2") - |> fetch_query_params() - |> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - end - - test "ignore not GET requests" do - expected = - {200, - [ - {"cache-control", "max-age=0, private, must-revalidate"}, - {"content-type", "cofe/hot; charset=utf-8"} - ], "cofe"} - - assert expected == - conn(:post, "/") - |> Cache.call(%{query_params: true, ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - end - - test "ignore non-successful responses" do - expected = - {418, - [ - {"cache-control", "max-age=0, private, must-revalidate"}, - {"content-type", "tea/iced; charset=utf-8"} - ], "🥤"} - - assert expected == - conn(:get, "/cofe") - |> Cache.call(%{query_params: true, ttl: nil}) - |> put_resp_content_type("tea/iced") - |> send_resp(:im_a_teapot, "🥤") - |> sent_resp() - end -end diff --git a/test/plugs/ensure_authenticated_plug_test.exs b/test/plugs/ensure_authenticated_plug_test.exs @@ -1,96 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.EnsureAuthenticatedPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.EnsureAuthenticatedPlug - alias Pleroma.User - - describe "without :if_func / :unless_func options" do - test "it halts if user is NOT assigned", %{conn: conn} do - conn = EnsureAuthenticatedPlug.call(conn, %{}) - - assert conn.status == 403 - assert conn.halted == true - end - - test "it continues if a user is assigned", %{conn: conn} do - conn = assign(conn, :user, %User{}) - ret_conn = EnsureAuthenticatedPlug.call(conn, %{}) - - refute ret_conn.halted - end - end - - test "it halts if user is assigned and MFA enabled", %{conn: conn} do - conn = - conn - |> assign(:user, %User{multi_factor_authentication_settings: %{enabled: true}}) - |> assign(:auth_credentials, %{password: "xd-42"}) - |> EnsureAuthenticatedPlug.call(%{}) - - assert conn.status == 403 - assert conn.halted == true - - assert conn.resp_body == - "{\"error\":\"Two-factor authentication enabled, you must use a access token.\"}" - end - - test "it continues if user is assigned and MFA disabled", %{conn: conn} do - conn = - conn - |> assign(:user, %User{multi_factor_authentication_settings: %{enabled: false}}) - |> assign(:auth_credentials, %{password: "xd-42"}) - |> EnsureAuthenticatedPlug.call(%{}) - - refute conn.status == 403 - refute conn.halted - end - - describe "with :if_func / :unless_func options" do - setup do - %{ - true_fn: fn _conn -> true end, - false_fn: fn _conn -> false end - } - end - - test "it continues if a user is assigned", %{conn: conn, true_fn: true_fn, false_fn: false_fn} do - conn = assign(conn, :user, %User{}) - refute EnsureAuthenticatedPlug.call(conn, if_func: true_fn).halted - refute EnsureAuthenticatedPlug.call(conn, if_func: false_fn).halted - refute EnsureAuthenticatedPlug.call(conn, unless_func: true_fn).halted - refute EnsureAuthenticatedPlug.call(conn, unless_func: false_fn).halted - end - - test "it continues if a user is NOT assigned but :if_func evaluates to `false`", - %{conn: conn, false_fn: false_fn} do - ret_conn = EnsureAuthenticatedPlug.call(conn, if_func: false_fn) - refute ret_conn.halted - end - - test "it continues if a user is NOT assigned but :unless_func evaluates to `true`", - %{conn: conn, true_fn: true_fn} do - ret_conn = EnsureAuthenticatedPlug.call(conn, unless_func: true_fn) - refute ret_conn.halted - end - - test "it halts if a user is NOT assigned and :if_func evaluates to `true`", - %{conn: conn, true_fn: true_fn} do - conn = EnsureAuthenticatedPlug.call(conn, if_func: true_fn) - - assert conn.status == 403 - assert conn.halted == true - end - - test "it halts if a user is NOT assigned and :unless_func evaluates to `false`", - %{conn: conn, false_fn: false_fn} do - conn = EnsureAuthenticatedPlug.call(conn, unless_func: false_fn) - - assert conn.status == 403 - assert conn.halted == true - end - end -end diff --git a/test/plugs/ensure_public_or_authenticated_plug_test.exs b/test/plugs/ensure_public_or_authenticated_plug_test.exs @@ -1,48 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Config - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.User - - setup do: clear_config([:instance, :public]) - - test "it halts if not public and no user is assigned", %{conn: conn} do - Config.put([:instance, :public], false) - - conn = - conn - |> EnsurePublicOrAuthenticatedPlug.call(%{}) - - assert conn.status == 403 - assert conn.halted == true - end - - test "it continues if public", %{conn: conn} do - Config.put([:instance, :public], true) - - ret_conn = - conn - |> EnsurePublicOrAuthenticatedPlug.call(%{}) - - refute ret_conn.halted - end - - test "it continues if a user is assigned, even if not public", %{conn: conn} do - Config.put([:instance, :public], false) - - conn = - conn - |> assign(:user, %User{}) - - ret_conn = - conn - |> EnsurePublicOrAuthenticatedPlug.call(%{}) - - refute ret_conn.halted - end -end diff --git a/test/plugs/ensure_user_key_plug_test.exs b/test/plugs/ensure_user_key_plug_test.exs @@ -1,29 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.EnsureUserKeyPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.EnsureUserKeyPlug - - test "if the conn has a user key set, it does nothing", %{conn: conn} do - conn = - conn - |> assign(:user, 1) - - ret_conn = - conn - |> EnsureUserKeyPlug.call(%{}) - - assert conn == ret_conn - end - - test "if the conn has no key set, it sets it to nil", %{conn: conn} do - conn = - conn - |> EnsureUserKeyPlug.call(%{}) - - assert Map.has_key?(conn.assigns, :user) - end -end diff --git a/test/plugs/frontend_static_test.exs b/test/plugs/frontend_static_test.exs @@ -1,57 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.FrontendStaticPlugTest do - alias Pleroma.Plugs.FrontendStatic - use Pleroma.Web.ConnCase - - @dir "test/tmp/instance_static" - - setup do - File.mkdir_p!(@dir) - on_exit(fn -> File.rm_rf(@dir) end) - end - - setup do: clear_config([:instance, :static_dir], @dir) - - test "init will give a static plug config + the frontend type" do - opts = - [ - at: "/admin", - frontend_type: :admin - ] - |> FrontendStatic.init() - - assert opts[:at] == ["admin"] - assert opts[:frontend_type] == :admin - end - - test "overrides existing static files", %{conn: conn} do - name = "pelmora" - ref = "uguu" - - clear_config([:frontends, :primary], %{"name" => name, "ref" => ref}) - path = "#{@dir}/frontends/#{name}/#{ref}" - - File.mkdir_p!(path) - File.write!("#{path}/index.html", "from frontend plug") - - index = get(conn, "/") - assert html_response(index, 200) == "from frontend plug" - end - - test "overrides existing static files for the `pleroma/admin` path", %{conn: conn} do - name = "pelmora" - ref = "uguu" - - clear_config([:frontends, :admin], %{"name" => name, "ref" => ref}) - path = "#{@dir}/frontends/#{name}/#{ref}" - - File.mkdir_p!(path) - File.write!("#{path}/index.html", "from frontend plug") - - index = get(conn, "/pleroma/admin/") - assert html_response(index, 200) == "from frontend plug" - end -end diff --git a/test/plugs/http_security_plug_test.exs b/test/plugs/http_security_plug_test.exs @@ -1,140 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Config - alias Plug.Conn - - describe "http security enabled" do - setup do: clear_config([:http_security, :enabled], true) - - test "it sends CSP headers when enabled", %{conn: conn} do - conn = get(conn, "/api/v1/instance") - - refute Conn.get_resp_header(conn, "x-xss-protection") == [] - refute Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] - refute Conn.get_resp_header(conn, "x-frame-options") == [] - refute Conn.get_resp_header(conn, "x-content-type-options") == [] - refute Conn.get_resp_header(conn, "x-download-options") == [] - refute Conn.get_resp_header(conn, "referrer-policy") == [] - refute Conn.get_resp_header(conn, "content-security-policy") == [] - end - - test "it sends STS headers when enabled", %{conn: conn} do - clear_config([:http_security, :sts], true) - - conn = get(conn, "/api/v1/instance") - - refute Conn.get_resp_header(conn, "strict-transport-security") == [] - refute Conn.get_resp_header(conn, "expect-ct") == [] - end - - test "it does not send STS headers when disabled", %{conn: conn} do - clear_config([:http_security, :sts], false) - - conn = get(conn, "/api/v1/instance") - - assert Conn.get_resp_header(conn, "strict-transport-security") == [] - assert Conn.get_resp_header(conn, "expect-ct") == [] - end - - test "referrer-policy header reflects configured value", %{conn: conn} do - resp = get(conn, "/api/v1/instance") - - assert Conn.get_resp_header(resp, "referrer-policy") == ["same-origin"] - - clear_config([:http_security, :referrer_policy], "no-referrer") - - resp = get(conn, "/api/v1/instance") - - assert Conn.get_resp_header(resp, "referrer-policy") == ["no-referrer"] - end - - test "it sends `report-to` & `report-uri` CSP response headers", %{conn: conn} do - conn = get(conn, "/api/v1/instance") - - [csp] = Conn.get_resp_header(conn, "content-security-policy") - - assert csp =~ ~r|report-uri https://endpoint.com;report-to csp-endpoint;| - - [reply_to] = Conn.get_resp_header(conn, "reply-to") - - assert reply_to == - "{\"endpoints\":[{\"url\":\"https://endpoint.com\"}],\"group\":\"csp-endpoint\",\"max-age\":10886400}" - end - - test "default values for img-src and media-src with disabled media proxy", %{conn: conn} do - conn = get(conn, "/api/v1/instance") - - [csp] = Conn.get_resp_header(conn, "content-security-policy") - assert csp =~ "media-src 'self' https:;" - assert csp =~ "img-src 'self' data: blob: https:;" - end - end - - describe "img-src and media-src" do - setup do - clear_config([:http_security, :enabled], true) - clear_config([:media_proxy, :enabled], true) - clear_config([:media_proxy, :proxy_opts, :redirect_on_failure], false) - end - - test "media_proxy with base_url", %{conn: conn} do - url = "https://example.com" - clear_config([:media_proxy, :base_url], url) - assert_media_img_src(conn, url) - end - - test "upload with base url", %{conn: conn} do - url = "https://example2.com" - clear_config([Pleroma.Upload, :base_url], url) - assert_media_img_src(conn, url) - end - - test "with S3 public endpoint", %{conn: conn} do - url = "https://example3.com" - clear_config([Pleroma.Uploaders.S3, :public_endpoint], url) - assert_media_img_src(conn, url) - end - - test "with captcha endpoint", %{conn: conn} do - clear_config([Pleroma.Captcha.Mock, :endpoint], "https://captcha.com") - assert_media_img_src(conn, "https://captcha.com") - end - - test "with media_proxy whitelist", %{conn: conn} do - clear_config([:media_proxy, :whitelist], ["https://example6.com", "https://example7.com"]) - assert_media_img_src(conn, "https://example7.com https://example6.com") - end - - # TODO: delete after removing support bare domains for media proxy whitelist - test "with media_proxy bare domains whitelist (deprecated)", %{conn: conn} do - clear_config([:media_proxy, :whitelist], ["example4.com", "example5.com"]) - assert_media_img_src(conn, "example5.com example4.com") - end - end - - defp assert_media_img_src(conn, url) do - conn = get(conn, "/api/v1/instance") - [csp] = Conn.get_resp_header(conn, "content-security-policy") - assert csp =~ "media-src 'self' #{url};" - assert csp =~ "img-src 'self' data: blob: #{url};" - end - - test "it does not send CSP headers when disabled", %{conn: conn} do - clear_config([:http_security, :enabled], false) - - conn = get(conn, "/api/v1/instance") - - assert Conn.get_resp_header(conn, "x-xss-protection") == [] - assert Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] - assert Conn.get_resp_header(conn, "x-frame-options") == [] - assert Conn.get_resp_header(conn, "x-content-type-options") == [] - assert Conn.get_resp_header(conn, "x-download-options") == [] - assert Conn.get_resp_header(conn, "referrer-policy") == [] - assert Conn.get_resp_header(conn, "content-security-policy") == [] - end -end diff --git a/test/plugs/http_signature_plug_test.exs b/test/plugs/http_signature_plug_test.exs @@ -1,89 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do - use Pleroma.Web.ConnCase - alias Pleroma.Web.Plugs.HTTPSignaturePlug - - import Plug.Conn - import Phoenix.Controller, only: [put_format: 2] - import Mock - - test "it call HTTPSignatures to check validity if the actor sighed it" do - params = %{"actor" => "http://mastodon.example.org/users/admin"} - conn = build_conn(:get, "/doesntmattter", params) - - with_mock HTTPSignatures, validate_conn: fn _ -> true end do - conn = - conn - |> put_req_header( - "signature", - "keyId=\"http://mastodon.example.org/users/admin#main-key" - ) - |> put_format("activity+json") - |> HTTPSignaturePlug.call(%{}) - - assert conn.assigns.valid_signature == true - assert conn.halted == false - assert called(HTTPSignatures.validate_conn(:_)) - end - end - - describe "requires a signature when `authorized_fetch_mode` is enabled" do - setup do - Pleroma.Config.put([:activitypub, :authorized_fetch_mode], true) - - on_exit(fn -> - Pleroma.Config.put([:activitypub, :authorized_fetch_mode], false) - end) - - params = %{"actor" => "http://mastodon.example.org/users/admin"} - conn = build_conn(:get, "/doesntmattter", params) |> put_format("activity+json") - - [conn: conn] - end - - test "when signature header is present", %{conn: conn} do - with_mock HTTPSignatures, validate_conn: fn _ -> false end do - conn = - conn - |> put_req_header( - "signature", - "keyId=\"http://mastodon.example.org/users/admin#main-key" - ) - |> HTTPSignaturePlug.call(%{}) - - assert conn.assigns.valid_signature == false - assert conn.halted == true - assert conn.status == 401 - assert conn.state == :sent - assert conn.resp_body == "Request not signed" - assert called(HTTPSignatures.validate_conn(:_)) - end - - with_mock HTTPSignatures, validate_conn: fn _ -> true end do - conn = - conn - |> put_req_header( - "signature", - "keyId=\"http://mastodon.example.org/users/admin#main-key" - ) - |> HTTPSignaturePlug.call(%{}) - - assert conn.assigns.valid_signature == true - assert conn.halted == false - assert called(HTTPSignatures.validate_conn(:_)) - end - end - - test "halts the connection when `signature` header is not present", %{conn: conn} do - conn = HTTPSignaturePlug.call(conn, %{}) - assert conn.assigns[:valid_signature] == nil - assert conn.halted == true - assert conn.status == 401 - assert conn.state == :sent - assert conn.resp_body == "Request not signed" - end - end -end diff --git a/test/plugs/idempotency_plug_test.exs b/test/plugs/idempotency_plug_test.exs @@ -1,110 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.IdempotencyPlugTest do - use ExUnit.Case, async: true - use Plug.Test - - alias Pleroma.Plugs.IdempotencyPlug - alias Plug.Conn - - test "returns result from cache" do - key = "test1" - orig_request_id = "test1" - second_request_id = "test2" - body = "testing" - status = 200 - - :post - |> conn("/cofe") - |> put_req_header("idempotency-key", key) - |> Conn.put_resp_header("x-request-id", orig_request_id) - |> Conn.put_resp_content_type("application/json") - |> IdempotencyPlug.call([]) - |> Conn.send_resp(status, body) - - conn = - :post - |> conn("/cofe") - |> put_req_header("idempotency-key", key) - |> Conn.put_resp_header("x-request-id", second_request_id) - |> Conn.put_resp_content_type("application/json") - |> IdempotencyPlug.call([]) - - assert_raise Conn.AlreadySentError, fn -> - Conn.send_resp(conn, :im_a_teapot, "no cofe") - end - - assert conn.resp_body == body - assert conn.status == status - - assert [^second_request_id] = Conn.get_resp_header(conn, "x-request-id") - assert [^orig_request_id] = Conn.get_resp_header(conn, "x-original-request-id") - assert [^key] = Conn.get_resp_header(conn, "idempotency-key") - assert ["true"] = Conn.get_resp_header(conn, "idempotent-replayed") - assert ["application/json; charset=utf-8"] = Conn.get_resp_header(conn, "content-type") - end - - test "pass conn downstream if the cache not found" do - key = "test2" - orig_request_id = "test3" - body = "testing" - status = 200 - - conn = - :post - |> conn("/cofe") - |> put_req_header("idempotency-key", key) - |> Conn.put_resp_header("x-request-id", orig_request_id) - |> Conn.put_resp_content_type("application/json") - |> IdempotencyPlug.call([]) - |> Conn.send_resp(status, body) - - assert conn.resp_body == body - assert conn.status == status - - assert [] = Conn.get_resp_header(conn, "idempotent-replayed") - assert [^key] = Conn.get_resp_header(conn, "idempotency-key") - end - - test "passes conn downstream if idempotency is not present in headers" do - orig_request_id = "test4" - body = "testing" - status = 200 - - conn = - :post - |> conn("/cofe") - |> Conn.put_resp_header("x-request-id", orig_request_id) - |> Conn.put_resp_content_type("application/json") - |> IdempotencyPlug.call([]) - |> Conn.send_resp(status, body) - - assert [] = Conn.get_resp_header(conn, "idempotency-key") - end - - test "doesn't work with GET/DELETE" do - key = "test3" - body = "testing" - status = 200 - - conn = - :get - |> conn("/cofe") - |> put_req_header("idempotency-key", key) - |> IdempotencyPlug.call([]) - |> Conn.send_resp(status, body) - - assert [] = Conn.get_resp_header(conn, "idempotency-key") - - conn = - :delete - |> conn("/cofe") - |> put_req_header("idempotency-key", key) - |> IdempotencyPlug.call([]) - |> Conn.send_resp(status, body) - - assert [] = Conn.get_resp_header(conn, "idempotency-key") - end -end diff --git a/test/plugs/instance_static_test.exs b/test/plugs/instance_static_test.exs @@ -1,65 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.InstanceStaticPlugTest do - use Pleroma.Web.ConnCase - - @dir "test/tmp/instance_static" - - setup do - File.mkdir_p!(@dir) - on_exit(fn -> File.rm_rf(@dir) end) - end - - setup do: clear_config([:instance, :static_dir], @dir) - - test "overrides index" do - bundled_index = get(build_conn(), "/") - refute html_response(bundled_index, 200) == "hello world" - - File.write!(@dir <> "/index.html", "hello world") - - index = get(build_conn(), "/") - assert html_response(index, 200) == "hello world" - end - - test "also overrides frontend files", %{conn: conn} do - name = "pelmora" - ref = "uguu" - - clear_config([:frontends, :primary], %{"name" => name, "ref" => ref}) - - bundled_index = get(conn, "/") - refute html_response(bundled_index, 200) == "from frontend plug" - - path = "#{@dir}/frontends/#{name}/#{ref}" - File.mkdir_p!(path) - File.write!("#{path}/index.html", "from frontend plug") - - index = get(conn, "/") - assert html_response(index, 200) == "from frontend plug" - - File.write!(@dir <> "/index.html", "from instance static") - - index = get(conn, "/") - assert html_response(index, 200) == "from instance static" - end - - test "overrides any file in static/static" do - bundled_index = get(build_conn(), "/static/terms-of-service.html") - - assert html_response(bundled_index, 200) == - File.read!("priv/static/static/terms-of-service.html") - - File.mkdir!(@dir <> "/static") - File.write!(@dir <> "/static/terms-of-service.html", "plz be kind") - - index = get(build_conn(), "/static/terms-of-service.html") - assert html_response(index, 200) == "plz be kind" - - File.write!(@dir <> "/static/kaniini.html", "<h1>rabbit hugs as a service</h1>") - index = get(build_conn(), "/static/kaniini.html") - assert html_response(index, 200) == "<h1>rabbit hugs as a service</h1>" - end -end diff --git a/test/plugs/legacy_authentication_plug_test.exs b/test/plugs/legacy_authentication_plug_test.exs @@ -1,82 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - alias Pleroma.Plugs.LegacyAuthenticationPlug - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.PlugHelper - alias Pleroma.User - - setup do - user = - insert(:user, - password: "password", - password_hash: - "$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1" - ) - - %{user: user} - end - - test "it does nothing if a user is assigned", %{conn: conn, user: user} do - conn = - conn - |> assign(:auth_credentials, %{username: "dude", password: "password"}) - |> assign(:auth_user, user) - |> assign(:user, %User{}) - - ret_conn = - conn - |> LegacyAuthenticationPlug.call(%{}) - - assert ret_conn == conn - end - - @tag :skip_on_mac - test "if `auth_user` is present and password is correct, " <> - "it authenticates the user, resets the password, marks OAuthScopesPlug as skipped", - %{ - conn: conn, - user: user - } do - conn = - conn - |> assign(:auth_credentials, %{username: "dude", password: "password"}) - |> assign(:auth_user, user) - - conn = LegacyAuthenticationPlug.call(conn, %{}) - - assert conn.assigns.user.id == user.id - assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) - end - - @tag :skip_on_mac - test "it does nothing if the password is wrong", %{ - conn: conn, - user: user - } do - conn = - conn - |> assign(:auth_credentials, %{username: "dude", password: "wrong_password"}) - |> assign(:auth_user, user) - - ret_conn = - conn - |> LegacyAuthenticationPlug.call(%{}) - - assert conn == ret_conn - end - - test "with no credentials or user it does nothing", %{conn: conn} do - ret_conn = - conn - |> LegacyAuthenticationPlug.call(%{}) - - assert ret_conn == conn - end -end diff --git a/test/plugs/mapped_identity_to_signature_plug_test.exs b/test/plugs/mapped_identity_to_signature_plug_test.exs @@ -1,59 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do - use Pleroma.Web.ConnCase - alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug - - import Tesla.Mock - import Plug.Conn - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - defp set_signature(conn, key_id) do - conn - |> put_req_header("signature", "keyId=\"#{key_id}\"") - |> assign(:valid_signature, true) - end - - test "it successfully maps a valid identity with a valid signature" do - conn = - build_conn(:get, "/doesntmattter") - |> set_signature("http://mastodon.example.org/users/admin") - |> MappedSignatureToIdentityPlug.call(%{}) - - refute is_nil(conn.assigns.user) - end - - test "it successfully maps a valid identity with a valid signature with payload" do - conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("http://mastodon.example.org/users/admin") - |> MappedSignatureToIdentityPlug.call(%{}) - - refute is_nil(conn.assigns.user) - end - - test "it considers a mapped identity to be invalid when it mismatches a payload" do - conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("https://niu.moe/users/rye") - |> MappedSignatureToIdentityPlug.call(%{}) - - assert %{valid_signature: false} == conn.assigns - end - - @tag skip: "known breakage; the testsuite presently depends on it" - test "it considers a mapped identity to be invalid when the identity cannot be found" do - conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("http://niu.moe/users/rye") - |> MappedSignatureToIdentityPlug.call(%{}) - - assert %{valid_signature: false} == conn.assigns - end -end diff --git a/test/plugs/oauth_plug_test.exs b/test/plugs/oauth_plug_test.exs @@ -1,80 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.OAuthPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.OAuthPlug - import Pleroma.Factory - - @session_opts [ - store: :cookie, - key: "_test", - signing_salt: "cooldude" - ] - - setup %{conn: conn} do - user = insert(:user) - {:ok, %{token: token}} = Pleroma.Web.OAuth.Token.create(insert(:oauth_app), user) - %{user: user, token: token, conn: conn} - end - - test "with valid token(uppercase), it assigns the user", %{conn: conn} = opts do - conn = - conn - |> put_req_header("authorization", "BEARER #{opts[:token]}") - |> OAuthPlug.call(%{}) - - assert conn.assigns[:user] == opts[:user] - end - - test "with valid token(downcase), it assigns the user", %{conn: conn} = opts do - conn = - conn - |> put_req_header("authorization", "bearer #{opts[:token]}") - |> OAuthPlug.call(%{}) - - assert conn.assigns[:user] == opts[:user] - end - - test "with valid token(downcase) in url parameters, it assigns the user", opts do - conn = - :get - |> build_conn("/?access_token=#{opts[:token]}") - |> put_req_header("content-type", "application/json") - |> fetch_query_params() - |> OAuthPlug.call(%{}) - - assert conn.assigns[:user] == opts[:user] - end - - test "with valid token(downcase) in body parameters, it assigns the user", opts do - conn = - :post - |> build_conn("/api/v1/statuses", access_token: opts[:token], status: "test") - |> OAuthPlug.call(%{}) - - assert conn.assigns[:user] == opts[:user] - end - - test "with invalid token, it not assigns the user", %{conn: conn} do - conn = - conn - |> put_req_header("authorization", "bearer TTTTT") - |> OAuthPlug.call(%{}) - - refute conn.assigns[:user] - end - - test "when token is missed but token in session, it assigns the user", %{conn: conn} = opts do - conn = - conn - |> Plug.Session.call(Plug.Session.init(@session_opts)) - |> fetch_session() - |> put_session(:oauth_token, opts[:token]) - |> OAuthPlug.call(%{}) - - assert conn.assigns[:user] == opts[:user] - end -end diff --git a/test/plugs/oauth_scopes_plug_test.exs b/test/plugs/oauth_scopes_plug_test.exs @@ -1,210 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.OAuthScopesPlugTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Repo - - import Mock - import Pleroma.Factory - - test "is not performed if marked as skipped", %{conn: conn} do - with_mock OAuthScopesPlug, [:passthrough], perform: &passthrough([&1, &2]) do - conn = - conn - |> OAuthScopesPlug.skip_plug() - |> OAuthScopesPlug.call(%{scopes: ["random_scope"]}) - - refute called(OAuthScopesPlug.perform(:_, :_)) - refute conn.halted - end - end - - test "if `token.scopes` fulfills specified 'any of' conditions, " <> - "proceeds with no op", - %{conn: conn} do - token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user) - - conn = - conn - |> assign(:user, token.user) - |> assign(:token, token) - |> OAuthScopesPlug.call(%{scopes: ["read"]}) - - refute conn.halted - assert conn.assigns[:user] - end - - test "if `token.scopes` fulfills specified 'all of' conditions, " <> - "proceeds with no op", - %{conn: conn} do - token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user) - - conn = - conn - |> assign(:user, token.user) - |> assign(:token, token) - |> OAuthScopesPlug.call(%{scopes: ["scope2", "scope3"], op: :&}) - - refute conn.halted - assert conn.assigns[:user] - end - - describe "with `fallback: :proceed_unauthenticated` option, " do - test "if `token.scopes` doesn't fulfill specified conditions, " <> - "clears :user and :token assigns", - %{conn: conn} do - user = insert(:user) - token1 = insert(:oauth_token, scopes: ["read", "write"], user: user) - - for token <- [token1, nil], op <- [:|, :&] do - ret_conn = - conn - |> assign(:user, user) - |> assign(:token, token) - |> OAuthScopesPlug.call(%{ - scopes: ["follow"], - op: op, - fallback: :proceed_unauthenticated - }) - - refute ret_conn.halted - refute ret_conn.assigns[:user] - refute ret_conn.assigns[:token] - end - end - end - - describe "without :fallback option, " do - test "if `token.scopes` does not fulfill specified 'any of' conditions, " <> - "returns 403 and halts", - %{conn: conn} do - for token <- [insert(:oauth_token, scopes: ["read", "write"]), nil] do - any_of_scopes = ["follow", "push"] - - ret_conn = - conn - |> assign(:token, token) - |> OAuthScopesPlug.call(%{scopes: any_of_scopes}) - - assert ret_conn.halted - assert 403 == ret_conn.status - - expected_error = "Insufficient permissions: #{Enum.join(any_of_scopes, " | ")}." - assert Jason.encode!(%{error: expected_error}) == ret_conn.resp_body - end - end - - test "if `token.scopes` does not fulfill specified 'all of' conditions, " <> - "returns 403 and halts", - %{conn: conn} do - for token <- [insert(:oauth_token, scopes: ["read", "write"]), nil] do - token_scopes = (token && token.scopes) || [] - all_of_scopes = ["write", "follow"] - - conn = - conn - |> assign(:token, token) - |> OAuthScopesPlug.call(%{scopes: all_of_scopes, op: :&}) - - assert conn.halted - assert 403 == conn.status - - expected_error = - "Insufficient permissions: #{Enum.join(all_of_scopes -- token_scopes, " & ")}." - - assert Jason.encode!(%{error: expected_error}) == conn.resp_body - end - end - end - - describe "with hierarchical scopes, " do - test "if `token.scopes` fulfills specified 'any of' conditions, " <> - "proceeds with no op", - %{conn: conn} do - token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user) - - conn = - conn - |> assign(:user, token.user) - |> assign(:token, token) - |> OAuthScopesPlug.call(%{scopes: ["read:something"]}) - - refute conn.halted - assert conn.assigns[:user] - end - - test "if `token.scopes` fulfills specified 'all of' conditions, " <> - "proceeds with no op", - %{conn: conn} do - token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user) - - conn = - conn - |> assign(:user, token.user) - |> assign(:token, token) - |> OAuthScopesPlug.call(%{scopes: ["scope1:subscope", "scope2:subscope"], op: :&}) - - refute conn.halted - assert conn.assigns[:user] - end - end - - describe "filter_descendants/2" do - test "filters scopes which directly match or are ancestors of supported scopes" do - f = fn scopes, supported_scopes -> - OAuthScopesPlug.filter_descendants(scopes, supported_scopes) - end - - assert f.(["read", "follow"], ["write", "read"]) == ["read"] - - assert f.(["read", "write:something", "follow"], ["write", "read"]) == - ["read", "write:something"] - - assert f.(["admin:read"], ["write", "read"]) == [] - - assert f.(["admin:read"], ["write", "admin"]) == ["admin:read"] - end - end - - describe "transform_scopes/2" do - setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage]) - - setup do - {:ok, %{f: &OAuthScopesPlug.transform_scopes/2}} - end - - test "with :admin option, prefixes all requested scopes with `admin:` " <> - "and [optionally] keeps only prefixed scopes, " <> - "depending on `[:auth, :enforce_oauth_admin_scope_usage]` setting", - %{f: f} do - Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false) - - assert f.(["read"], %{admin: true}) == ["admin:read", "read"] - - assert f.(["read", "write"], %{admin: true}) == [ - "admin:read", - "read", - "admin:write", - "write" - ] - - Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true) - - assert f.(["read:accounts"], %{admin: true}) == ["admin:read:accounts"] - - assert f.(["read", "write:reports"], %{admin: true}) == [ - "admin:read", - "admin:write:reports" - ] - end - - test "with no supported options, returns unmodified scopes", %{f: f} do - assert f.(["read"], %{}) == ["read"] - assert f.(["read", "write"], %{}) == ["read", "write"] - end - end -end diff --git a/test/plugs/rate_limiter_test.exs b/test/plugs/rate_limiter_test.exs @@ -1,263 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.RateLimiterTest do - use Pleroma.Web.ConnCase - - alias Phoenix.ConnTest - alias Pleroma.Config - alias Pleroma.Plugs.RateLimiter - alias Plug.Conn - - import Pleroma.Factory - import Pleroma.Tests.Helpers, only: [clear_config: 1, clear_config: 2] - - # Note: each example must work with separate buckets in order to prevent concurrency issues - setup do: clear_config([Pleroma.Web.Endpoint, :http, :ip]) - setup do: clear_config(:rate_limit) - - describe "config" do - @limiter_name :test_init - setup do: clear_config([Pleroma.Plugs.RemoteIp, :enabled]) - - test "config is required for plug to work" do - Config.put([:rate_limit, @limiter_name], {1, 1}) - Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - - assert %{limits: {1, 1}, name: :test_init, opts: [name: :test_init]} == - [name: @limiter_name] - |> RateLimiter.init() - |> RateLimiter.action_settings() - - assert nil == - [name: :nonexisting_limiter] - |> RateLimiter.init() - |> RateLimiter.action_settings() - end - end - - test "it is disabled if it remote ip plug is enabled but no remote ip is found" do - assert RateLimiter.disabled?(Conn.assign(build_conn(), :remote_ip_found, false)) - end - - test "it is enabled if remote ip found" do - refute RateLimiter.disabled?(Conn.assign(build_conn(), :remote_ip_found, true)) - end - - test "it is enabled if remote_ip_found flag doesn't exist" do - refute RateLimiter.disabled?(build_conn()) - end - - test "it restricts based on config values" do - limiter_name = :test_plug_opts - scale = 80 - limit = 5 - - Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - Config.put([: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) - end - - conn = RateLimiter.call(conn, plug_opts) - assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests) - assert conn.halted - - Process.sleep(50) - - conn = build_conn(:get, "/") - - conn = RateLimiter.call(conn, plug_opts) - assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) - - refute conn.status == Conn.Status.code(:too_many_requests) - refute conn.resp_body - refute conn.halted - end - - describe "options" do - test "`bucket_name` option overrides default bucket name" do - limiter_name = :test_bucket_name - - Config.put([:rate_limit, limiter_name], {1000, 5}) - Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - - base_bucket_name = "#{limiter_name}:group1" - plug_opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name) - - conn = build_conn(:get, "/") - - RateLimiter.call(conn, plug_opts) - assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, plug_opts) - assert {:error, :not_found} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) - end - - test "`params` option allows different queries to be tracked independently" do - limiter_name = :test_params - Config.put([:rate_limit, limiter_name], {1000, 5}) - Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - - plug_opts = RateLimiter.init(name: limiter_name, params: ["id"]) - - conn = build_conn(:get, "/?id=1") - conn = Conn.fetch_query_params(conn) - conn_2 = build_conn(:get, "/?id=2") - - RateLimiter.call(conn, plug_opts) - assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) - assert {0, 5} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts) - end - - test "it supports combination of options modifying bucket name" do - limiter_name = :test_options_combo - Config.put([:rate_limit, limiter_name], {1000, 5}) - Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - - base_bucket_name = "#{limiter_name}:group1" - - plug_opts = - RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name, params: ["id"]) - - id = "100" - - conn = build_conn(:get, "/?id=#{id}") - conn = Conn.fetch_query_params(conn) - conn_2 = build_conn(:get, "/?id=#{101}") - - RateLimiter.call(conn, plug_opts) - assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, plug_opts) - assert {0, 5} = RateLimiter.inspect_bucket(conn_2, base_bucket_name, plug_opts) - end - end - - describe "unauthenticated users" do - test "are restricted based on remote IP" do - limiter_name = :test_unauthenticated - Config.put([:rate_limit, limiter_name], [{1000, 5}, {1, 10}]) - Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - - plug_opts = RateLimiter.init(name: limiter_name) - - conn = %{build_conn(:get, "/") | remote_ip: {127, 0, 0, 2}} - conn_2 = %{build_conn(:get, "/") | remote_ip: {127, 0, 0, 3}} - - for i <- 1..5 do - conn = RateLimiter.call(conn, plug_opts) - assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) - refute conn.halted - end - - conn = RateLimiter.call(conn, plug_opts) - - assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests) - assert conn.halted - - conn_2 = RateLimiter.call(conn_2, plug_opts) - assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts) - - refute conn_2.status == Conn.Status.code(:too_many_requests) - refute conn_2.resp_body - refute conn_2.halted - end - end - - describe "authenticated users" do - setup do - Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo) - - :ok - end - - test "can have limits separate from unauthenticated connections" do - limiter_name = :test_authenticated1 - - scale = 50 - limit = 5 - Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - Config.put([:rate_limit, limiter_name], [{1000, 1}, {scale, limit}]) - - plug_opts = RateLimiter.init(name: limiter_name) - - user = insert(:user) - conn = build_conn(:get, "/") |> assign(:user, user) - - for i <- 1..5 do - conn = RateLimiter.call(conn, plug_opts) - assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) - refute conn.halted - end - - conn = RateLimiter.call(conn, plug_opts) - - assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests) - assert conn.halted - end - - test "different users are counted independently" do - limiter_name = :test_authenticated2 - Config.put([:rate_limit, limiter_name], [{1, 10}, {1000, 5}]) - Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - - plug_opts = RateLimiter.init(name: limiter_name) - - user = insert(:user) - conn = build_conn(:get, "/") |> assign(:user, user) - - user_2 = insert(:user) - conn_2 = build_conn(:get, "/") |> assign(:user, user_2) - - for i <- 1..5 do - conn = RateLimiter.call(conn, plug_opts) - assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) - end - - conn = RateLimiter.call(conn, plug_opts) - assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests) - assert conn.halted - - conn_2 = RateLimiter.call(conn_2, plug_opts) - assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts) - refute conn_2.status == Conn.Status.code(:too_many_requests) - refute conn_2.resp_body - refute conn_2.halted - end - end - - test "doesn't crash due to a race condition when multiple requests are made at the same time and the bucket is not yet initialized" do - limiter_name = :test_race_condition - Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5}) - Pleroma.Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - - opts = RateLimiter.init(name: limiter_name) - - conn = build_conn(:get, "/") - conn_2 = build_conn(:get, "/") - - %Task{pid: pid1} = - task1 = - Task.async(fn -> - receive do - :process2_up -> - RateLimiter.call(conn, opts) - end - end) - - task2 = - Task.async(fn -> - send(pid1, :process2_up) - RateLimiter.call(conn_2, opts) - end) - - Task.await(task1) - Task.await(task2) - - refute {:err, :not_found} == RateLimiter.inspect_bucket(conn, limiter_name, opts) - end -end diff --git a/test/plugs/remote_ip_test.exs b/test/plugs/remote_ip_test.exs @@ -1,108 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.RemoteIpTest do - use ExUnit.Case - use Plug.Test - - alias Pleroma.Plugs.RemoteIp - - import Pleroma.Tests.Helpers, only: [clear_config: 2] - - setup do: - clear_config(RemoteIp, - enabled: true, - headers: ["x-forwarded-for"], - proxies: [], - reserved: [ - "127.0.0.0/8", - "::1/128", - "fc00::/7", - "10.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16" - ] - ) - - test "disabled" do - Pleroma.Config.put(RemoteIp, enabled: false) - - %{remote_ip: remote_ip} = conn(:get, "/") - - conn = - conn(:get, "/") - |> put_req_header("x-forwarded-for", "1.1.1.1") - |> RemoteIp.call(nil) - - assert conn.remote_ip == remote_ip - end - - test "enabled" do - conn = - conn(:get, "/") - |> put_req_header("x-forwarded-for", "1.1.1.1") - |> RemoteIp.call(nil) - - assert conn.remote_ip == {1, 1, 1, 1} - end - - test "custom headers" do - Pleroma.Config.put(RemoteIp, enabled: true, headers: ["cf-connecting-ip"]) - - conn = - conn(:get, "/") - |> put_req_header("x-forwarded-for", "1.1.1.1") - |> RemoteIp.call(nil) - - refute conn.remote_ip == {1, 1, 1, 1} - - conn = - conn(:get, "/") - |> put_req_header("cf-connecting-ip", "1.1.1.1") - |> RemoteIp.call(nil) - - assert conn.remote_ip == {1, 1, 1, 1} - end - - test "custom proxies" do - conn = - conn(:get, "/") - |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2") - |> RemoteIp.call(nil) - - refute conn.remote_ip == {1, 1, 1, 1} - - Pleroma.Config.put([RemoteIp, :proxies], ["173.245.48.0/20"]) - - conn = - conn(:get, "/") - |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2") - |> RemoteIp.call(nil) - - assert conn.remote_ip == {1, 1, 1, 1} - end - - test "proxies set without CIDR format" do - Pleroma.Config.put([RemoteIp, :proxies], ["173.245.48.1"]) - - conn = - conn(:get, "/") - |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1") - |> RemoteIp.call(nil) - - assert conn.remote_ip == {1, 1, 1, 1} - end - - test "proxies set `nonsensical` CIDR" do - Pleroma.Config.put([RemoteIp, :reserved], ["127.0.0.0/8"]) - Pleroma.Config.put([RemoteIp, :proxies], ["10.0.0.3/24"]) - - conn = - conn(:get, "/") - |> put_req_header("x-forwarded-for", "10.0.0.3, 1.1.1.1") - |> RemoteIp.call(nil) - - assert conn.remote_ip == {1, 1, 1, 1} - end -end diff --git a/test/plugs/session_authentication_plug_test.exs b/test/plugs/session_authentication_plug_test.exs @@ -1,63 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.SessionAuthenticationPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.SessionAuthenticationPlug - alias Pleroma.User - - setup %{conn: conn} do - session_opts = [ - store: :cookie, - key: "_test", - signing_salt: "cooldude" - ] - - conn = - conn - |> Plug.Session.call(Plug.Session.init(session_opts)) - |> fetch_session - |> assign(:auth_user, %User{id: 1}) - - %{conn: conn} - end - - test "it does nothing if a user is assigned", %{conn: conn} do - conn = - conn - |> assign(:user, %User{}) - - ret_conn = - conn - |> SessionAuthenticationPlug.call(%{}) - - assert ret_conn == conn - end - - test "if the auth_user has the same id as the user_id in the session, it assigns the user", %{ - conn: conn - } do - conn = - conn - |> put_session(:user_id, conn.assigns.auth_user.id) - |> SessionAuthenticationPlug.call(%{}) - - assert conn.assigns.user == conn.assigns.auth_user - end - - test "if the auth_user has a different id as the user_id in the session, it does nothing", %{ - conn: conn - } do - conn = - conn - |> put_session(:user_id, -1) - - ret_conn = - conn - |> SessionAuthenticationPlug.call(%{}) - - assert ret_conn == conn - end -end diff --git a/test/plugs/set_format_plug_test.exs b/test/plugs/set_format_plug_test.exs @@ -1,38 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.SetFormatPlugTest do - use ExUnit.Case, async: true - use Plug.Test - - alias Pleroma.Plugs.SetFormatPlug - - test "set format from params" do - conn = - :get - |> conn("/cofe?_format=json") - |> SetFormatPlug.call([]) - - assert %{format: "json"} == conn.assigns - end - - test "set format from header" do - conn = - :get - |> conn("/cofe") - |> put_private(:phoenix_format, "xml") - |> SetFormatPlug.call([]) - - assert %{format: "xml"} == conn.assigns - end - - test "doesn't set format" do - conn = - :get - |> conn("/cofe") - |> SetFormatPlug.call([]) - - refute conn.assigns[:format] - end -end diff --git a/test/plugs/set_locale_plug_test.exs b/test/plugs/set_locale_plug_test.exs @@ -1,46 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.SetLocalePlugTest do - use ExUnit.Case, async: true - use Plug.Test - - alias Pleroma.Plugs.SetLocalePlug - alias Plug.Conn - - test "default locale is `en`" do - conn = - :get - |> conn("/cofe") - |> SetLocalePlug.call([]) - - assert "en" == Gettext.get_locale() - assert %{locale: "en"} == conn.assigns - end - - test "use supported locale from `accept-language`" do - conn = - :get - |> conn("/cofe") - |> Conn.put_req_header( - "accept-language", - "ru, fr-CH, fr;q=0.9, en;q=0.8, *;q=0.5" - ) - |> SetLocalePlug.call([]) - - assert "ru" == Gettext.get_locale() - assert %{locale: "ru"} == conn.assigns - end - - test "use default locale if locale from `accept-language` is not supported" do - conn = - :get - |> conn("/cofe") - |> Conn.put_req_header("accept-language", "tlh") - |> SetLocalePlug.call([]) - - assert "en" == Gettext.get_locale() - assert %{locale: "en"} == conn.assigns - end -end diff --git a/test/plugs/set_user_session_id_plug_test.exs b/test/plugs/set_user_session_id_plug_test.exs @@ -1,45 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.SetUserSessionIdPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.SetUserSessionIdPlug - alias Pleroma.User - - setup %{conn: conn} do - session_opts = [ - store: :cookie, - key: "_test", - signing_salt: "cooldude" - ] - - conn = - conn - |> Plug.Session.call(Plug.Session.init(session_opts)) - |> fetch_session - - %{conn: conn} - end - - test "doesn't do anything if the user isn't set", %{conn: conn} do - ret_conn = - conn - |> SetUserSessionIdPlug.call(%{}) - - assert ret_conn == conn - end - - test "sets the user_id in the session to the user id of the user assign", %{conn: conn} do - Code.ensure_compiled(Pleroma.User) - - conn = - conn - |> assign(:user, %User{id: 1}) - |> SetUserSessionIdPlug.call(%{}) - - id = get_session(conn, :user_id) - assert id == 1 - end -end diff --git a/test/plugs/uploaded_media_plug_test.exs b/test/plugs/uploaded_media_plug_test.exs @@ -1,43 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.UploadedMediaPlugTest do - use Pleroma.Web.ConnCase - alias Pleroma.Upload - - defp upload_file(context) do - Pleroma.DataCase.ensure_local_uploader(context) - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "nice_tf.jpg" - } - - {:ok, data} = Upload.store(file) - [%{"href" => attachment_url} | _] = data["url"] - [attachment_url: attachment_url] - end - - setup_all :upload_file - - test "does not send Content-Disposition header when name param is not set", %{ - attachment_url: attachment_url - } do - conn = get(build_conn(), attachment_url) - refute Enum.any?(conn.resp_headers, &(elem(&1, 0) == "content-disposition")) - end - - test "sends Content-Disposition header when name param is set", %{ - attachment_url: attachment_url - } do - conn = get(build_conn(), attachment_url <> "?name=\"cofe\".gif") - - assert Enum.any?( - conn.resp_headers, - &(&1 == {"content-disposition", "filename=\"\\\"cofe\\\".gif\""}) - ) - end -end diff --git a/test/plugs/user_enabled_plug_test.exs b/test/plugs/user_enabled_plug_test.exs @@ -1,59 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.UserEnabledPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.UserEnabledPlug - import Pleroma.Factory - - setup do: clear_config([:instance, :account_activation_required]) - - test "doesn't do anything if the user isn't set", %{conn: conn} do - ret_conn = - conn - |> UserEnabledPlug.call(%{}) - - assert ret_conn == conn - end - - test "with a user that's not confirmed and a config requiring confirmation, it removes that user", - %{conn: conn} do - Pleroma.Config.put([:instance, :account_activation_required], true) - - user = insert(:user, confirmation_pending: true) - - conn = - conn - |> assign(:user, user) - |> UserEnabledPlug.call(%{}) - - assert conn.assigns.user == nil - end - - test "with a user that is deactivated, it removes that user", %{conn: conn} do - user = insert(:user, deactivated: true) - - conn = - conn - |> assign(:user, user) - |> UserEnabledPlug.call(%{}) - - assert conn.assigns.user == nil - end - - test "with a user that is not deactivated, it does nothing", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - - ret_conn = - conn - |> UserEnabledPlug.call(%{}) - - assert conn == ret_conn - end -end diff --git a/test/plugs/user_fetcher_plug_test.exs b/test/plugs/user_fetcher_plug_test.exs @@ -1,41 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.UserFetcherPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.UserFetcherPlug - import Pleroma.Factory - - setup do - user = insert(:user) - %{user: user} - end - - test "if an auth_credentials assign is present, it tries to fetch the user and assigns it", %{ - conn: conn, - user: user - } do - conn = - conn - |> assign(:auth_credentials, %{ - username: user.nickname, - password: nil - }) - - conn = - conn - |> UserFetcherPlug.call(%{}) - - assert conn.assigns[:auth_user] == user - end - - test "without a credential assign it doesn't do anything", %{conn: conn} do - ret_conn = - conn - |> UserFetcherPlug.call(%{}) - - assert conn == ret_conn - end -end diff --git a/test/plugs/user_is_admin_plug_test.exs b/test/plugs/user_is_admin_plug_test.exs @@ -1,37 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.UserIsAdminPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.UserIsAdminPlug - 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 = UserIsAdminPlug.call(conn, %{}) - - assert conn == ret_conn - end - - test "denies a user that isn't an admin" do - user = insert(:user) - - conn = - build_conn() - |> assign(:user, user) - |> UserIsAdminPlug.call(%{}) - - assert conn.status == 403 - end - - test "denies when a user isn't set" do - conn = UserIsAdminPlug.call(build_conn(), %{}) - - assert conn.status == 403 - end -end diff --git a/test/registration_test.exs b/test/registration_test.exs @@ -1,59 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.RegistrationTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Registration - alias Pleroma.Repo - - describe "generic changeset" do - test "requires :provider, :uid" do - registration = build(:registration, provider: nil, uid: nil) - - cs = Registration.changeset(registration, %{}) - refute cs.valid? - - assert [ - provider: {"can't be blank", [validation: :required]}, - uid: {"can't be blank", [validation: :required]} - ] == cs.errors - end - - test "ensures uniqueness of [:provider, :uid]" do - registration = insert(:registration) - registration2 = build(:registration, provider: registration.provider, uid: registration.uid) - - cs = Registration.changeset(registration2, %{}) - assert cs.valid? - - assert {:error, - %Ecto.Changeset{ - errors: [ - uid: - {"has already been taken", - [constraint: :unique, constraint_name: "registrations_provider_uid_index"]} - ] - }} = Repo.insert(cs) - - # Note: multiple :uid values per [:user_id, :provider] are intentionally allowed - cs2 = Registration.changeset(registration2, %{uid: "available.uid"}) - assert cs2.valid? - assert {:ok, _} = Repo.insert(cs2) - - cs3 = Registration.changeset(registration2, %{provider: "provider2"}) - assert cs3.valid? - assert {:ok, _} = Repo.insert(cs3) - end - - test "allows `nil` :user_id (user-unbound registration)" do - registration = build(:registration, user_id: nil) - cs = Registration.changeset(registration, %{}) - assert cs.valid? - assert {:ok, _} = Repo.insert(cs) - end - end -end diff --git a/test/repo_test.exs b/test/repo_test.exs @@ -1,80 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.RepoTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.User - - describe "find_resource/1" do - test "returns user" do - user = insert(:user) - query = from(t in User, where: t.id == ^user.id) - assert Repo.find_resource(query) == {:ok, user} - end - - test "returns not_found" do - query = from(t in User, where: t.id == ^"9gBuXNpD2NyDmmxxdw") - assert Repo.find_resource(query) == {:error, :not_found} - end - end - - describe "get_assoc/2" do - test "get assoc from preloaded data" do - user = %User{name: "Agent Smith"} - token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user} - assert Repo.get_assoc(token, :user) == {:ok, user} - end - - test "get one-to-one assoc from repo" do - user = insert(:user, name: "Jimi Hendrix") - token = refresh_record(insert(:oauth_token, user: user)) - - assert Repo.get_assoc(token, :user) == {:ok, user} - end - - test "get one-to-many assoc from repo" do - user = insert(:user) - - notification = - refresh_record(insert(:notification, user: user, activity: insert(:note_activity))) - - assert Repo.get_assoc(user, :notifications) == {:ok, [notification]} - end - - test "return error if has not assoc " do - token = insert(:oauth_token, user: nil) - assert Repo.get_assoc(token, :user) == {:error, :not_found} - end - end - - describe "chunk_stream/3" do - test "fetch records one-by-one" do - users = insert_list(50, :user) - - {fetch_users, 50} = - from(t in User) - |> Repo.chunk_stream(5) - |> Enum.reduce({[], 0}, fn %User{} = user, {acc, count} -> - {acc ++ [user], count + 1} - end) - - assert users == fetch_users - end - - test "fetch records in bulk" do - users = insert_list(50, :user) - - {fetch_users, 10} = - from(t in User) - |> Repo.chunk_stream(5, :batches) - |> Enum.reduce({[], 0}, fn users, {acc, count} -> - {acc ++ users, count + 1} - end) - - assert users == fetch_users - end - end -end diff --git a/test/report_note_test.exs b/test/report_note_test.exs @@ -1,16 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ReportNoteTest do - alias Pleroma.ReportNote - use Pleroma.DataCase - import Pleroma.Factory - - test "create/3" do - user = insert(:user) - report = insert(:report_activity) - assert {:ok, note} = ReportNote.create(user.id, report.id, "naughty boy") - assert note.content == "naughty boy" - end -end diff --git a/test/reverse_proxy/reverse_proxy_test.exs b/test/reverse_proxy/reverse_proxy_test.exs @@ -1,336 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ReverseProxyTest do - use Pleroma.Web.ConnCase, async: true - - import ExUnit.CaptureLog - import Mox - - alias Pleroma.ReverseProxy - alias Pleroma.ReverseProxy.ClientMock - alias Plug.Conn - - setup_all do - {:ok, _} = Registry.start_link(keys: :unique, name: ClientMock) - :ok - end - - setup :verify_on_exit! - - defp user_agent_mock(user_agent, invokes) do - json = Jason.encode!(%{"user-agent": user_agent}) - - ClientMock - |> expect(:request, fn :get, url, _, _, _ -> - Registry.register(ClientMock, url, 0) - - {:ok, 200, - [ - {"content-type", "application/json"}, - {"content-length", byte_size(json) |> to_string()} - ], %{url: url}} - end) - |> expect(:stream_body, invokes, fn %{url: url} = client -> - case Registry.lookup(ClientMock, url) do - [{_, 0}] -> - Registry.update_value(ClientMock, url, &(&1 + 1)) - {:ok, json, client} - - [{_, 1}] -> - Registry.unregister(ClientMock, url) - :done - end - end) - end - - describe "reverse proxy" do - test "do not track successful request", %{conn: conn} do - user_agent_mock("hackney/1.15.1", 2) - url = "/success" - - conn = ReverseProxy.call(conn, url) - - assert conn.status == 200 - assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, nil} - end - end - - describe "user-agent" do - test "don't keep", %{conn: conn} do - user_agent_mock("hackney/1.15.1", 2) - conn = ReverseProxy.call(conn, "/user-agent") - assert json_response(conn, 200) == %{"user-agent" => "hackney/1.15.1"} - end - - test "keep", %{conn: conn} do - user_agent_mock(Pleroma.Application.user_agent(), 2) - conn = ReverseProxy.call(conn, "/user-agent-keep", keep_user_agent: true) - assert json_response(conn, 200) == %{"user-agent" => Pleroma.Application.user_agent()} - end - end - - test "closed connection", %{conn: conn} do - ClientMock - |> expect(:request, fn :get, "/closed", _, _, _ -> {:ok, 200, [], %{}} end) - |> expect(:stream_body, fn _ -> {:error, :closed} end) - |> expect(:close, fn _ -> :ok end) - - conn = ReverseProxy.call(conn, "/closed") - assert conn.halted - end - - defp stream_mock(invokes, with_close? \\ false) do - ClientMock - |> expect(:request, fn :get, "/stream-bytes/" <> length, _, _, _ -> - Registry.register(ClientMock, "/stream-bytes/" <> length, 0) - - {:ok, 200, [{"content-type", "application/octet-stream"}], - %{url: "/stream-bytes/" <> length}} - end) - |> expect(:stream_body, invokes, fn %{url: "/stream-bytes/" <> length} = client -> - max = String.to_integer(length) - - case Registry.lookup(ClientMock, "/stream-bytes/" <> length) do - [{_, current}] when current < max -> - Registry.update_value( - ClientMock, - "/stream-bytes/" <> length, - &(&1 + 10) - ) - - {:ok, "0123456789", client} - - [{_, ^max}] -> - Registry.unregister(ClientMock, "/stream-bytes/" <> length) - :done - end - end) - - if with_close? do - expect(ClientMock, :close, fn _ -> :ok end) - end - end - - describe "max_body" do - test "length returns error if content-length more than option", %{conn: conn} do - user_agent_mock("hackney/1.15.1", 0) - - assert capture_log(fn -> - ReverseProxy.call(conn, "/huge-file", max_body_length: 4) - end) =~ - "[error] Elixir.Pleroma.ReverseProxy: request to \"/huge-file\" failed: :body_too_large" - - assert {:ok, true} == Cachex.get(:failed_proxy_url_cache, "/huge-file") - - assert capture_log(fn -> - ReverseProxy.call(conn, "/huge-file", max_body_length: 4) - end) == "" - end - - test "max_body_length returns error if streaming body more than that option", %{conn: conn} do - stream_mock(3, true) - - assert capture_log(fn -> - ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 30) - end) =~ - "[warn] Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large" - end - end - - describe "HEAD requests" do - test "common", %{conn: conn} do - ClientMock - |> expect(:request, fn :head, "/head", _, _, _ -> - {:ok, 200, [{"content-type", "text/html; charset=utf-8"}]} - end) - - conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), "/head") - assert html_response(conn, 200) == "" - end - end - - defp error_mock(status) when is_integer(status) do - ClientMock - |> expect(:request, fn :get, "/status/" <> _, _, _, _ -> - {:error, status} - end) - end - - describe "returns error on" do - test "500", %{conn: conn} do - error_mock(500) - url = "/status/500" - - capture_log(fn -> ReverseProxy.call(conn, url) end) =~ - "[error] Elixir.Pleroma.ReverseProxy: request to /status/500 failed with HTTP status 500" - - assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true} - - {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url) - assert ttl <= 60_000 - end - - test "400", %{conn: conn} do - error_mock(400) - url = "/status/400" - - capture_log(fn -> ReverseProxy.call(conn, url) end) =~ - "[error] Elixir.Pleroma.ReverseProxy: request to /status/400 failed with HTTP status 400" - - assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true} - assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil} - end - - test "403", %{conn: conn} do - error_mock(403) - url = "/status/403" - - capture_log(fn -> - ReverseProxy.call(conn, url, failed_request_ttl: :timer.seconds(120)) - end) =~ - "[error] Elixir.Pleroma.ReverseProxy: request to /status/403 failed with HTTP status 403" - - {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url) - assert ttl > 100_000 - end - - test "204", %{conn: conn} do - url = "/status/204" - expect(ClientMock, :request, fn :get, _url, _, _, _ -> {:ok, 204, [], %{}} end) - - capture_log(fn -> - conn = ReverseProxy.call(conn, url) - assert conn.resp_body == "Request failed: No Content" - assert conn.halted - end) =~ - "[error] Elixir.Pleroma.ReverseProxy: request to \"/status/204\" failed with HTTP status 204" - - assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true} - assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil} - end - end - - test "streaming", %{conn: conn} do - stream_mock(21) - conn = ReverseProxy.call(conn, "/stream-bytes/200") - assert conn.state == :chunked - assert byte_size(conn.resp_body) == 200 - assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"] - end - - defp headers_mock(_) do - ClientMock - |> expect(:request, fn :get, "/headers", headers, _, _ -> - Registry.register(ClientMock, "/headers", 0) - {:ok, 200, [{"content-type", "application/json"}], %{url: "/headers", headers: headers}} - end) - |> expect(:stream_body, 2, fn %{url: url, headers: headers} = client -> - case Registry.lookup(ClientMock, url) do - [{_, 0}] -> - Registry.update_value(ClientMock, url, &(&1 + 1)) - headers = for {k, v} <- headers, into: %{}, do: {String.capitalize(k), v} - {:ok, Jason.encode!(%{headers: headers}), client} - - [{_, 1}] -> - Registry.unregister(ClientMock, url) - :done - end - end) - - :ok - end - - describe "keep request headers" do - setup [:headers_mock] - - test "header passes", %{conn: conn} do - conn = - Conn.put_req_header( - conn, - "accept", - "text/html" - ) - |> ReverseProxy.call("/headers") - - %{"headers" => headers} = json_response(conn, 200) - assert headers["Accept"] == "text/html" - end - - test "header is filtered", %{conn: conn} do - conn = - Conn.put_req_header( - conn, - "accept-language", - "en-US" - ) - |> ReverseProxy.call("/headers") - - %{"headers" => headers} = json_response(conn, 200) - refute headers["Accept-Language"] - end - end - - test "returns 400 on non GET, HEAD requests", %{conn: conn} do - conn = ReverseProxy.call(Map.put(conn, :method, "POST"), "/ip") - assert conn.status == 400 - end - - describe "cache resp headers" do - test "add cache-control", %{conn: conn} do - ClientMock - |> expect(:request, fn :get, "/cache", _, _, _ -> - {:ok, 200, [{"ETag", "some ETag"}], %{}} - end) - |> expect(:stream_body, fn _ -> :done end) - - conn = ReverseProxy.call(conn, "/cache") - assert {"cache-control", "public, max-age=1209600"} in conn.resp_headers - end - end - - defp disposition_headers_mock(headers) do - ClientMock - |> expect(:request, fn :get, "/disposition", _, _, _ -> - Registry.register(ClientMock, "/disposition", 0) - - {:ok, 200, headers, %{url: "/disposition"}} - end) - |> expect(:stream_body, 2, fn %{url: "/disposition"} = client -> - case Registry.lookup(ClientMock, "/disposition") do - [{_, 0}] -> - Registry.update_value(ClientMock, "/disposition", &(&1 + 1)) - {:ok, "", client} - - [{_, 1}] -> - Registry.unregister(ClientMock, "/disposition") - :done - end - end) - end - - describe "response content disposition header" do - test "not atachment", %{conn: conn} do - disposition_headers_mock([ - {"content-type", "image/gif"}, - {"content-length", "0"} - ]) - - conn = ReverseProxy.call(conn, "/disposition") - - assert {"content-type", "image/gif"} in conn.resp_headers - end - - test "with content-disposition header", %{conn: conn} do - disposition_headers_mock([ - {"content-disposition", "attachment; filename=\"filename.jpg\""}, - {"content-length", "0"} - ]) - - conn = ReverseProxy.call(conn, "/disposition") - - assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers - end - end -end diff --git a/test/runtime_test.exs b/test/runtime_test.exs @@ -1,11 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.RuntimeTest do - use ExUnit.Case, async: true - - test "it loads custom runtime modules" do - assert {:module, RuntimeModule} == Code.ensure_compiled(RuntimeModule) - end -end diff --git a/test/safe_jsonb_set_test.exs b/test/safe_jsonb_set_test.exs @@ -1,16 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.SafeJsonbSetTest do - use Pleroma.DataCase - - test "it doesn't wipe the object when asked to set the value to NULL" do - assert %{rows: [[%{"key" => "value", "test" => nil}]]} = - Ecto.Adapters.SQL.query!( - Pleroma.Repo, - "select safe_jsonb_set('{\"key\": \"value\"}'::jsonb, '{test}', NULL);", - [] - ) - end -end diff --git a/test/scheduled_activity_test.exs b/test/scheduled_activity_test.exs @@ -1,105 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ScheduledActivityTest do - use Pleroma.DataCase - alias Pleroma.DataCase - alias Pleroma.ScheduledActivity - import Pleroma.Factory - - setup do: clear_config([ScheduledActivity, :enabled]) - - setup context do - DataCase.ensure_local_uploader(context) - end - - describe "creation" do - test "scheduled activities with jobs when ScheduledActivity enabled" do - Pleroma.Config.put([ScheduledActivity, :enabled], true) - user = insert(:user) - - today = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(6), :millisecond) - |> NaiveDateTime.to_iso8601() - - attrs = %{params: %{}, scheduled_at: today} - {:ok, sa1} = ScheduledActivity.create(user, attrs) - {:ok, sa2} = ScheduledActivity.create(user, attrs) - - jobs = - Repo.all(from(j in Oban.Job, where: j.queue == "scheduled_activities", select: j.args)) - - assert jobs == [%{"activity_id" => sa1.id}, %{"activity_id" => sa2.id}] - end - - test "scheduled activities without jobs when ScheduledActivity disabled" do - Pleroma.Config.put([ScheduledActivity, :enabled], false) - user = insert(:user) - - today = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(6), :millisecond) - |> NaiveDateTime.to_iso8601() - - attrs = %{params: %{}, scheduled_at: today} - {:ok, _sa1} = ScheduledActivity.create(user, attrs) - {:ok, _sa2} = ScheduledActivity.create(user, attrs) - - jobs = - Repo.all(from(j in Oban.Job, where: j.queue == "scheduled_activities", select: j.args)) - - assert jobs == [] - end - - test "when daily user limit is exceeded" do - user = insert(:user) - - today = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(6), :millisecond) - |> NaiveDateTime.to_iso8601() - - attrs = %{params: %{}, scheduled_at: today} - {:ok, _} = ScheduledActivity.create(user, attrs) - {:ok, _} = ScheduledActivity.create(user, attrs) - - {:error, changeset} = ScheduledActivity.create(user, attrs) - assert changeset.errors == [scheduled_at: {"daily limit exceeded", []}] - end - - test "when total user limit is exceeded" do - user = insert(:user) - - today = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(6), :millisecond) - |> NaiveDateTime.to_iso8601() - - tomorrow = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.hours(36), :millisecond) - |> NaiveDateTime.to_iso8601() - - {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: today}) - {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: today}) - {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) - {:error, changeset} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) - assert changeset.errors == [scheduled_at: {"total limit exceeded", []}] - end - - test "when scheduled_at is earlier than 5 minute from now" do - user = insert(:user) - - scheduled_at = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(4), :millisecond) - |> NaiveDateTime.to_iso8601() - - attrs = %{params: %{}, scheduled_at: scheduled_at} - {:error, changeset} = ScheduledActivity.create(user, attrs) - assert changeset.errors == [scheduled_at: {"must be at least 5 minutes from now", []}] - end - end -end diff --git a/test/signature_test.exs b/test/signature_test.exs @@ -1,134 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.SignatureTest do - use Pleroma.DataCase - - import ExUnit.CaptureLog - import Pleroma.Factory - import Tesla.Mock - import Mock - - alias Pleroma.Signature - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - @private_key "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA48qb4v6kqigZutO9Ot0wkp27GIF2LiVaADgxQORZozZR63jH\nTaoOrS3Xhngbgc8SSOhfXET3omzeCLqaLNfXnZ8OXmuhJfJSU6mPUvmZ9QdT332j\nfN/g3iWGhYMf/M9ftCKh96nvFVO/tMruzS9xx7tkrfJjehdxh/3LlJMMImPtwcD7\nkFXwyt1qZTAU6Si4oQAJxRDQXHp1ttLl3Ob829VM7IKkrVmY8TD+JSlV0jtVJPj6\n1J19ytKTx/7UaucYvb9HIiBpkuiy5n/irDqKLVf5QEdZoNCdojOZlKJmTLqHhzKP\n3E9TxsUjhrf4/EqegNc/j982RvOxeu4i40zMQwIDAQABAoIBAQDH5DXjfh21i7b4\ncXJuw0cqget617CDUhemdakTDs9yH+rHPZd3mbGDWuT0hVVuFe4vuGpmJ8c+61X0\nRvugOlBlavxK8xvYlsqTzAmPgKUPljyNtEzQ+gz0I+3mH2jkin2rL3D+SksZZgKm\nfiYMPIQWB2WUF04gB46DDb2mRVuymGHyBOQjIx3WC0KW2mzfoFUFRlZEF+Nt8Ilw\nT+g/u0aZ1IWoszbsVFOEdghgZET0HEarum0B2Je/ozcPYtwmU10iBANGMKdLqaP/\nj954BPunrUf6gmlnLZKIKklJj0advx0NA+cL79+zeVB3zexRYSA5o9q0WPhiuTwR\n/aedWHnBAoGBAP0sDWBAM1Y4TRAf8ZI9PcztwLyHPzfEIqzbObJJnx1icUMt7BWi\n+/RMOnhrlPGE1kMhOqSxvXYN3u+eSmWTqai2sSH5Hdw2EqnrISSTnwNUPINX7fHH\njEkgmXQ6ixE48SuBZnb4w1EjdB/BA6/sjL+FNhggOc87tizLTkMXmMtTAoGBAOZV\n+wPuAMBDBXmbmxCuDIjoVmgSlgeRunB1SA8RCPAFAiUo3+/zEgzW2Oz8kgI+xVwM\n33XkLKrWG1Orhpp6Hm57MjIc5MG+zF4/YRDpE/KNG9qU1tiz0UD5hOpIU9pP4bR/\ngxgPxZzvbk4h5BfHWLpjlk8UUpgk6uxqfti48c1RAoGBALBOKDZ6HwYRCSGMjUcg\n3NPEUi84JD8qmFc2B7Tv7h2he2ykIz9iFAGpwCIyETQsJKX1Ewi0OlNnD3RhEEAy\nl7jFGQ+mkzPSeCbadmcpYlgIJmf1KN/x7fDTAepeBpCEzfZVE80QKbxsaybd3Dp8\nCfwpwWUFtBxr4c7J+gNhAGe/AoGAPn8ZyqkrPv9wXtyfqFjxQbx4pWhVmNwrkBPi\nZ2Qh3q4dNOPwTvTO8vjghvzIyR8rAZzkjOJKVFgftgYWUZfM5gE7T2mTkBYq8W+U\n8LetF+S9qAM2gDnaDx0kuUTCq7t87DKk6URuQ/SbI0wCzYjjRD99KxvChVGPBHKo\n1DjqMuECgYEAgJGNm7/lJCS2wk81whfy/ttKGsEIkyhPFYQmdGzSYC5aDc2gp1R3\nxtOkYEvdjfaLfDGEa4UX8CHHF+w3t9u8hBtcdhMH6GYb9iv6z0VBTt4A/11HUR49\n3Z7TQ18Iyh3jAUCzFV9IJlLIExq5Y7P4B3ojWFBN607sDCt8BMPbDYs=\n-----END RSA PRIVATE KEY-----" - - @public_key "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw0P/Tq4gb4G/QVuMGbJo\nC/AfMNcv+m7NfrlOwkVzcU47jgESuYI4UtJayissCdBycHUnfVUd9qol+eznSODz\nCJhfJloqEIC+aSnuEPGA0POtWad6DU0E6/Ho5zQn5WAWUwbRQqowbrsm/GHo2+3v\neR5jGenwA6sYhINg/c3QQbksyV0uJ20Umyx88w8+TJuv53twOfmyDWuYNoQ3y5cc\nHKOZcLHxYOhvwg3PFaGfFHMFiNmF40dTXt9K96r7sbzc44iLD+VphbMPJEjkMuf8\nPGEFOBzy8pm3wJZw2v32RNW2VESwMYyqDzwHXGSq1a73cS7hEnc79gXlELsK04L9\nQQIDAQAB\n-----END PUBLIC KEY-----\n" - - @rsa_public_key { - :RSAPublicKey, - 24_650_000_183_914_698_290_885_268_529_673_621_967_457_234_469_123_179_408_466_269_598_577_505_928_170_923_974_132_111_403_341_217_239_999_189_084_572_368_839_502_170_501_850_920_051_662_384_964_248_315_257_926_552_945_648_828_895_432_624_227_029_881_278_113_244_073_644_360_744_504_606_177_648_469_825_063_267_913_017_309_199_785_535_546_734_904_379_798_564_556_494_962_268_682_532_371_146_333_972_821_570_577_277_375_020_977_087_539_994_500_097_107_935_618_711_808_260_846_821_077_839_605_098_669_707_417_692_791_905_543_116_911_754_774_323_678_879_466_618_738_207_538_013_885_607_095_203_516_030_057_611_111_308_904_599_045_146_148_350_745_339_208_006_497_478_057_622_336_882_506_112_530_056_970_653_403_292_123_624_453_213_574_011_183_684_739_084_105_206_483_178_943_532_208_537_215_396_831_110_268_758_639_826_369_857, - # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength - 65_537 - } - - defp make_fake_signature(key_id), do: "keyId=\"#{key_id}\"" - - defp make_fake_conn(key_id), - do: %Plug.Conn{req_headers: %{"signature" => make_fake_signature(key_id <> "#main-key")}} - - describe "fetch_public_key/1" do - test "it returns key" do - expected_result = {:ok, @rsa_public_key} - - user = insert(:user, public_key: @public_key) - - assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result - 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" - end - - test "it returns error if public key is nil" do - user = insert(:user, public_key: nil) - - assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == {:error, :error} - end - end - - describe "refetch_public_key/1" do - test "it returns key" do - ap_id = "https://mastodon.social/users/lambadalambda" - - assert Signature.refetch_public_key(make_fake_conn(ap_id)) == {:ok, @rsa_public_key} - end - - test "it returns error when not found user" do - assert capture_log(fn -> - {:error, _} = Signature.refetch_public_key(make_fake_conn("https://test-ap_id")) - end) =~ "[error] Could not decode user" - end - end - - describe "sign/2" do - test "it returns signature headers" do - user = - insert(:user, %{ - ap_id: "https://mastodon.social/users/lambadalambda", - keys: @private_key - }) - - assert Signature.sign( - user, - %{ - host: "test.test", - "content-length": 100 - } - ) == - "keyId=\"https://mastodon.social/users/lambadalambda#main-key\",algorithm=\"rsa-sha256\",headers=\"content-length host\",signature=\"sibUOoqsFfTDerquAkyprxzDjmJm6erYc42W5w1IyyxusWngSinq5ILTjaBxFvfarvc7ci1xAi+5gkBwtshRMWm7S+Uqix24Yg5EYafXRun9P25XVnYBEIH4XQ+wlnnzNIXQkU3PU9e6D8aajDZVp3hPJNeYt1gIPOA81bROI8/glzb1SAwQVGRbqUHHHKcwR8keiR/W2h7BwG3pVRy4JgnIZRSW7fQogKedDg02gzRXwUDFDk0pr2p3q6bUWHUXNV8cZIzlMK+v9NlyFbVYBTHctAR26GIAN6Hz0eV0mAQAePHDY1mXppbA8Gpp6hqaMuYfwifcXmcc+QFm4e+n3A==\"" - end - - test "it returns error" do - user = insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", keys: ""}) - - assert Signature.sign( - user, - %{host: "test.test", "content-length": 100} - ) == {:error, []} - end - end - - describe "key_id_to_actor_id/1" do - test "it properly deduces the actor id for misskey" do - assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") == - {:ok, "https://example.com/users/1234"} - end - - test "it properly deduces the actor id for mastodon and pleroma" 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 - ) do - assert Signature.key_id_to_actor_id("acct:raymoo@gensokyo.2hu") == - {:ok, "https://gensokyo.2hu/users/raymoo"} - end - end - end - - describe "signed_date" do - test "it returns formatted current date" do - with_mock(NaiveDateTime, utc_now: fn -> ~N[2019-08-23 18:11:24.822233] end) do - assert Signature.signed_date() == "Fri, 23 Aug 2019 18:11:24 GMT" - end - end - - test "it returns formatted date" do - assert Signature.signed_date(~N[2019-08-23 08:11:24.822233]) == - "Fri, 23 Aug 2019 08:11:24 GMT" - end - end -end diff --git a/test/stats_test.exs b/test/stats_test.exs @@ -1,122 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.StatsTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Stats - alias Pleroma.Web.CommonAPI - - describe "user count" do - test "it ignores internal users" do - _user = insert(:user, local: true) - _internal = insert(:user, local: true, nickname: nil) - _internal = Pleroma.Web.ActivityPub.Relay.get_actor() - - assert match?(%{stats: %{user_count: 1}}, Stats.calculate_stat_data()) - end - end - - describe "status visibility sum count" do - test "on new status" do - instance2 = "instance2.tld" - user = insert(:user) - other_user = insert(:user, %{ap_id: "https://#{instance2}/@actor"}) - - CommonAPI.post(user, %{visibility: "public", status: "hey"}) - - Enum.each(0..1, fn _ -> - CommonAPI.post(user, %{ - visibility: "unlisted", - status: "hey" - }) - end) - - Enum.each(0..2, fn _ -> - CommonAPI.post(user, %{ - visibility: "direct", - status: "hey @#{other_user.nickname}" - }) - end) - - Enum.each(0..3, fn _ -> - CommonAPI.post(user, %{ - visibility: "private", - status: "hey" - }) - end) - - assert %{"direct" => 3, "private" => 4, "public" => 1, "unlisted" => 2} = - Stats.get_status_visibility_count() - end - - test "on status delete" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{visibility: "public", status: "hey"}) - assert %{"public" => 1} = Stats.get_status_visibility_count() - CommonAPI.delete(activity.id, user) - assert %{"public" => 0} = Stats.get_status_visibility_count() - end - - test "on status visibility update" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{visibility: "public", status: "hey"}) - assert %{"public" => 1, "private" => 0} = Stats.get_status_visibility_count() - {:ok, _} = CommonAPI.update_activity_scope(activity.id, %{visibility: "private"}) - assert %{"public" => 0, "private" => 1} = Stats.get_status_visibility_count() - end - - test "doesn't count unrelated activities" do - user = insert(:user) - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{visibility: "public", status: "hey"}) - _ = CommonAPI.follow(user, other_user) - CommonAPI.favorite(other_user, activity.id) - CommonAPI.repeat(activity.id, other_user) - - assert %{"direct" => 0, "private" => 0, "public" => 1, "unlisted" => 0} = - Stats.get_status_visibility_count() - end - end - - describe "status visibility by instance count" do - test "single instance" do - local_instance = Pleroma.Web.Endpoint.url() |> String.split("//") |> Enum.at(1) - instance2 = "instance2.tld" - user1 = insert(:user) - user2 = insert(:user, %{ap_id: "https://#{instance2}/@actor"}) - - CommonAPI.post(user1, %{visibility: "public", status: "hey"}) - - Enum.each(1..5, fn _ -> - CommonAPI.post(user1, %{ - visibility: "unlisted", - status: "hey" - }) - end) - - Enum.each(1..10, fn _ -> - CommonAPI.post(user1, %{ - visibility: "direct", - status: "hey @#{user2.nickname}" - }) - end) - - Enum.each(1..20, fn _ -> - CommonAPI.post(user2, %{ - visibility: "private", - status: "hey" - }) - end) - - assert %{"direct" => 10, "private" => 0, "public" => 1, "unlisted" => 5} = - Stats.get_status_visibility_count(local_instance) - - assert %{"direct" => 0, "private" => 20, "public" => 0, "unlisted" => 0} = - Stats.get_status_visibility_count(instance2) - end - end -end diff --git a/test/support/captcha/mock.ex b/test/support/captcha/mock.ex @@ -0,0 +1,28 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Captcha.Mock do + alias Pleroma.Captcha.Service + @behaviour Service + + @solution "63615261b77f5354fb8c4e4986477555" + + def solution, do: @solution + + @impl Service + def new, + do: %{ + type: :mock, + token: "afa1815e14e29355e6c8f6b143a39fa2", + answer_data: @solution, + url: "https://example.org/captcha.png", + seconds_valid: 300 + } + + @impl Service + def validate(_token, captcha, captcha) when not is_nil(captcha), do: :ok + + def validate(_token, captcha, answer), + do: {:error, "Invalid CAPTCHA captcha: #{inspect(captcha)} ; answer: #{inspect(answer)}"} +end diff --git a/test/support/captcha_mock.ex b/test/support/captcha_mock.ex @@ -1,28 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Captcha.Mock do - alias Pleroma.Captcha.Service - @behaviour Service - - @solution "63615261b77f5354fb8c4e4986477555" - - def solution, do: @solution - - @impl Service - def new, - do: %{ - type: :mock, - token: "afa1815e14e29355e6c8f6b143a39fa2", - answer_data: @solution, - url: "https://example.org/captcha.png", - seconds_valid: 300 - } - - @impl Service - def validate(_token, captcha, captcha) when not is_nil(captcha), do: :ok - - def validate(_token, captcha, answer), - do: {:error, "Invalid CAPTCHA captcha: #{inspect(captcha)} ; answer: #{inspect(answer)}"} -end diff --git a/test/tasks/app_test.exs b/test/tasks/app_test.exs @@ -1,65 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.AppTest do - use Pleroma.DataCase, async: true - - setup_all do - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - end - - describe "creates new app" do - test "with default scopes" do - name = "Some name" - redirect = "https://example.com" - Mix.Tasks.Pleroma.App.run(["create", "-n", name, "-r", redirect]) - - assert_app(name, redirect, ["read", "write", "follow", "push"]) - end - - test "with custom scopes" do - name = "Another name" - redirect = "https://example.com" - - Mix.Tasks.Pleroma.App.run([ - "create", - "-n", - name, - "-r", - redirect, - "-s", - "read,write,follow,push,admin" - ]) - - assert_app(name, redirect, ["read", "write", "follow", "push", "admin"]) - end - end - - test "with errors" do - Mix.Tasks.Pleroma.App.run(["create"]) - {:mix_shell, :error, ["Creating failed:"]} - {:mix_shell, :error, ["name: can't be blank"]} - {:mix_shell, :error, ["redirect_uris: can't be blank"]} - end - - defp assert_app(name, redirect, scopes) do - app = Repo.get_by(Pleroma.Web.OAuth.App, client_name: name) - - assert_receive {:mix_shell, :info, [message]} - assert message == "#{name} successfully created:" - - assert_receive {:mix_shell, :info, [message]} - assert message == "App client_id: #{app.client_id}" - - assert_receive {:mix_shell, :info, [message]} - assert message == "App client_secret: #{app.client_secret}" - - assert app.scopes == scopes - assert app.redirect_uris == redirect - end -end diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs @@ -1,189 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.ConfigTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.ConfigDB - alias Pleroma.Repo - - setup_all do - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - Application.delete_env(:pleroma, :first_setting) - Application.delete_env(:pleroma, :second_setting) - end) - - :ok - end - - setup_all do: clear_config(:configurable_from_database, true) - - test "error if file with custom settings doesn't exist" do - Mix.Tasks.Pleroma.Config.migrate_to_db("config/not_existance_config_file.exs") - - assert_receive {:mix_shell, :info, - [ - "To migrate settings, you must define custom settings in config/not_existance_config_file.exs." - ]}, - 15 - end - - describe "migrate_to_db/1" do - setup do - initial = Application.get_env(:quack, :level) - on_exit(fn -> Application.put_env(:quack, :level, initial) end) - end - - @tag capture_log: true - test "config migration refused when deprecated settings are found" do - clear_config([:media_proxy, :whitelist], ["domain_without_scheme.com"]) - assert Repo.all(ConfigDB) == [] - - Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs") - - assert_received {:mix_shell, :error, [message]} - - assert message =~ - "Migration is not allowed until all deprecation warnings have been resolved." - end - - test "filtered settings are migrated to db" do - assert Repo.all(ConfigDB) == [] - - Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs") - - 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 - insert(:config, key: :first_setting, value: [key: "value", key2: ["Activity"]]) - assert Repo.aggregate(ConfigDB, :count, :id) == 1 - - Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs") - - config = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"}) - assert config.value == [key: "value", key2: [Repo]] - end - end - - describe "with deletion temp file" do - setup do - temp_file = "config/temp.exported_from_db.secret.exs" - - on_exit(fn -> - :ok = File.rm(temp_file) - end) - - {:ok, temp_file: temp_file} - end - - test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do - insert(:config, key: :setting_first, value: [key: "value", key2: ["Activity"]]) - insert(:config, key: :setting_second, value: [key: "value2", key2: [Repo]]) - insert(:config, group: :quack, key: :level, value: :info) - - Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"]) - - assert Repo.all(ConfigDB) == [] - - 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 - insert(:config, - key: :instance, - value: [ - name: "Pleroma", - email: "example@example.com", - notify_email: "noreply@example.com", - description: "A Pleroma instance, an alternative fediverse server", - limit: 5_000, - chat_limit: 5_000, - remote_limit: 100_000, - upload_limit: 16_000_000, - avatar_upload_limit: 2_000_000, - background_upload_limit: 4_000_000, - banner_upload_limit: 4_000_000, - poll_limits: %{ - max_options: 20, - max_option_chars: 200, - min_expiration: 0, - max_expiration: 365 * 24 * 60 * 60 - }, - registrations_open: true, - 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: [], - managed_config: true, - static_dir: "instance/static/", - allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"], - autofollowed_nicknames: [], - max_pinned_statuses: 1, - attachment_links: false, - max_report_comment_size: 1000, - safe_dm_mentions: false, - healthcheck: false, - remote_post_retention_days: 90, - skip_thread_containment: true, - limit_to_local_content: :unauthenticated, - user_bio_length: 5000, - user_name_length: 100, - max_account_fields: 10, - max_remote_account_fields: 20, - account_field_name_length: 512, - account_field_value_length: 2048, - external_user_synchronization: true, - extended_nickname_format: true, - multi_factor_authentication: [ - totp: [ - digits: 6, - period: 30 - ], - backup_codes: [ - number: 2, - length: 6 - ] - ] - ] - ) - - Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"]) - - assert Repo.all(ConfigDB) == [] - assert File.exists?(temp_file) - {:ok, file} = File.read(temp_file) - - header = - if Code.ensure_loaded?(Config.Reader) do - "import Config" - else - "use Mix.Config" - end - - assert file == - "#{header}\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" - end - end -end diff --git a/test/tasks/count_statuses_test.exs b/test/tasks/count_statuses_test.exs @@ -1,39 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.CountStatusesTest do - use Pleroma.DataCase - - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import ExUnit.CaptureIO, only: [capture_io: 1] - import Pleroma.Factory - - test "counts statuses" do - user = insert(:user) - {:ok, _} = CommonAPI.post(user, %{status: "test"}) - {:ok, _} = CommonAPI.post(user, %{status: "test2"}) - - user2 = insert(:user) - {:ok, _} = CommonAPI.post(user2, %{status: "test3"}) - - user = refresh_record(user) - user2 = refresh_record(user2) - - assert %{note_count: 2} = user - assert %{note_count: 1} = user2 - - {:ok, user} = User.update_note_count(user, 0) - {:ok, user2} = User.update_note_count(user2, 0) - - assert %{note_count: 0} = user - assert %{note_count: 0} = user2 - - assert capture_io(fn -> Mix.Tasks.Pleroma.CountStatuses.run([]) end) == "Done\n" - - assert %{note_count: 2} = refresh_record(user) - assert %{note_count: 1} = refresh_record(user2) - end -end diff --git a/test/tasks/database_test.exs b/test/tasks/database_test.exs @@ -1,175 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.DatabaseTest do - use Pleroma.DataCase - use Oban.Testing, repo: Pleroma.Repo - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - setup_all do - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - - :ok - end - - describe "running remove_embedded_objects" do - test "it replaces objects with references" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "test"}) - new_data = Map.put(activity.data, "object", activity.object.data) - - {:ok, activity} = - activity - |> Activity.change(%{data: new_data}) - |> Repo.update() - - assert is_map(activity.data["object"]) - - Mix.Tasks.Pleroma.Database.run(["remove_embedded_objects"]) - - activity = Activity.get_by_id_with_object(activity.id) - assert is_binary(activity.data["object"]) - end - end - - describe "prune_objects" do - test "it prunes old objects from the database" do - insert(:note) - deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) + 1 - - date = - Timex.now() - |> Timex.shift(days: -deadline) - |> Timex.to_naive_datetime() - |> NaiveDateTime.truncate(:second) - - %{id: id} = - :note - |> insert() - |> Ecto.Changeset.change(%{inserted_at: date}) - |> Repo.update!() - - assert length(Repo.all(Object)) == 2 - - Mix.Tasks.Pleroma.Database.run(["prune_objects"]) - - assert length(Repo.all(Object)) == 1 - refute Object.get_by_id(id) - end - end - - describe "running update_users_following_followers_counts" do - test "following and followers count are updated" do - [user, user2] = insert_pair(:user) - {:ok, %User{} = user} = User.follow(user, user2) - - following = User.following(user) - - assert length(following) == 2 - assert user.follower_count == 0 - - {:ok, user} = - user - |> Ecto.Changeset.change(%{follower_count: 3}) - |> Repo.update() - - assert user.follower_count == 3 - - assert :ok == Mix.Tasks.Pleroma.Database.run(["update_users_following_followers_counts"]) - - user = User.get_by_id(user.id) - - assert length(User.following(user)) == 2 - assert user.follower_count == 0 - end - end - - describe "running fix_likes_collections" do - test "it turns OrderedCollection likes into empty arrays" do - [user, user2] = insert_pair(:user) - - {:ok, %{id: id, object: object}} = CommonAPI.post(user, %{status: "test"}) - {:ok, %{object: object2}} = CommonAPI.post(user, %{status: "test test"}) - - CommonAPI.favorite(user2, id) - - likes = %{ - "first" => - "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1", - "id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes", - "totalItems" => 3, - "type" => "OrderedCollection" - } - - new_data = Map.put(object2.data, "likes", likes) - - object2 - |> Ecto.Changeset.change(%{data: new_data}) - |> Repo.update() - - assert length(Object.get_by_id(object.id).data["likes"]) == 1 - assert is_map(Object.get_by_id(object2.id).data["likes"]) - - assert :ok == Mix.Tasks.Pleroma.Database.run(["fix_likes_collections"]) - - assert length(Object.get_by_id(object.id).data["likes"]) == 1 - assert Enum.empty?(Object.get_by_id(object2.id).data["likes"]) - end - end - - describe "ensure_expiration" do - test "it adds to expiration old statuses" do - activity1 = insert(:note_activity) - - {:ok, inserted_at, 0} = DateTime.from_iso8601("2015-01-23T23:50:07Z") - activity2 = insert(:note_activity, %{inserted_at: inserted_at}) - - %{id: activity_id3} = insert(:note_activity) - - expires_at = DateTime.add(DateTime.utc_now(), 60 * 61) - - Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ - activity_id: activity_id3, - expires_at: expires_at - }) - - Mix.Tasks.Pleroma.Database.run(["ensure_expiration"]) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: activity1.id}, - scheduled_at: - activity1.inserted_at - |> DateTime.from_naive!("Etc/UTC") - |> Timex.shift(days: 365) - ) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: activity2.id}, - scheduled_at: - activity2.inserted_at - |> DateTime.from_naive!("Etc/UTC") - |> Timex.shift(days: 365) - ) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: activity_id3}, - scheduled_at: expires_at - ) - end - end -end diff --git a/test/tasks/digest_test.exs b/test/tasks/digest_test.exs @@ -1,60 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.DigestTest do - use Pleroma.DataCase - - import Pleroma.Factory - import Swoosh.TestAssertions - - alias Pleroma.Tests.ObanHelpers - alias Pleroma.Web.CommonAPI - - setup_all do - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - - :ok - end - - setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true) - - describe "pleroma.digest test" do - test "Sends digest to the given user" do - user1 = insert(:user) - user2 = insert(:user) - - Enum.each(0..10, fn i -> - {:ok, _activity} = - CommonAPI.post(user1, %{ - status: "hey ##{i} @#{user2.nickname}!" - }) - end) - - yesterday = - NaiveDateTime.add( - NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), - -60 * 60 * 24, - :second - ) - - {:ok, yesterday_date} = Timex.format(yesterday, "%F", :strftime) - - :ok = Mix.Tasks.Pleroma.Digest.run(["test", user2.nickname, yesterday_date]) - - ObanHelpers.perform_all() - - assert_receive {:mix_shell, :info, [message]} - assert message =~ "Digest email have been sent" - - assert_email_sent( - to: {user2.name, user2.email}, - html_body: ~r/here is what you've missed!/i - ) - end - end -end diff --git a/test/tasks/ecto/ecto_test.exs b/test/tasks/ecto/ecto_test.exs @@ -1,15 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.EctoTest do - use ExUnit.Case, async: true - - test "raise on bad path" do - assert_raise RuntimeError, ~r/Could not find migrations directory/, fn -> - Mix.Tasks.Pleroma.Ecto.ensure_migrations_path(Pleroma.Repo, - migrations_path: "some-path" - ) - end - end -end diff --git a/test/tasks/ecto/migrate_test.exs b/test/tasks/ecto/migrate_test.exs @@ -1,20 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-onl - -defmodule Mix.Tasks.Pleroma.Ecto.MigrateTest do - use Pleroma.DataCase, async: true - import ExUnit.CaptureLog - require Logger - - test "ecto.migrate info message" do - level = Logger.level() - Logger.configure(level: :warn) - - assert capture_log(fn -> - Mix.Tasks.Pleroma.Ecto.Migrate.run() - end) =~ "[info] Already up" - - Logger.configure(level: level) - end -end diff --git a/test/tasks/ecto/rollback_test.exs b/test/tasks/ecto/rollback_test.exs @@ -1,20 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.Ecto.RollbackTest do - use Pleroma.DataCase - import ExUnit.CaptureLog - require Logger - - test "ecto.rollback info message" do - level = Logger.level() - Logger.configure(level: :warn) - - assert capture_log(fn -> - Mix.Tasks.Pleroma.Ecto.Rollback.run() - end) =~ "[info] Rollback succesfully" - - Logger.configure(level: level) - end -end diff --git a/test/tasks/email_test.exs b/test/tasks/email_test.exs @@ -1,127 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.EmailTest do - use Pleroma.DataCase - - import Swoosh.TestAssertions - - alias Pleroma.Config - alias Pleroma.Tests.ObanHelpers - - import Pleroma.Factory - - setup_all do - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - - :ok - end - - setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true) - setup do: clear_config([:instance, :account_activation_required], true) - - describe "pleroma.email test" do - test "Sends test email with no given address" do - mail_to = Config.get([:instance, :email]) - - :ok = Mix.Tasks.Pleroma.Email.run(["test"]) - - ObanHelpers.perform_all() - - assert_receive {:mix_shell, :info, [message]} - assert message =~ "Test email has been sent" - - assert_email_sent( - to: mail_to, - html_body: ~r/a test email was requested./i - ) - end - - test "Sends test email with given address" do - mail_to = "hewwo@example.com" - - :ok = Mix.Tasks.Pleroma.Email.run(["test", "--to", mail_to]) - - ObanHelpers.perform_all() - - assert_receive {:mix_shell, :info, [message]} - assert message =~ "Test email has been sent" - - assert_email_sent( - to: mail_to, - html_body: ~r/a test email was requested./i - ) - end - - test "Sends confirmation emails" do - local_user1 = - insert(:user, %{ - confirmation_pending: true, - confirmation_token: "mytoken", - deactivated: false, - email: "local1@pleroma.com", - local: true - }) - - local_user2 = - insert(:user, %{ - confirmation_pending: true, - confirmation_token: "mytoken", - deactivated: false, - email: "local2@pleroma.com", - local: true - }) - - :ok = Mix.Tasks.Pleroma.Email.run(["resend_confirmation_emails"]) - - ObanHelpers.perform_all() - - assert_email_sent(to: {local_user1.name, local_user1.email}) - assert_email_sent(to: {local_user2.name, local_user2.email}) - end - - test "Does not send confirmation email to inappropriate users" do - # confirmed user - insert(:user, %{ - confirmation_pending: false, - confirmation_token: "mytoken", - deactivated: false, - email: "confirmed@pleroma.com", - local: true - }) - - # remote user - insert(:user, %{ - deactivated: false, - email: "remote@not-pleroma.com", - local: false - }) - - # deactivated user = - insert(:user, %{ - deactivated: true, - email: "deactivated@pleroma.com", - local: false - }) - - # invisible user - insert(:user, %{ - deactivated: false, - email: "invisible@pleroma.com", - local: true, - invisible: true - }) - - :ok = Mix.Tasks.Pleroma.Email.run(["resend_confirmation_emails"]) - - ObanHelpers.perform_all() - - refute_email_sent() - end - end -end diff --git a/test/tasks/emoji_test.exs b/test/tasks/emoji_test.exs @@ -1,243 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.EmojiTest do - use ExUnit.Case, async: true - - import ExUnit.CaptureIO - import Tesla.Mock - - alias Mix.Tasks.Pleroma.Emoji - - describe "ls-packs" do - test "with default manifest as url" do - mock(fn - %{ - method: :get, - url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json" - } -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/emoji/packs/default-manifest.json") - } - end) - - capture_io(fn -> Emoji.run(["ls-packs"]) end) =~ - "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip" - end - - test "with passed manifest as file" do - capture_io(fn -> - Emoji.run(["ls-packs", "-m", "test/fixtures/emoji/packs/manifest.json"]) - end) =~ "https://git.pleroma.social/pleroma/emoji-index/raw/master/packs/blobs_gg.zip" - end - end - - describe "get-packs" do - test "download pack from default manifest" do - mock(fn - %{ - method: :get, - url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json" - } -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/emoji/packs/default-manifest.json") - } - - %{ - method: :get, - url: "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip" - } -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/emoji/packs/blank.png.zip") - } - - %{ - method: :get, - url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/finmoji.json" - } -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/emoji/packs/finmoji.json") - } - end) - - assert capture_io(fn -> Emoji.run(["get-packs", "finmoji"]) end) =~ "Writing pack.json for" - - emoji_path = - Path.join( - Pleroma.Config.get!([:instance, :static_dir]), - "emoji" - ) - - assert File.exists?(Path.join([emoji_path, "finmoji", "pack.json"])) - on_exit(fn -> File.rm_rf!("test/instance_static/emoji/finmoji") end) - end - - test "install local emoji pack" do - assert capture_io(fn -> - Emoji.run([ - "get-packs", - "local", - "--manifest", - "test/instance_static/local_pack/manifest.json" - ]) - end) =~ "Writing pack.json for" - - on_exit(fn -> File.rm_rf!("test/instance_static/emoji/local") end) - end - - test "pack not found" do - mock(fn - %{ - method: :get, - url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json" - } -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/emoji/packs/default-manifest.json") - } - end) - - assert capture_io(fn -> Emoji.run(["get-packs", "not_found"]) end) =~ - "No pack named \"not_found\" found" - end - - test "raise on bad sha256" do - mock(fn - %{ - method: :get, - url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/packs/blobs_gg.zip" - } -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/emoji/packs/blank.png.zip") - } - end) - - assert_raise RuntimeError, ~r/^Bad SHA256 for blobs.gg/, fn -> - capture_io(fn -> - Emoji.run(["get-packs", "blobs.gg", "-m", "test/fixtures/emoji/packs/manifest.json"]) - end) - end - end - end - - describe "gen-pack" do - setup do - url = "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip" - - mock(fn %{ - method: :get, - url: ^url - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/emoji/packs/blank.png.zip")} - end) - - {:ok, url: url} - end - - test "with default extensions", %{url: url} do - name = "pack1" - pack_json = "#{name}.json" - files_json = "#{name}_file.json" - refute File.exists?(pack_json) - refute File.exists?(files_json) - - captured = - capture_io(fn -> - Emoji.run([ - "gen-pack", - url, - "--name", - name, - "--license", - "license", - "--homepage", - "homepage", - "--description", - "description", - "--files", - files_json, - "--extensions", - ".png .gif" - ]) - end) - - assert captured =~ "#{pack_json} has been created with the pack1 pack" - assert captured =~ "Using .png .gif extensions" - - assert File.exists?(pack_json) - assert File.exists?(files_json) - - on_exit(fn -> - File.rm!(pack_json) - File.rm!(files_json) - end) - end - - test "with custom extensions and update existing files", %{url: url} do - name = "pack2" - pack_json = "#{name}.json" - files_json = "#{name}_file.json" - refute File.exists?(pack_json) - refute File.exists?(files_json) - - captured = - capture_io(fn -> - Emoji.run([ - "gen-pack", - url, - "--name", - name, - "--license", - "license", - "--homepage", - "homepage", - "--description", - "description", - "--files", - files_json, - "--extensions", - " .png .gif .jpeg " - ]) - end) - - assert captured =~ "#{pack_json} has been created with the pack2 pack" - assert captured =~ "Using .png .gif .jpeg extensions" - - assert File.exists?(pack_json) - assert File.exists?(files_json) - - captured = - capture_io(fn -> - Emoji.run([ - "gen-pack", - url, - "--name", - name, - "--license", - "license", - "--homepage", - "homepage", - "--description", - "description", - "--files", - files_json, - "--extensions", - " .png .gif .jpeg " - ]) - end) - - assert captured =~ "#{pack_json} has been updated with the pack2 pack" - - on_exit(fn -> - File.rm!(pack_json) - File.rm!(files_json) - end) - end - end -end diff --git a/test/tasks/frontend_test.exs b/test/tasks/frontend_test.exs @@ -1,85 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.FrontendTest do - use Pleroma.DataCase - alias Mix.Tasks.Pleroma.Frontend - - import ExUnit.CaptureIO, only: [capture_io: 1] - - @dir "test/frontend_static_test" - - setup do - File.mkdir_p!(@dir) - clear_config([:instance, :static_dir], @dir) - - on_exit(fn -> - File.rm_rf(@dir) - end) - end - - test "it downloads and unzips a known frontend" do - clear_config([:frontends, :available], %{ - "pleroma" => %{ - "ref" => "fantasy", - "name" => "pleroma", - "build_url" => "http://gensokyo.2hu/builds/${ref}" - } - }) - - Tesla.Mock.mock(fn %{url: "http://gensokyo.2hu/builds/fantasy"} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/frontend_dist.zip")} - end) - - capture_io(fn -> - Frontend.run(["install", "pleroma"]) - end) - - assert File.exists?(Path.join([@dir, "frontends", "pleroma", "fantasy", "test.txt"])) - end - - test "it also works given a file" do - clear_config([:frontends, :available], %{ - "pleroma" => %{ - "ref" => "fantasy", - "name" => "pleroma", - "build_dir" => "" - } - }) - - folder = Path.join([@dir, "frontends", "pleroma", "fantasy"]) - previously_existing = Path.join([folder, "temp"]) - File.mkdir_p!(folder) - File.write!(previously_existing, "yey") - assert File.exists?(previously_existing) - - capture_io(fn -> - Frontend.run(["install", "pleroma", "--file", "test/fixtures/tesla_mock/frontend.zip"]) - end) - - assert File.exists?(Path.join([folder, "test.txt"])) - refute File.exists?(previously_existing) - end - - test "it downloads and unzips unknown frontends" do - Tesla.Mock.mock(fn %{url: "http://gensokyo.2hu/madeup.zip"} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/frontend.zip")} - end) - - capture_io(fn -> - Frontend.run([ - "install", - "unknown", - "--ref", - "baka", - "--build-url", - "http://gensokyo.2hu/madeup.zip", - "--build-dir", - "" - ]) - end) - - assert File.exists?(Path.join([@dir, "frontends", "unknown", "baka", "test.txt"])) - end -end diff --git a/test/tasks/instance_test.exs b/test/tasks/instance_test.exs @@ -1,99 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.InstanceTest do - use ExUnit.Case - - setup do - File.mkdir_p!(tmp_path()) - - on_exit(fn -> - File.rm_rf(tmp_path()) - static_dir = Pleroma.Config.get([:instance, :static_dir], "test/instance_static/") - - if File.exists?(static_dir) do - File.rm_rf(Path.join(static_dir, "robots.txt")) - end - - Pleroma.Config.put([:instance, :static_dir], static_dir) - end) - - :ok - end - - defp tmp_path do - "/tmp/generated_files/" - end - - test "running gen" do - mix_task = fn -> - Mix.Tasks.Pleroma.Instance.run([ - "gen", - "--output", - tmp_path() <> "generated_config.exs", - "--output-psql", - tmp_path() <> "setup.psql", - "--domain", - "test.pleroma.social", - "--instance-name", - "Pleroma", - "--admin-email", - "admin@example.com", - "--notify-email", - "notify@example.com", - "--dbhost", - "dbhost", - "--dbname", - "dbname", - "--dbuser", - "dbuser", - "--dbpass", - "dbpass", - "--indexable", - "y", - "--db-configurable", - "y", - "--rum", - "y", - "--listen-port", - "4000", - "--listen-ip", - "127.0.0.1", - "--uploads-dir", - "test/uploads", - "--static-dir", - "./test/../test/instance/static/", - "--strip-uploads", - "y", - "--dedupe-uploads", - "n", - "--anonymize-uploads", - "n" - ]) - end - - ExUnit.CaptureIO.capture_io(fn -> - mix_task.() - end) - - generated_config = File.read!(tmp_path() <> "generated_config.exs") - assert generated_config =~ "host: \"test.pleroma.social\"" - assert generated_config =~ "name: \"Pleroma\"" - assert generated_config =~ "email: \"admin@example.com\"" - assert generated_config =~ "notify_email: \"notify@example.com\"" - assert generated_config =~ "hostname: \"dbhost\"" - assert generated_config =~ "database: \"dbname\"" - assert generated_config =~ "username: \"dbuser\"" - assert generated_config =~ "password: \"dbpass\"" - assert generated_config =~ "configurable_from_database: true" - assert generated_config =~ "http: [ip: {127, 0, 0, 1}, port: 4000]" - assert generated_config =~ "filters: [Pleroma.Upload.Filter.ExifTool]" - assert File.read!(tmp_path() <> "setup.psql") == generated_setup_psql() - assert File.exists?(Path.expand("./test/instance/static/robots.txt")) - end - - defp generated_setup_psql do - ~s(CREATE USER dbuser WITH ENCRYPTED PASSWORD 'dbpass';\nCREATE DATABASE dbname OWNER dbuser;\n\\c dbname;\n--Extensions made by ecto.migrate that need superuser access\nCREATE EXTENSION IF NOT EXISTS citext;\nCREATE EXTENSION IF NOT EXISTS pg_trgm;\nCREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";\nCREATE EXTENSION IF NOT EXISTS rum;\n) - end -end diff --git a/test/tasks/pleroma_test.exs b/test/tasks/pleroma_test.exs @@ -1,50 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.PleromaTest do - use ExUnit.Case, async: true - import Mix.Pleroma - - setup_all do - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - - :ok - end - - describe "shell_prompt/1" do - test "input" do - send(self(), {:mix_shell_input, :prompt, "Yes"}) - - answer = shell_prompt("Do you want this?") - assert_received {:mix_shell, :prompt, [message]} - assert message =~ "Do you want this?" - assert answer == "Yes" - end - - test "with defval" do - send(self(), {:mix_shell_input, :prompt, "\n"}) - - answer = shell_prompt("Do you want this?", "defval") - - assert_received {:mix_shell, :prompt, [message]} - assert message =~ "Do you want this? [defval]" - assert answer == "defval" - end - end - - describe "get_option/3" do - test "get from options" do - assert get_option([domain: "some-domain.com"], :domain, "Promt") == "some-domain.com" - end - - test "get from prompt" do - send(self(), {:mix_shell_input, :prompt, "another-domain.com"}) - assert get_option([], :domain, "Prompt") == "another-domain.com" - end - end -end diff --git a/test/tasks/refresh_counter_cache_test.exs b/test/tasks/refresh_counter_cache_test.exs @@ -1,43 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.RefreshCounterCacheTest do - use Pleroma.DataCase - alias Pleroma.Web.CommonAPI - import ExUnit.CaptureIO, only: [capture_io: 1] - import Pleroma.Factory - - test "counts statuses" do - user = insert(:user) - other_user = insert(:user) - - CommonAPI.post(user, %{visibility: "public", status: "hey"}) - - Enum.each(0..1, fn _ -> - CommonAPI.post(user, %{ - visibility: "unlisted", - status: "hey" - }) - end) - - Enum.each(0..2, fn _ -> - CommonAPI.post(user, %{ - visibility: "direct", - status: "hey @#{other_user.nickname}" - }) - end) - - Enum.each(0..3, fn _ -> - CommonAPI.post(user, %{ - visibility: "private", - status: "hey" - }) - end) - - assert capture_io(fn -> Mix.Tasks.Pleroma.RefreshCounterCache.run([]) end) =~ "Done\n" - - assert %{"direct" => 3, "private" => 4, "public" => 1, "unlisted" => 2} = - Pleroma.Stats.get_status_visibility_count() - end -end diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs @@ -1,180 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.RelayTest do - alias Pleroma.Activity - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Relay - alias Pleroma.Web.ActivityPub.Utils - use Pleroma.DataCase - - import Pleroma.Factory - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - - :ok - end - - describe "running follow" do - test "relay is followed" do - target_instance = "http://mastodon.example.org/users/admin" - - Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) - - local_user = Relay.get_actor() - assert local_user.ap_id =~ "/relay" - - target_user = User.get_cached_by_ap_id(target_instance) - refute target_user.local - - activity = Utils.fetch_latest_follow(local_user, target_user) - assert activity.data["type"] == "Follow" - assert activity.data["actor"] == local_user.ap_id - assert activity.data["object"] == target_user.ap_id - - :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) - - assert_receive {:mix_shell, :info, - [ - "http://mastodon.example.org/users/admin - no Accept received (relay didn't follow back)" - ]} - end - end - - describe "running unfollow" do - test "relay is unfollowed" do - user = insert(:user) - target_instance = user.ap_id - - Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) - - %User{ap_id: follower_id} = local_user = Relay.get_actor() - target_user = User.get_cached_by_ap_id(target_instance) - follow_activity = Utils.fetch_latest_follow(local_user, target_user) - User.follow(local_user, target_user) - assert "#{target_instance}/followers" in User.following(local_user) - Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance]) - - cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) - assert cancelled_activity.data["state"] == "cancelled" - - [undo_activity] = - ActivityPub.fetch_activities([], %{ - type: "Undo", - actor_id: follower_id, - limit: 1, - skip_preload: true, - invisible_actors: true - }) - - assert undo_activity.data["type"] == "Undo" - assert undo_activity.data["actor"] == local_user.ap_id - assert undo_activity.data["object"]["id"] == cancelled_activity.data["id"] - refute "#{target_instance}/followers" in User.following(local_user) - end - - test "unfollow when relay is dead" do - user = insert(:user) - target_instance = user.ap_id - - Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) - - %User{ap_id: follower_id} = local_user = Relay.get_actor() - target_user = User.get_cached_by_ap_id(target_instance) - follow_activity = Utils.fetch_latest_follow(local_user, target_user) - User.follow(local_user, target_user) - - assert "#{target_instance}/followers" in User.following(local_user) - - Tesla.Mock.mock(fn %{method: :get, url: ^target_instance} -> - %Tesla.Env{status: 404} - end) - - Pleroma.Repo.delete(user) - Cachex.clear(:user_cache) - - Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance]) - - cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) - assert cancelled_activity.data["state"] == "accept" - - assert [] == - ActivityPub.fetch_activities( - [], - %{ - type: "Undo", - actor_id: follower_id, - skip_preload: true, - invisible_actors: true - } - ) - end - - test "force unfollow when relay is dead" do - user = insert(:user) - target_instance = user.ap_id - - Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) - - %User{ap_id: follower_id} = local_user = Relay.get_actor() - target_user = User.get_cached_by_ap_id(target_instance) - follow_activity = Utils.fetch_latest_follow(local_user, target_user) - User.follow(local_user, target_user) - - assert "#{target_instance}/followers" in User.following(local_user) - - Tesla.Mock.mock(fn %{method: :get, url: ^target_instance} -> - %Tesla.Env{status: 404} - end) - - Pleroma.Repo.delete(user) - Cachex.clear(:user_cache) - - Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance, "--force"]) - - cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) - assert cancelled_activity.data["state"] == "cancelled" - - [undo_activity] = - ActivityPub.fetch_activities( - [], - %{type: "Undo", actor_id: follower_id, skip_preload: true, invisible_actors: true} - ) - - assert undo_activity.data["type"] == "Undo" - assert undo_activity.data["actor"] == local_user.ap_id - assert undo_activity.data["object"]["id"] == cancelled_activity.data["id"] - refute "#{target_instance}/followers" in User.following(local_user) - end - end - - describe "mix pleroma.relay list" do - test "Prints relay subscription list" do - :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) - - refute_receive {:mix_shell, :info, _} - - relay_user = Relay.get_actor() - - ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"] - |> Enum.each(fn ap_id -> - {:ok, user} = User.get_or_fetch_by_ap_id(ap_id) - User.follow(relay_user, user) - end) - - :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) - - assert_receive {:mix_shell, :info, ["https://mstdn.io/users/mayuutann"]} - assert_receive {:mix_shell, :info, ["http://mastodon.example.org/users/admin"]} - end - end -end diff --git a/test/tasks/robots_txt_test.exs b/test/tasks/robots_txt_test.exs @@ -1,45 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.RobotsTxtTest do - use ExUnit.Case - use Pleroma.Tests.Helpers - alias Mix.Tasks.Pleroma.RobotsTxt - - setup do: clear_config([:instance, :static_dir]) - - test "creates new dir" do - path = "test/fixtures/new_dir/" - file_path = path <> "robots.txt" - Pleroma.Config.put([:instance, :static_dir], path) - - on_exit(fn -> - {:ok, ["test/fixtures/new_dir/", "test/fixtures/new_dir/robots.txt"]} = File.rm_rf(path) - end) - - RobotsTxt.run(["disallow_all"]) - - assert File.exists?(file_path) - {:ok, file} = File.read(file_path) - - assert file == "User-Agent: *\nDisallow: /\n" - end - - test "to existance folder" do - path = "test/fixtures/" - file_path = path <> "robots.txt" - Pleroma.Config.put([:instance, :static_dir], path) - - on_exit(fn -> - :ok = File.rm(file_path) - end) - - RobotsTxt.run(["disallow_all"]) - - assert File.exists?(file_path) - {:ok, file} = File.read(file_path) - - assert file == "User-Agent: *\nDisallow: /\n" - end -end diff --git a/test/tasks/uploads_test.exs b/test/tasks/uploads_test.exs @@ -1,56 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.UploadsTest do - alias Pleroma.Upload - use Pleroma.DataCase - - import Mock - - setup_all do - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - - :ok - end - - describe "running migrate_local" do - test "uploads migrated" do - with_mock Upload, - store: fn %Upload{name: _file, path: _path}, _opts -> {:ok, %{}} end do - Mix.Tasks.Pleroma.Uploads.run(["migrate_local", "S3"]) - - assert_received {:mix_shell, :info, [message]} - assert message =~ "Migrating files from local" - - assert_received {:mix_shell, :info, [message]} - - assert %{"total_count" => total_count} = - Regex.named_captures(~r"^Found (?<total_count>\d+) uploads$", message) - - assert_received {:mix_shell, :info, [message]} - - # @logevery in Mix.Tasks.Pleroma.Uploads - count = - min(50, String.to_integer(total_count)) - |> to_string() - - assert %{"count" => ^count, "total_count" => ^total_count} = - Regex.named_captures( - ~r"^Uploaded (?<count>\d+)/(?<total_count>\d+) files$", - message - ) - end - end - - test "nonexistent uploader" do - assert_raise RuntimeError, ~r/The uploader .* is not an existing/, fn -> - Mix.Tasks.Pleroma.Uploads.run(["migrate_local", "nonexistent"]) - end - end - end -end diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs @@ -1,614 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.UserTest do - alias Pleroma.Activity - alias Pleroma.MFA - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.Token - - use Pleroma.DataCase - use Oban.Testing, repo: Pleroma.Repo - - import ExUnit.CaptureIO - import Mock - import Pleroma.Factory - - setup_all do - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - - :ok - end - - describe "running new" do - test "user is created" do - # just get random data - unsaved = build(:user) - - # prepare to answer yes - send(self(), {:mix_shell_input, :yes?, true}) - - Mix.Tasks.Pleroma.User.run([ - "new", - unsaved.nickname, - unsaved.email, - "--name", - unsaved.name, - "--bio", - unsaved.bio, - "--password", - "test", - "--moderator", - "--admin" - ]) - - assert_received {:mix_shell, :info, [message]} - assert message =~ "user will be created" - - assert_received {:mix_shell, :yes?, [message]} - assert message =~ "Continue" - - assert_received {:mix_shell, :info, [message]} - assert message =~ "created" - - user = User.get_cached_by_nickname(unsaved.nickname) - assert user.name == unsaved.name - assert user.email == unsaved.email - assert user.bio == unsaved.bio - assert user.is_moderator - assert user.is_admin - end - - test "user is not created" do - unsaved = build(:user) - - # prepare to answer no - send(self(), {:mix_shell_input, :yes?, false}) - - Mix.Tasks.Pleroma.User.run(["new", unsaved.nickname, unsaved.email]) - - assert_received {:mix_shell, :info, [message]} - assert message =~ "user will be created" - - assert_received {:mix_shell, :yes?, [message]} - assert message =~ "Continue" - - assert_received {:mix_shell, :info, [message]} - assert message =~ "will not be created" - - refute User.get_cached_by_nickname(unsaved.nickname) - end - end - - describe "running rm" do - test "user is deleted" do - clear_config([:instance, :federating], true) - user = insert(:user) - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - Mix.Tasks.Pleroma.User.run(["rm", user.nickname]) - ObanHelpers.perform_all() - - assert_received {:mix_shell, :info, [message]} - assert message =~ " deleted" - assert %{deactivated: true} = User.get_by_nickname(user.nickname) - - assert called(Pleroma.Web.Federator.publish(:_)) - end - end - - test "a remote user's create activity is deleted when the object has been pruned" do - user = insert(:user) - user2 = insert(:user) - - {:ok, post} = CommonAPI.post(user, %{status: "uguu"}) - {:ok, post2} = CommonAPI.post(user2, %{status: "test"}) - obj = Object.normalize(post2) - - {:ok, like_object, meta} = Pleroma.Web.ActivityPub.Builder.like(user, obj) - - {:ok, like_activity, _meta} = - Pleroma.Web.ActivityPub.Pipeline.common_pipeline( - like_object, - Keyword.put(meta, :local, true) - ) - - like_activity.data["object"] - |> Pleroma.Object.get_by_ap_id() - |> Repo.delete() - - clear_config([:instance, :federating], true) - - object = Object.normalize(post) - Object.prune(object) - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - Mix.Tasks.Pleroma.User.run(["rm", user.nickname]) - ObanHelpers.perform_all() - - assert_received {:mix_shell, :info, [message]} - assert message =~ " deleted" - assert %{deactivated: true} = User.get_by_nickname(user.nickname) - - assert called(Pleroma.Web.Federator.publish(:_)) - refute Pleroma.Repo.get(Pleroma.Activity, like_activity.id) - end - - refute Activity.get_by_id(post.id) - end - - test "no user to delete" do - Mix.Tasks.Pleroma.User.run(["rm", "nonexistent"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No local user" - end - end - - describe "running toggle_activated" do - test "user is deactivated" do - user = insert(:user) - - Mix.Tasks.Pleroma.User.run(["toggle_activated", user.nickname]) - - assert_received {:mix_shell, :info, [message]} - assert message =~ " deactivated" - - user = User.get_cached_by_nickname(user.nickname) - assert user.deactivated - end - - test "user is activated" do - user = insert(:user, deactivated: true) - - Mix.Tasks.Pleroma.User.run(["toggle_activated", user.nickname]) - - assert_received {:mix_shell, :info, [message]} - assert message =~ " activated" - - user = User.get_cached_by_nickname(user.nickname) - refute user.deactivated - end - - test "no user to toggle" do - Mix.Tasks.Pleroma.User.run(["toggle_activated", "nonexistent"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No user" - end - end - - describe "running deactivate" do - test "user is unsubscribed" do - followed = insert(:user) - remote_followed = insert(:user, local: false) - user = insert(:user) - - User.follow(user, followed, :follow_accept) - User.follow(user, remote_followed, :follow_accept) - - Mix.Tasks.Pleroma.User.run(["deactivate", user.nickname]) - - assert_received {:mix_shell, :info, [message]} - assert message =~ "Deactivating" - - # Note that the task has delay :timer.sleep(500) - assert_received {:mix_shell, :info, [message]} - assert message =~ "Successfully unsubscribed" - - user = User.get_cached_by_nickname(user.nickname) - assert Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) - assert user.deactivated - end - - test "no user to deactivate" do - Mix.Tasks.Pleroma.User.run(["deactivate", "nonexistent"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No user" - end - end - - describe "running set" do - test "All statuses set" do - user = insert(:user) - - Mix.Tasks.Pleroma.User.run([ - "set", - user.nickname, - "--admin", - "--confirmed", - "--locked", - "--moderator" - ]) - - assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Admin status .* true/ - - assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Confirmation pending .* false/ - - assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Locked status .* true/ - - assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Moderator status .* true/ - - user = User.get_cached_by_nickname(user.nickname) - assert user.is_moderator - assert user.locked - assert user.is_admin - refute user.confirmation_pending - end - - test "All statuses unset" do - user = - insert(:user, locked: true, is_moderator: true, is_admin: true, confirmation_pending: true) - - Mix.Tasks.Pleroma.User.run([ - "set", - user.nickname, - "--no-admin", - "--no-confirmed", - "--no-locked", - "--no-moderator" - ]) - - assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Admin status .* false/ - - assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Confirmation pending .* true/ - - assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Locked status .* false/ - - assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Moderator status .* false/ - - user = User.get_cached_by_nickname(user.nickname) - refute user.is_moderator - refute user.locked - refute user.is_admin - assert user.confirmation_pending - end - - test "no user to set status" do - Mix.Tasks.Pleroma.User.run(["set", "nonexistent", "--moderator"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No local user" - end - end - - describe "running reset_password" do - test "password reset token is generated" do - user = insert(:user) - - assert capture_io(fn -> - Mix.Tasks.Pleroma.User.run(["reset_password", user.nickname]) - end) =~ "URL:" - - assert_received {:mix_shell, :info, [message]} - assert message =~ "Generated" - end - - test "no user to reset password" do - Mix.Tasks.Pleroma.User.run(["reset_password", "nonexistent"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No local user" - end - end - - describe "running reset_mfa" do - test "disables MFA" do - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: "xx", confirmed: true} - } - ) - - Mix.Tasks.Pleroma.User.run(["reset_mfa", user.nickname]) - - assert_received {:mix_shell, :info, [message]} - assert message == "Multi-Factor Authentication disabled for #{user.nickname}" - - assert %{enabled: false, totp: false} == - user.nickname - |> User.get_cached_by_nickname() - |> MFA.mfa_settings() - end - - test "no user to reset MFA" do - Mix.Tasks.Pleroma.User.run(["reset_password", "nonexistent"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No local user" - end - end - - describe "running invite" do - test "invite token is generated" do - assert capture_io(fn -> - Mix.Tasks.Pleroma.User.run(["invite"]) - end) =~ "http" - - assert_received {:mix_shell, :info, [message]} - assert message =~ "Generated user invite token one time" - end - - test "token is generated with expires_at" do - assert capture_io(fn -> - Mix.Tasks.Pleroma.User.run([ - "invite", - "--expires-at", - Date.to_string(Date.utc_today()) - ]) - end) - - assert_received {:mix_shell, :info, [message]} - assert message =~ "Generated user invite token date limited" - end - - test "token is generated with max use" do - assert capture_io(fn -> - Mix.Tasks.Pleroma.User.run([ - "invite", - "--max-use", - "5" - ]) - end) - - assert_received {:mix_shell, :info, [message]} - assert message =~ "Generated user invite token reusable" - end - - test "token is generated with max use and expires date" do - assert capture_io(fn -> - Mix.Tasks.Pleroma.User.run([ - "invite", - "--max-use", - "5", - "--expires-at", - Date.to_string(Date.utc_today()) - ]) - end) - - assert_received {:mix_shell, :info, [message]} - assert message =~ "Generated user invite token reusable date limited" - end - end - - describe "running invites" do - test "invites are listed" do - {:ok, invite} = Pleroma.UserInviteToken.create_invite() - - {:ok, invite2} = - Pleroma.UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 15}) - - # assert capture_io(fn -> - Mix.Tasks.Pleroma.User.run([ - "invites" - ]) - - # end) - - assert_received {:mix_shell, :info, [message]} - assert_received {:mix_shell, :info, [message2]} - assert_received {:mix_shell, :info, [message3]} - assert message =~ "Invites list:" - assert message2 =~ invite.invite_type - assert message3 =~ invite2.invite_type - end - end - - describe "running revoke_invite" do - test "invite is revoked" do - {:ok, invite} = Pleroma.UserInviteToken.create_invite(%{expires_at: Date.utc_today()}) - - assert capture_io(fn -> - Mix.Tasks.Pleroma.User.run([ - "revoke_invite", - invite.token - ]) - end) - - assert_received {:mix_shell, :info, [message]} - assert message =~ "Invite for token #{invite.token} was revoked." - end - - test "it prints an error message when invite is not exist" do - Mix.Tasks.Pleroma.User.run(["revoke_invite", "foo"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No invite found" - end - end - - describe "running delete_activities" do - test "activities are deleted" do - %{nickname: nickname} = insert(:user) - - assert :ok == Mix.Tasks.Pleroma.User.run(["delete_activities", nickname]) - assert_received {:mix_shell, :info, [message]} - assert message == "User #{nickname} statuses deleted." - end - - test "it prints an error message when user is not exist" do - Mix.Tasks.Pleroma.User.run(["delete_activities", "foo"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No local user" - end - end - - describe "running toggle_confirmed" do - test "user is confirmed" do - %{id: id, nickname: nickname} = insert(:user, confirmation_pending: false) - - assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname]) - assert_received {:mix_shell, :info, [message]} - assert message == "#{nickname} needs confirmation." - - user = Repo.get(User, id) - assert user.confirmation_pending - assert user.confirmation_token - end - - test "user is not confirmed" do - %{id: id, nickname: nickname} = - insert(:user, confirmation_pending: true, confirmation_token: "some token") - - assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname]) - assert_received {:mix_shell, :info, [message]} - assert message == "#{nickname} doesn't need confirmation." - - user = Repo.get(User, id) - refute user.confirmation_pending - refute user.confirmation_token - end - - test "it prints an error message when user is not exist" do - Mix.Tasks.Pleroma.User.run(["toggle_confirmed", "foo"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No local user" - end - end - - describe "search" do - test "it returns users matching" do - user = insert(:user) - moon = insert(:user, nickname: "moon", name: "fediverse expert moon") - moot = insert(:user, nickname: "moot") - kawen = insert(:user, nickname: "kawen", name: "fediverse expert moon") - - {:ok, user} = User.follow(user, moon) - - assert [moon.id, kawen.id] == User.Search.search("moon") |> Enum.map(& &1.id) - - res = User.search("moo") |> Enum.map(& &1.id) - assert Enum.sort([moon.id, moot.id, kawen.id]) == Enum.sort(res) - - assert [kawen.id, moon.id] == User.Search.search("expert fediverse") |> Enum.map(& &1.id) - - assert [moon.id, kawen.id] == - User.Search.search("expert fediverse", for_user: user) |> Enum.map(& &1.id) - end - end - - describe "signing out" do - test "it deletes all user's tokens and authorizations" do - user = insert(:user) - insert(:oauth_token, user: user) - insert(:oauth_authorization, user: user) - - assert Repo.get_by(Token, user_id: user.id) - assert Repo.get_by(Authorization, user_id: user.id) - - :ok = Mix.Tasks.Pleroma.User.run(["sign_out", user.nickname]) - - refute Repo.get_by(Token, user_id: user.id) - refute Repo.get_by(Authorization, user_id: user.id) - end - - test "it prints an error message when user is not exist" do - Mix.Tasks.Pleroma.User.run(["sign_out", "foo"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No local user" - end - end - - describe "tagging" do - test "it add tags to a user" do - user = insert(:user) - - :ok = Mix.Tasks.Pleroma.User.run(["tag", user.nickname, "pleroma"]) - - user = User.get_cached_by_nickname(user.nickname) - assert "pleroma" in user.tags - end - - test "it prints an error message when user is not exist" do - Mix.Tasks.Pleroma.User.run(["tag", "foo"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "Could not change user tags" - end - end - - describe "untagging" do - test "it deletes tags from a user" do - user = insert(:user, tags: ["pleroma"]) - assert "pleroma" in user.tags - - :ok = Mix.Tasks.Pleroma.User.run(["untag", user.nickname, "pleroma"]) - - user = User.get_cached_by_nickname(user.nickname) - assert Enum.empty?(user.tags) - end - - test "it prints an error message when user is not exist" do - Mix.Tasks.Pleroma.User.run(["untag", "foo"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "Could not change user tags" - end - end - - describe "bulk confirm and unconfirm" do - test "confirm all" do - user1 = insert(:user, confirmation_pending: true) - user2 = insert(:user, confirmation_pending: true) - - assert user1.confirmation_pending - assert user2.confirmation_pending - - Mix.Tasks.Pleroma.User.run(["confirm_all"]) - - user1 = User.get_cached_by_nickname(user1.nickname) - user2 = User.get_cached_by_nickname(user2.nickname) - - refute user1.confirmation_pending - refute user2.confirmation_pending - end - - test "unconfirm all" do - user1 = insert(:user, confirmation_pending: false) - user2 = insert(:user, confirmation_pending: false) - admin = insert(:user, is_admin: true, confirmation_pending: false) - mod = insert(:user, is_moderator: true, confirmation_pending: false) - - refute user1.confirmation_pending - refute user2.confirmation_pending - - Mix.Tasks.Pleroma.User.run(["unconfirm_all"]) - - user1 = User.get_cached_by_nickname(user1.nickname) - user2 = User.get_cached_by_nickname(user2.nickname) - admin = User.get_cached_by_nickname(admin.nickname) - mod = User.get_cached_by_nickname(mod.nickname) - - assert user1.confirmation_pending - assert user2.confirmation_pending - refute admin.confirmation_pending - refute mod.confirmation_pending - end - end -end diff --git a/test/upload/filter/anonymize_filename_test.exs b/test/upload/filter/anonymize_filename_test.exs @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.Upload - - setup do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - upload_file = %Upload{ - name: "an… image.jpg", - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg") - } - - %{upload_file: upload_file} - end - - setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text]) - - test "it replaces filename on pre-defined text", %{upload_file: upload_file} do - Config.put([Upload.Filter.AnonymizeFilename, :text], "custom-file.png") - {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) - assert name == "custom-file.png" - end - - test "it replaces filename on pre-defined text expression", %{upload_file: upload_file} do - Config.put([Upload.Filter.AnonymizeFilename, :text], "custom-file.{extension}") - {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) - assert name == "custom-file.jpg" - end - - test "it replaces filename on random text", %{upload_file: upload_file} do - {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) - assert <<_::bytes-size(14)>> <> ".jpg" = name - refute name == "an… image.jpg" - end -end diff --git a/test/upload/filter/dedupe_test.exs b/test/upload/filter/dedupe_test.exs @@ -1,32 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Upload.Filter.DedupeTest do - use Pleroma.DataCase - - alias Pleroma.Upload - alias Pleroma.Upload.Filter.Dedupe - - @shasum "e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781" - - test "adds shasum" do - File.cp!( - "test/fixtures/image.jpg", - "test/fixtures/image_tmp.jpg" - ) - - upload = %Upload{ - name: "an… image.jpg", - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - tempfile: Path.absname("test/fixtures/image_tmp.jpg") - } - - assert { - :ok, - :filtered, - %Pleroma.Upload{id: @shasum, path: @shasum <> ".jpg"} - } = Dedupe.filter(upload) - end -end diff --git a/test/upload/filter/exiftool_test.exs b/test/upload/filter/exiftool_test.exs @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Upload.Filter.ExiftoolTest do - use Pleroma.DataCase - alias Pleroma.Upload.Filter - - test "apply exiftool filter" do - assert Pleroma.Utils.command_available?("exiftool") - - File.cp!( - "test/fixtures/DSCN0010.jpg", - "test/fixtures/DSCN0010_tmp.jpg" - ) - - upload = %Pleroma.Upload{ - name: "image_with_GPS_data.jpg", - content_type: "image/jpg", - path: Path.absname("test/fixtures/DSCN0010.jpg"), - tempfile: Path.absname("test/fixtures/DSCN0010_tmp.jpg") - } - - assert Filter.Exiftool.filter(upload) == {:ok, :filtered} - - {exif_original, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010.jpg"]) - {exif_filtered, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010_tmp.jpg"]) - - refute exif_original == exif_filtered - assert String.match?(exif_original, ~r/GPS/) - 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.filter(upload) == {:ok, :noop} - end -end diff --git a/test/upload/filter/mogrifun_test.exs b/test/upload/filter/mogrifun_test.exs @@ -1,44 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Upload.Filter.MogrifunTest do - use Pleroma.DataCase - import Mock - - alias Pleroma.Upload - alias Pleroma.Upload.Filter - - test "apply mogrify filter" do - File.cp!( - "test/fixtures/image.jpg", - "test/fixtures/image_tmp.jpg" - ) - - upload = %Upload{ - name: "an… image.jpg", - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - tempfile: Path.absname("test/fixtures/image_tmp.jpg") - } - - task = - Task.async(fn -> - assert_receive {:apply_filter, {}}, 4_000 - end) - - with_mocks([ - {Mogrify, [], - [ - open: fn _f -> %Mogrify.Image{} end, - custom: fn _m, _a -> send(task.pid, {:apply_filter, {}}) end, - custom: fn _m, _a, _o -> send(task.pid, {:apply_filter, {}}) end, - save: fn _f, _o -> :ok end - ]} - ]) do - assert Filter.Mogrifun.filter(upload) == {:ok, :filtered} - end - - Task.await(task) - end -end diff --git a/test/upload/filter/mogrify_test.exs b/test/upload/filter/mogrify_test.exs @@ -1,41 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Upload.Filter.MogrifyTest do - use Pleroma.DataCase - import Mock - - alias Pleroma.Upload.Filter - - test "apply mogrify filter" do - clear_config(Filter.Mogrify, args: [{"tint", "40"}]) - - File.cp!( - "test/fixtures/image.jpg", - "test/fixtures/image_tmp.jpg" - ) - - upload = %Pleroma.Upload{ - name: "an… image.jpg", - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - tempfile: Path.absname("test/fixtures/image_tmp.jpg") - } - - task = - Task.async(fn -> - assert_receive {:apply_filter, {_, "tint", "40"}}, 4_000 - end) - - with_mock Mogrify, - open: fn _f -> %Mogrify.Image{} end, - custom: fn _m, _a -> :ok end, - custom: fn m, a, o -> send(task.pid, {:apply_filter, {m, a, o}}) end, - save: fn _f, _o -> :ok end do - assert Filter.Mogrify.filter(upload) == {:ok, :filtered} - end - - Task.await(task) - end -end diff --git a/test/upload/filter_test.exs b/test/upload/filter_test.exs @@ -1,33 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Upload.FilterTest do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.Upload.Filter - - setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text]) - - test "applies filters" do - Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], "custom-file.png") - - File.cp!( - "test/fixtures/image.jpg", - "test/fixtures/image_tmp.jpg" - ) - - upload = %Pleroma.Upload{ - name: "an… image.jpg", - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - tempfile: Path.absname("test/fixtures/image_tmp.jpg") - } - - assert Filter.filter([], upload) == {:ok, upload} - - assert {:ok, upload} = Filter.filter([Pleroma.Upload.Filter.AnonymizeFilename], upload) - assert upload.name == "custom-file.png" - end -end diff --git a/test/upload_test.exs b/test/upload_test.exs @@ -1,287 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.UploadTest do - use Pleroma.DataCase - - import ExUnit.CaptureLog - - alias Pleroma.Upload - alias Pleroma.Uploaders.Uploader - - @upload_file %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "image.jpg" - } - - defmodule TestUploaderBase do - def put_file(%{path: path} = _upload, module_name) do - task_pid = - Task.async(fn -> - :timer.sleep(10) - - {Uploader, path} - |> :global.whereis_name() - |> send({Uploader, self(), {:test}, %{}}) - - assert_receive {Uploader, {:test}}, 4_000 - end) - - Agent.start(fn -> task_pid end, name: module_name) - - :wait_callback - end - end - - describe "Tried storing a file when http callback response success result" do - defmodule TestUploaderSuccess do - def http_callback(conn, _params), - do: {:ok, conn, {:file, "post-process-file.jpg"}} - - def put_file(upload), do: TestUploaderBase.put_file(upload, __MODULE__) - end - - setup do: [uploader: TestUploaderSuccess] - setup [:ensure_local_uploader] - - 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" - } - ] - }} - - Task.await(Agent.get(TestUploaderSuccess, fn task_pid -> task_pid end)) - end - end - - describe "Tried storing a file when http callback response error" do - defmodule TestUploaderError do - def http_callback(conn, _params), do: {:error, conn, "Errors"} - - def put_file(upload), do: TestUploaderBase.put_file(upload, __MODULE__) - end - - setup do: [uploader: TestUploaderError] - setup [:ensure_local_uploader] - - test "it returns error" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - assert capture_log(fn -> - assert Upload.store(@upload_file) == {:error, "Errors"} - Task.await(Agent.get(TestUploaderError, fn task_pid -> task_pid end)) - end) =~ - "[error] Elixir.Pleroma.Upload store (using Pleroma.UploadTest.TestUploaderError) failed: \"Errors\"" - end - end - - describe "Tried storing a file when http callback doesn't response by timeout" do - defmodule(TestUploader, do: def(put_file(_upload), do: :wait_callback)) - setup do: [uploader: TestUploader] - setup [:ensure_local_uploader] - - test "it returns error" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - assert capture_log(fn -> - assert Upload.store(@upload_file) == {:error, "Uploader callback timeout"} - end) =~ - "[error] Elixir.Pleroma.Upload store (using Pleroma.UploadTest.TestUploader) failed: \"Uploader callback timeout\"" - end - end - - describe "Storing a file with the Local uploader" do - setup [:ensure_local_uploader] - - test "does not allow descriptions longer than the post limit" do - clear_config([:instance, :description_limit], 2) - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "image.jpg" - } - - {:error, :description_too_long} = Upload.store(file, description: "123") - end - - test "returns a media url" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "image.jpg" - } - - {:ok, data} = Upload.store(file) - - assert %{"url" => [%{"href" => url}]} = data - - assert String.starts_with?(url, Pleroma.Web.base_url() <> "/media/") - end - - test "copies the file to the configured folder with deduping" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "an [image.jpg" - } - - {:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.Dedupe]) - - assert List.first(data["url"])["href"] == - Pleroma.Web.base_url() <> - "/media/e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781.jpg" - end - - test "copies the file to the configured folder without deduping" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "an [image.jpg" - } - - {:ok, data} = Upload.store(file) - assert data["name"] == "an [image.jpg" - end - - test "fixes incorrect content type" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "application/octet-stream", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "an [image.jpg" - } - - {:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.Dedupe]) - assert hd(data["url"])["mediaType"] == "image/jpeg" - end - - test "adds missing extension" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "an [image" - } - - {:ok, data} = Upload.store(file) - assert data["name"] == "an [image.jpg" - end - - test "fixes incorrect file extension" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "an [image.blah" - } - - {:ok, data} = Upload.store(file) - assert data["name"] == "an [image.jpg" - end - - test "don't modify filename of an unknown type" do - File.cp("test/fixtures/test.txt", "test/fixtures/test_tmp.txt") - - file = %Plug.Upload{ - content_type: "text/plain", - path: Path.absname("test/fixtures/test_tmp.txt"), - filename: "test.txt" - } - - {:ok, data} = Upload.store(file) - assert data["name"] == "test.txt" - end - - test "copies the file to the configured folder with anonymizing filename" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "an [image.jpg" - } - - {:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.AnonymizeFilename]) - - refute data["name"] == "an [image.jpg" - end - - test "escapes invalid characters in url" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "an… image.jpg" - } - - {:ok, data} = Upload.store(file) - [attachment_url | _] = data["url"] - - assert Path.basename(attachment_url["href"]) == "an%E2%80%A6%20image.jpg" - end - - test "escapes reserved uri characters" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: ":?#[]@!$&\\'()*+,;=.jpg" - } - - {:ok, data} = Upload.store(file) - [attachment_url | _] = data["url"] - - assert Path.basename(attachment_url["href"]) == - "%3A%3F%23%5B%5D%40%21%24%26%5C%27%28%29%2A%2B%2C%3B%3D.jpg" - end - end - - describe "Setting a custom base_url for uploaded media" do - setup do: clear_config([Pleroma.Upload, :base_url], "https://cache.pleroma.social") - - test "returns a media url with configured base_url" do - base_url = Pleroma.Config.get([Pleroma.Upload, :base_url]) - - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "image.jpg" - } - - {:ok, data} = Upload.store(file, base_url: base_url) - - assert %{"url" => [%{"href" => url}]} = data - - refute String.starts_with?(url, base_url <> "/media/") - end - end -end diff --git a/test/uploaders/local_test.exs b/test/uploaders/local_test.exs @@ -1,55 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Uploaders.LocalTest do - use Pleroma.DataCase - alias Pleroma.Uploaders.Local - - describe "get_file/1" do - test "it returns path to local folder for files" do - assert Local.get_file("") == {:ok, {:static_dir, "test/uploads"}} - end - end - - describe "put_file/1" do - test "put file to local folder" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - file_path = "local_upload/files/image.jpg" - - file = %Pleroma.Upload{ - name: "image.jpg", - content_type: "image/jpg", - path: file_path, - tempfile: Path.absname("test/fixtures/image_tmp.jpg") - } - - assert Local.put_file(file) == :ok - - assert Path.join([Local.upload_path(), file_path]) - |> File.exists?() - end - end - - describe "delete_file/1" do - test "deletes local file" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - file_path = "local_upload/files/image.jpg" - - file = %Pleroma.Upload{ - name: "image.jpg", - content_type: "image/jpg", - path: file_path, - tempfile: Path.absname("test/fixtures/image_tmp.jpg") - } - - :ok = Local.put_file(file) - local_path = Path.join([Local.upload_path(), file_path]) - assert File.exists?(local_path) - - Local.delete_file(file_path) - - refute File.exists?(local_path) - end - end -end diff --git a/test/uploaders/s3_test.exs b/test/uploaders/s3_test.exs @@ -1,88 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Uploaders.S3Test do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.Uploaders.S3 - - import Mock - import ExUnit.CaptureLog - - setup do: - clear_config(Pleroma.Uploaders.S3, - bucket: "test_bucket", - public_endpoint: "https://s3.amazonaws.com" - ) - - describe "get_file/1" do - test "it returns path to local folder for files" do - assert S3.get_file("test_image.jpg") == { - :ok, - {:url, "https://s3.amazonaws.com/test_bucket/test_image.jpg"} - } - end - - test "it returns path without bucket when truncated_namespace set to ''" do - Config.put([Pleroma.Uploaders.S3], - bucket: "test_bucket", - public_endpoint: "https://s3.amazonaws.com", - truncated_namespace: "" - ) - - assert S3.get_file("test_image.jpg") == { - :ok, - {:url, "https://s3.amazonaws.com/test_image.jpg"} - } - end - - test "it returns path with bucket namespace when namespace is set" do - Config.put([Pleroma.Uploaders.S3], - bucket: "test_bucket", - public_endpoint: "https://s3.amazonaws.com", - bucket_namespace: "family" - ) - - assert S3.get_file("test_image.jpg") == { - :ok, - {:url, "https://s3.amazonaws.com/family:test_bucket/test_image.jpg"} - } - end - end - - describe "put_file/1" do - setup do - file_upload = %Pleroma.Upload{ - name: "image-tet.jpg", - content_type: "image/jpg", - path: "test_folder/image-tet.jpg", - tempfile: Path.absname("test/instance_static/add/shortcode.png") - } - - [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 - 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 - end - end - - describe "delete_file/1" do - test_with_mock "deletes file", ExAws, request: fn _req -> {:ok, %{status_code: 204}} end do - assert :ok = S3.delete_file("image.jpg") - assert_called(ExAws.request(:_)) - end - end -end diff --git a/test/user/import_test.exs b/test/user/import_test.exs @@ -1,76 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.User.ImportTest do - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - - use Pleroma.DataCase - use Oban.Testing, repo: Pleroma.Repo - - import Pleroma.Factory - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe "follow_import" do - test "it imports user followings from list" do - [user1, user2, user3] = insert_list(3, :user) - - identifiers = [ - user2.ap_id, - user3.nickname - ] - - {:ok, job} = User.Import.follow_import(user1, identifiers) - - assert {:ok, result} = ObanHelpers.perform(job) - assert is_list(result) - assert result == [user2, user3] - assert User.following?(user1, user2) - assert User.following?(user1, user3) - end - end - - describe "blocks_import" do - test "it imports user blocks from list" do - [user1, user2, user3] = insert_list(3, :user) - - identifiers = [ - user2.ap_id, - user3.nickname - ] - - {:ok, job} = User.Import.blocks_import(user1, identifiers) - - assert {:ok, result} = ObanHelpers.perform(job) - assert is_list(result) - assert result == [user2, user3] - assert User.blocks?(user1, user2) - assert User.blocks?(user1, user3) - end - end - - describe "mutes_import" do - test "it imports user mutes from list" do - [user1, user2, user3] = insert_list(3, :user) - - identifiers = [ - user2.ap_id, - user3.nickname - ] - - {:ok, job} = User.Import.mutes_import(user1, identifiers) - - assert {:ok, result} = ObanHelpers.perform(job) - assert is_list(result) - assert result == [user2, user3] - assert User.mutes?(user1, user2) - assert User.mutes?(user1, user3) - end - end -end diff --git a/test/user/notification_setting_test.exs b/test/user/notification_setting_test.exs @@ -1,21 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.User.NotificationSettingTest do - use Pleroma.DataCase - - alias Pleroma.User.NotificationSetting - - describe "changeset/2" do - test "sets option to hide notification contents" do - changeset = - NotificationSetting.changeset( - %NotificationSetting{}, - %{"hide_notification_contents" => true} - ) - - assert %Ecto.Changeset{valid?: true} = changeset - end - end -end diff --git a/test/user/query_test.exs b/test/user/query_test.exs @@ -1,37 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.User.QueryTest do - use Pleroma.DataCase, async: true - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.User.Query - alias Pleroma.Web.ActivityPub.InternalFetchActor - - import Pleroma.Factory - - describe "internal users" do - test "it filters out internal users by default" do - %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor() - - assert [_user] = User |> Repo.all() - assert [] == %{} |> Query.build() |> Repo.all() - end - - test "it filters out users without nickname by default" do - insert(:user, %{nickname: nil}) - - assert [_user] = User |> Repo.all() - assert [] == %{} |> Query.build() |> Repo.all() - end - - test "it returns internal users when enabled" do - %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor() - insert(:user, %{nickname: nil}) - - assert %{internal: true} |> Query.build() |> Repo.aggregate(:count) == 2 - end - end -end diff --git a/test/user/welcome_chat_massage_test.exs b/test/user/welcome_chat_massage_test.exs @@ -1,35 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.User.WelcomeChatMessageTest do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.User.WelcomeChatMessage - - import Pleroma.Factory - - setup do: clear_config([:welcome]) - - describe "post_message/1" do - test "send a chat welcome message" do - welcome_user = insert(:user, name: "mewmew") - user = insert(:user) - - Config.put([:welcome, :chat_message, :enabled], true) - Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname) - - Config.put( - [:welcome, :chat_message, :message], - "Hello, welcome to Blob/Cat!" - ) - - {:ok, %Pleroma.Activity{} = activity} = WelcomeChatMessage.post_message(user) - - assert user.ap_id in activity.recipients - assert Pleroma.Object.normalize(activity).data["type"] == "ChatMessage" - assert Pleroma.Object.normalize(activity).data["content"] == "Hello, welcome to Blob/Cat!" - end - end -end diff --git a/test/user/welcome_email_test.exs b/test/user/welcome_email_test.exs @@ -1,61 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.User.WelcomeEmailTest do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User.WelcomeEmail - - import Pleroma.Factory - import Swoosh.TestAssertions - - setup do: clear_config([:welcome]) - - describe "send_email/1" do - test "send a welcome email" do - user = insert(:user, name: "Jimm") - - Config.put([:welcome, :email, :enabled], true) - Config.put([:welcome, :email, :sender], "welcome@pleroma.app") - - Config.put( - [:welcome, :email, :subject], - "Hello, welcome to pleroma: <%= instance_name %>" - ) - - Config.put( - [:welcome, :email, :html], - "<h1>Hello <%= user.name %>.</h1> <p>Welcome to <%= instance_name %></p>" - ) - - instance_name = Config.get([:instance, :name]) - - {:ok, _job} = WelcomeEmail.send_email(user) - - ObanHelpers.perform_all() - - assert_email_sent( - from: {instance_name, "welcome@pleroma.app"}, - to: {user.name, user.email}, - subject: "Hello, welcome to pleroma: #{instance_name}", - html_body: "<h1>Hello #{user.name}.</h1> <p>Welcome to #{instance_name}</p>" - ) - - Config.put([:welcome, :email, :sender], {"Pleroma App", "welcome@pleroma.app"}) - - {:ok, _job} = WelcomeEmail.send_email(user) - - ObanHelpers.perform_all() - - assert_email_sent( - from: {"Pleroma App", "welcome@pleroma.app"}, - to: {user.name, user.email}, - subject: "Hello, welcome to pleroma: #{instance_name}", - html_body: "<h1>Hello #{user.name}.</h1> <p>Welcome to #{instance_name}</p>" - ) - end - end -end diff --git a/test/user/welcome_message_test.exs b/test/user/welcome_message_test.exs @@ -1,34 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.User.WelcomeMessageTest do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.User.WelcomeMessage - - import Pleroma.Factory - - setup do: clear_config([:welcome]) - - describe "post_message/1" do - test "send a direct welcome message" do - welcome_user = insert(:user) - user = insert(:user, name: "Jimm") - - Config.put([:welcome, :direct_message, :enabled], true) - Config.put([:welcome, :direct_message, :sender_nickname], welcome_user.nickname) - - Config.put( - [:welcome, :direct_message, :message], - "Hello. Welcome to Pleroma" - ) - - {:ok, %Pleroma.Activity{} = activity} = WelcomeMessage.post_message(user) - assert user.ap_id in activity.recipients - assert activity.data["directMessage"] == true - assert Pleroma.Object.normalize(activity).data["content"] =~ "Hello. Welcome to Pleroma" - end - end -end diff --git a/test/user_invite_token_test.exs b/test/user_invite_token_test.exs @@ -1,96 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.UserInviteTokenTest do - use ExUnit.Case, async: true - alias Pleroma.UserInviteToken - - describe "valid_invite?/1 one time invites" do - setup do - invite = %UserInviteToken{invite_type: "one_time"} - - {:ok, invite: invite} - end - - test "not used returns true", %{invite: invite} do - invite = %{invite | used: false} - assert UserInviteToken.valid_invite?(invite) - end - - test "used returns false", %{invite: invite} do - invite = %{invite | used: true} - refute UserInviteToken.valid_invite?(invite) - end - end - - describe "valid_invite?/1 reusable invites" do - setup do - invite = %UserInviteToken{ - invite_type: "reusable", - max_use: 5 - } - - {:ok, invite: invite} - end - - test "with less uses then max use returns true", %{invite: invite} do - invite = %{invite | uses: 4} - assert UserInviteToken.valid_invite?(invite) - end - - test "with equal or more uses then max use returns false", %{invite: invite} do - invite = %{invite | uses: 5} - - refute UserInviteToken.valid_invite?(invite) - - invite = %{invite | uses: 6} - - refute UserInviteToken.valid_invite?(invite) - end - end - - describe "valid_token?/1 date limited invites" do - setup do - invite = %UserInviteToken{invite_type: "date_limited"} - {:ok, invite: invite} - end - - test "expires today returns true", %{invite: invite} do - invite = %{invite | expires_at: Date.utc_today()} - assert UserInviteToken.valid_invite?(invite) - end - - test "expires yesterday returns false", %{invite: invite} do - invite = %{invite | expires_at: Date.add(Date.utc_today(), -1)} - refute UserInviteToken.valid_invite?(invite) - end - end - - describe "valid_token?/1 reusable date limited invites" do - setup do - invite = %UserInviteToken{invite_type: "reusable_date_limited", max_use: 5} - {:ok, invite: invite} - end - - test "not overdue date and less uses returns true", %{invite: invite} do - invite = %{invite | expires_at: Date.utc_today(), uses: 4} - assert UserInviteToken.valid_invite?(invite) - end - - test "overdue date and less uses returns false", %{invite: invite} do - invite = %{invite | expires_at: Date.add(Date.utc_today(), -1)} - refute UserInviteToken.valid_invite?(invite) - end - - test "not overdue date with more uses returns false", %{invite: invite} do - invite = %{invite | expires_at: Date.utc_today(), uses: 5} - refute UserInviteToken.valid_invite?(invite) - end - - test "overdue date with more uses returns false", %{invite: invite} do - invite = %{invite | expires_at: Date.add(Date.utc_today(), -1), uses: 5} - refute UserInviteToken.valid_invite?(invite) - end - end -end diff --git a/test/user_relationship_test.exs b/test/user_relationship_test.exs @@ -1,130 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.UserRelationshipTest do - alias Pleroma.UserRelationship - - use Pleroma.DataCase - - import Pleroma.Factory - - describe "*_exists?/2" do - setup do - {:ok, users: insert_list(2, :user)} - end - - test "returns false if record doesn't exist", %{users: [user1, user2]} do - refute UserRelationship.block_exists?(user1, user2) - refute UserRelationship.mute_exists?(user1, user2) - refute UserRelationship.notification_mute_exists?(user1, user2) - refute UserRelationship.reblog_mute_exists?(user1, user2) - refute UserRelationship.inverse_subscription_exists?(user1, user2) - end - - test "returns true if record exists", %{users: [user1, user2]} do - for relationship_type <- [ - :block, - :mute, - :notification_mute, - :reblog_mute, - :inverse_subscription - ] do - insert(:user_relationship, - source: user1, - target: user2, - relationship_type: relationship_type - ) - end - - assert UserRelationship.block_exists?(user1, user2) - assert UserRelationship.mute_exists?(user1, user2) - assert UserRelationship.notification_mute_exists?(user1, user2) - assert UserRelationship.reblog_mute_exists?(user1, user2) - assert UserRelationship.inverse_subscription_exists?(user1, user2) - end - end - - describe "create_*/2" do - setup do - {:ok, users: insert_list(2, :user)} - end - - test "creates user relationship record if it doesn't exist", %{users: [user1, user2]} do - for relationship_type <- [ - :block, - :mute, - :notification_mute, - :reblog_mute, - :inverse_subscription - ] do - insert(:user_relationship, - source: user1, - target: user2, - relationship_type: relationship_type - ) - end - - UserRelationship.create_block(user1, user2) - UserRelationship.create_mute(user1, user2) - UserRelationship.create_notification_mute(user1, user2) - UserRelationship.create_reblog_mute(user1, user2) - UserRelationship.create_inverse_subscription(user1, user2) - - assert UserRelationship.block_exists?(user1, user2) - assert UserRelationship.mute_exists?(user1, user2) - assert UserRelationship.notification_mute_exists?(user1, user2) - assert UserRelationship.reblog_mute_exists?(user1, user2) - assert UserRelationship.inverse_subscription_exists?(user1, user2) - end - - test "if record already exists, returns it", %{users: [user1, user2]} do - user_block = UserRelationship.create_block(user1, user2) - assert user_block == UserRelationship.create_block(user1, user2) - end - end - - describe "delete_*/2" do - setup do - {:ok, users: insert_list(2, :user)} - end - - test "deletes user relationship record if it exists", %{users: [user1, user2]} do - for relationship_type <- [ - :block, - :mute, - :notification_mute, - :reblog_mute, - :inverse_subscription - ] do - insert(:user_relationship, - source: user1, - target: user2, - relationship_type: relationship_type - ) - end - - assert {:ok, %UserRelationship{}} = UserRelationship.delete_block(user1, user2) - assert {:ok, %UserRelationship{}} = UserRelationship.delete_mute(user1, user2) - assert {:ok, %UserRelationship{}} = UserRelationship.delete_notification_mute(user1, user2) - assert {:ok, %UserRelationship{}} = UserRelationship.delete_reblog_mute(user1, user2) - - assert {:ok, %UserRelationship{}} = - UserRelationship.delete_inverse_subscription(user1, user2) - - refute UserRelationship.block_exists?(user1, user2) - refute UserRelationship.mute_exists?(user1, user2) - refute UserRelationship.notification_mute_exists?(user1, user2) - refute UserRelationship.reblog_mute_exists?(user1, user2) - refute UserRelationship.inverse_subscription_exists?(user1, user2) - end - - test "if record does not exist, returns {:ok, nil}", %{users: [user1, user2]} do - assert {:ok, nil} = UserRelationship.delete_block(user1, user2) - assert {:ok, nil} = UserRelationship.delete_mute(user1, user2) - assert {:ok, nil} = UserRelationship.delete_notification_mute(user1, user2) - assert {:ok, nil} = UserRelationship.delete_reblog_mute(user1, user2) - assert {:ok, nil} = UserRelationship.delete_inverse_subscription(user1, user2) - end - end -end diff --git a/test/user_search_test.exs b/test/user_search_test.exs @@ -1,362 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.UserSearchTest do - alias Pleroma.Repo - alias Pleroma.User - use Pleroma.DataCase - - import Pleroma.Factory - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe "User.search" do - setup do: clear_config([:instance, :limit_to_local_content]) - - test "returns a resolved user as the first result" do - Pleroma.Config.put([:instance, :limit_to_local_content], false) - user = insert(:user, %{nickname: "no_relation", ap_id: "https://lain.com/users/lain"}) - _user = insert(:user, %{nickname: "com_user"}) - - [first_user, _second_user] = User.search("https://lain.com/users/lain", resolve: true) - - assert first_user.id == user.id - end - - test "returns a user with matching ap_id as the first result" do - user = insert(:user, %{nickname: "no_relation", ap_id: "https://lain.com/users/lain"}) - _user = insert(:user, %{nickname: "com_user"}) - - [first_user, _second_user] = User.search("https://lain.com/users/lain") - - assert first_user.id == user.id - end - - test "doesn't die if two users have the same uri" do - insert(:user, %{uri: "https://gensokyo.2hu/@raymoo"}) - insert(:user, %{uri: "https://gensokyo.2hu/@raymoo"}) - assert [_first_user, _second_user] = User.search("https://gensokyo.2hu/@raymoo") - end - - test "returns a user with matching uri as the first result" do - user = - insert(:user, %{ - nickname: "no_relation", - ap_id: "https://lain.com/users/lain", - uri: "https://lain.com/@lain" - }) - - _user = insert(:user, %{nickname: "com_user"}) - - [first_user, _second_user] = User.search("https://lain.com/@lain") - - assert first_user.id == user.id - end - - test "excludes invisible users from results" do - user = insert(:user, %{nickname: "john t1000"}) - insert(:user, %{invisible: true, nickname: "john t800"}) - - [found_user] = User.search("john") - assert found_user.id == user.id - end - - test "excludes users when discoverable is false" do - insert(:user, %{nickname: "john 3000", discoverable: false}) - insert(:user, %{nickname: "john 3001"}) - - users = User.search("john") - assert Enum.count(users) == 1 - end - - test "excludes service actors from results" do - insert(:user, actor_type: "Application", nickname: "user1") - service = insert(:user, actor_type: "Service", nickname: "user2") - person = insert(:user, actor_type: "Person", nickname: "user3") - - assert [found_user1, found_user2] = User.search("user") - assert [found_user1.id, found_user2.id] -- [service.id, person.id] == [] - end - - test "accepts limit parameter" do - Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"})) - assert length(User.search("john", limit: 3)) == 3 - assert length(User.search("john")) == 5 - end - - test "accepts offset parameter" do - Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"})) - assert length(User.search("john", limit: 3)) == 3 - assert length(User.search("john", limit: 3, offset: 3)) == 2 - end - - defp clear_virtual_fields(user) do - Map.merge(user, %{search_rank: nil, search_type: nil}) - end - - test "finds a user by full nickname or its leading fragment" do - user = insert(:user, %{nickname: "john"}) - - Enum.each(["john", "jo", "j"], fn query -> - assert user == - User.search(query) - |> List.first() - |> clear_virtual_fields() - end) - end - - test "finds a user by full name or leading fragment(s) of its words" do - user = insert(:user, %{name: "John Doe"}) - - Enum.each(["John Doe", "JOHN", "doe", "j d", "j", "d"], fn query -> - assert user == - User.search(query) - |> List.first() - |> clear_virtual_fields() - end) - end - - test "matches by leading fragment of user domain" do - user = insert(:user, %{nickname: "arandom@dude.com"}) - insert(:user, %{nickname: "iamthedude"}) - - assert [user.id] == User.search("dud") |> Enum.map(& &1.id) - end - - test "ranks full nickname match higher than full name match" do - nicknamed_user = insert(:user, %{nickname: "hj@shigusegubu.club"}) - named_user = insert(:user, %{nickname: "xyz@sample.com", name: "HJ"}) - - results = User.search("hj") - - assert [nicknamed_user.id, named_user.id] == Enum.map(results, & &1.id) - assert Enum.at(results, 0).search_rank > Enum.at(results, 1).search_rank - end - - test "finds users, considering density of matched tokens" do - u1 = insert(:user, %{name: "Bar Bar plus Word Word"}) - u2 = insert(:user, %{name: "Word Word Bar Bar Bar"}) - - assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id) - end - - test "finds users, boosting ranks of friends and followers" do - u1 = insert(:user) - u2 = insert(:user, %{name: "Doe"}) - follower = insert(:user, %{name: "Doe"}) - friend = insert(:user, %{name: "Doe"}) - - {:ok, follower} = User.follow(follower, u1) - {:ok, u1} = User.follow(u1, friend) - - assert [friend.id, follower.id, u2.id] -- - Enum.map(User.search("doe", resolve: false, for_user: u1), & &1.id) == [] - end - - test "finds followings of user by partial name" do - lizz = insert(:user, %{name: "Lizz"}) - jimi = insert(:user, %{name: "Jimi"}) - following_lizz = insert(:user, %{name: "Jimi Hendrix"}) - following_jimi = insert(:user, %{name: "Lizz Wright"}) - follower_lizz = insert(:user, %{name: "Jimi"}) - - {:ok, lizz} = User.follow(lizz, following_lizz) - {:ok, _jimi} = User.follow(jimi, following_jimi) - {:ok, _follower_lizz} = User.follow(follower_lizz, lizz) - - assert Enum.map(User.search("jimi", following: true, for_user: lizz), & &1.id) == [ - following_lizz.id - ] - - assert User.search("lizz", following: true, for_user: lizz) == [] - end - - test "find local and remote users for authenticated users" do - u1 = insert(:user, %{name: "lain"}) - u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false}) - u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false}) - - results = - "lain" - |> User.search(for_user: u1) - |> Enum.map(& &1.id) - |> Enum.sort() - - assert [u1.id, u2.id, u3.id] == results - end - - test "find only local users for unauthenticated users" do - %{id: id} = insert(:user, %{name: "lain"}) - insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false}) - insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false}) - - assert [%{id: ^id}] = User.search("lain") - end - - test "find only local users for authenticated users when `limit_to_local_content` is `:all`" do - Pleroma.Config.put([:instance, :limit_to_local_content], :all) - - %{id: id} = insert(:user, %{name: "lain"}) - insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false}) - insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false}) - - assert [%{id: ^id}] = User.search("lain") - end - - test "find all users for unauthenticated users when `limit_to_local_content` is `false`" do - Pleroma.Config.put([:instance, :limit_to_local_content], false) - - u1 = insert(:user, %{name: "lain"}) - u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false}) - u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false}) - - results = - "lain" - |> User.search() - |> Enum.map(& &1.id) - |> Enum.sort() - - assert [u1.id, u2.id, u3.id] == results - end - - test "does not yield false-positive matches" do - insert(:user, %{name: "John Doe"}) - - Enum.each(["mary", "a", ""], fn query -> - assert [] == User.search(query) - end) - end - - test "works with URIs" do - user = insert(:user) - - results = - User.search("http://mastodon.example.org/users/admin", resolve: true, for_user: user) - - result = results |> List.first() - - user = User.get_cached_by_ap_id("http://mastodon.example.org/users/admin") - - assert length(results) == 1 - - expected = - result - |> Map.put(:search_rank, nil) - |> Map.put(:search_type, nil) - |> Map.put(:last_digest_emailed_at, nil) - |> Map.put(:multi_factor_authentication_settings, nil) - |> Map.put(:notification_settings, nil) - - assert user == expected - end - - test "excludes a blocked users from search result" do - user = insert(:user, %{nickname: "Bill"}) - - [blocked_user | users] = Enum.map(0..3, &insert(:user, %{nickname: "john#{&1}"})) - - blocked_user2 = - insert( - :user, - %{nickname: "john awful", ap_id: "https://awful-and-rude-instance.com/user/bully"} - ) - - User.block_domain(user, "awful-and-rude-instance.com") - User.block(user, blocked_user) - - account_ids = User.search("john", for_user: refresh_record(user)) |> collect_ids - - assert account_ids == collect_ids(users) - refute Enum.member?(account_ids, blocked_user.id) - refute Enum.member?(account_ids, blocked_user2.id) - assert length(account_ids) == 3 - end - - test "local user has the same search_rank as for users with the same nickname, but another domain" do - user = insert(:user) - insert(:user, nickname: "lain@mastodon.social") - insert(:user, nickname: "lain") - insert(:user, nickname: "lain@pleroma.social") - - assert User.search("lain@localhost", resolve: true, for_user: user) - |> Enum.each(fn u -> u.search_rank == 0.5 end) - end - - test "localhost is the part of the domain" do - user = insert(:user) - insert(:user, nickname: "another@somedomain") - insert(:user, nickname: "lain") - insert(:user, nickname: "lain@examplelocalhost") - - result = User.search("lain@examplelocalhost", resolve: true, for_user: user) - assert Enum.each(result, fn u -> u.search_rank == 0.5 end) - assert length(result) == 2 - end - - test "local user search with users" do - user = insert(:user) - local_user = insert(:user, nickname: "lain") - insert(:user, nickname: "another@localhost.com") - insert(:user, nickname: "localhost@localhost.com") - - [result] = User.search("lain@localhost", resolve: true, for_user: user) - assert Map.put(result, :search_rank, nil) |> Map.put(:search_type, nil) == local_user - end - - test "works with idna domains" do - user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな"))) - - results = User.search("lain@zetsubou.みんな", resolve: false, for_user: user) - - result = List.first(results) - - assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) - end - - test "works with idna domains converted input" do - user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな"))) - - results = - User.search("lain@zetsubou." <> to_string(:idna.encode("zetsubou.みんな")), - resolve: false, - for_user: user - ) - - result = List.first(results) - - assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) - end - - test "works with idna domains and bad chars in domain" do - user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな"))) - - results = - User.search("lain@zetsubou!@#$%^&*()+,-/:;<=>?[]'_{}|~`.みんな", - resolve: false, - for_user: user - ) - - result = List.first(results) - - assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) - end - - test "works with idna domains and query as link" do - user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな"))) - - results = - User.search("https://zetsubou.みんな/users/lain", - resolve: false, - for_user: user - ) - - result = List.first(results) - - assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) - end - end -end diff --git a/test/user_test.exs b/test/user_test.exs @@ -1,2124 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.UserTest do - alias Pleroma.Activity - alias Pleroma.Builders.UserBuilder - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.CommonAPI - - use Pleroma.DataCase - use Oban.Testing, repo: Pleroma.Repo - - import Pleroma.Factory - import ExUnit.CaptureLog - import Swoosh.TestAssertions - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup do: clear_config([:instance, :account_activation_required]) - - describe "service actors" do - test "returns updated invisible actor" do - uri = "#{Pleroma.Web.Endpoint.url()}/relay" - followers_uri = "#{uri}/followers" - - insert( - :user, - %{ - nickname: "relay", - invisible: false, - local: true, - ap_id: uri, - follower_address: followers_uri - } - ) - - actor = User.get_or_create_service_actor_by_ap_id(uri, "relay") - assert actor.invisible - end - - test "returns relay user" do - uri = "#{Pleroma.Web.Endpoint.url()}/relay" - followers_uri = "#{uri}/followers" - - assert %User{ - nickname: "relay", - invisible: true, - local: true, - ap_id: ^uri, - follower_address: ^followers_uri - } = User.get_or_create_service_actor_by_ap_id(uri, "relay") - - assert capture_log(fn -> - refute User.get_or_create_service_actor_by_ap_id("/relay", "relay") - end) =~ "Cannot create service actor:" - end - - test "returns invisible actor" do - uri = "#{Pleroma.Web.Endpoint.url()}/internal/fetch-test" - followers_uri = "#{uri}/followers" - user = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test") - - assert %User{ - nickname: "internal.fetch-test", - invisible: true, - local: true, - ap_id: ^uri, - follower_address: ^followers_uri - } = user - - user2 = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test") - assert user.id == user2.id - end - end - - describe "AP ID user relationships" do - setup do - {:ok, user: insert(:user)} - end - - test "outgoing_relationships_ap_ids/1", %{user: user} do - rel_types = [:block, :mute, :notification_mute, :reblog_mute, :inverse_subscription] - - ap_ids_by_rel = - Enum.into( - rel_types, - %{}, - fn rel_type -> - rel_records = - insert_list(2, :user_relationship, %{source: user, relationship_type: rel_type}) - - ap_ids = Enum.map(rel_records, fn rr -> Repo.preload(rr, :target).target.ap_id end) - {rel_type, Enum.sort(ap_ids)} - end - ) - - assert ap_ids_by_rel[:block] == Enum.sort(User.blocked_users_ap_ids(user)) - assert ap_ids_by_rel[:block] == Enum.sort(Enum.map(User.blocked_users(user), & &1.ap_id)) - - assert ap_ids_by_rel[:mute] == Enum.sort(User.muted_users_ap_ids(user)) - assert ap_ids_by_rel[:mute] == Enum.sort(Enum.map(User.muted_users(user), & &1.ap_id)) - - assert ap_ids_by_rel[:notification_mute] == - Enum.sort(User.notification_muted_users_ap_ids(user)) - - assert ap_ids_by_rel[:notification_mute] == - Enum.sort(Enum.map(User.notification_muted_users(user), & &1.ap_id)) - - assert ap_ids_by_rel[:reblog_mute] == Enum.sort(User.reblog_muted_users_ap_ids(user)) - - assert ap_ids_by_rel[:reblog_mute] == - Enum.sort(Enum.map(User.reblog_muted_users(user), & &1.ap_id)) - - assert ap_ids_by_rel[:inverse_subscription] == Enum.sort(User.subscriber_users_ap_ids(user)) - - assert ap_ids_by_rel[:inverse_subscription] == - Enum.sort(Enum.map(User.subscriber_users(user), & &1.ap_id)) - - outgoing_relationships_ap_ids = User.outgoing_relationships_ap_ids(user, rel_types) - - assert ap_ids_by_rel == - Enum.into(outgoing_relationships_ap_ids, %{}, fn {k, v} -> {k, Enum.sort(v)} end) - end - end - - describe "when tags are nil" do - test "tagging a user" do - user = insert(:user, %{tags: nil}) - user = User.tag(user, ["cool", "dude"]) - - assert "cool" in user.tags - assert "dude" in user.tags - end - - test "untagging a user" do - user = insert(:user, %{tags: nil}) - user = User.untag(user, ["cool", "dude"]) - - assert user.tags == [] - end - end - - test "ap_id returns the activity pub id for the user" do - user = UserBuilder.build() - - expected_ap_id = "#{Pleroma.Web.base_url()}/users/#{user.nickname}" - - assert expected_ap_id == User.ap_id(user) - end - - test "ap_followers returns the followers collection for the user" do - user = UserBuilder.build() - - expected_followers_collection = "#{User.ap_id(user)}/followers" - - assert expected_followers_collection == User.ap_followers(user) - end - - test "ap_following returns the following collection for the user" do - user = UserBuilder.build() - - expected_followers_collection = "#{User.ap_id(user)}/following" - - assert expected_followers_collection == User.ap_following(user) - end - - test "returns all pending follow requests" do - unlocked = insert(:user) - locked = insert(:user, locked: true) - follower = insert(:user) - - CommonAPI.follow(follower, unlocked) - CommonAPI.follow(follower, locked) - - assert [] = User.get_follow_requests(unlocked) - assert [activity] = User.get_follow_requests(locked) - - assert activity - end - - test "doesn't return already accepted or duplicate follow requests" do - locked = insert(:user, locked: true) - pending_follower = insert(:user) - accepted_follower = insert(:user) - - CommonAPI.follow(pending_follower, locked) - CommonAPI.follow(pending_follower, locked) - CommonAPI.follow(accepted_follower, locked) - - Pleroma.FollowingRelationship.update(accepted_follower, locked, :follow_accept) - - assert [^pending_follower] = User.get_follow_requests(locked) - end - - test "doesn't return follow requests for deactivated accounts" do - locked = insert(:user, locked: true) - pending_follower = insert(:user, %{deactivated: true}) - - CommonAPI.follow(pending_follower, locked) - - assert true == pending_follower.deactivated - assert [] = User.get_follow_requests(locked) - end - - test "clears follow requests when requester is blocked" do - followed = insert(:user, locked: true) - follower = insert(:user) - - CommonAPI.follow(follower, followed) - assert [_activity] = User.get_follow_requests(followed) - - {:ok, _user_relationship} = User.block(followed, follower) - assert [] = User.get_follow_requests(followed) - end - - test "follow_all follows mutliple users" do - user = insert(:user) - followed_zero = insert(:user) - followed_one = insert(:user) - followed_two = insert(:user) - blocked = insert(:user) - not_followed = insert(:user) - reverse_blocked = insert(:user) - - {:ok, _user_relationship} = User.block(user, blocked) - {:ok, _user_relationship} = User.block(reverse_blocked, user) - - {:ok, user} = User.follow(user, followed_zero) - - {:ok, user} = User.follow_all(user, [followed_one, followed_two, blocked, reverse_blocked]) - - assert User.following?(user, followed_one) - assert User.following?(user, followed_two) - assert User.following?(user, followed_zero) - refute User.following?(user, not_followed) - refute User.following?(user, blocked) - refute User.following?(user, reverse_blocked) - end - - test "follow_all follows mutliple users without duplicating" do - user = insert(:user) - followed_zero = insert(:user) - followed_one = insert(:user) - followed_two = insert(:user) - - {:ok, user} = User.follow_all(user, [followed_zero, followed_one]) - assert length(User.following(user)) == 3 - - {:ok, user} = User.follow_all(user, [followed_one, followed_two]) - assert length(User.following(user)) == 4 - end - - test "follow takes a user and another user" do - user = insert(:user) - followed = insert(:user) - - {:ok, user} = User.follow(user, followed) - - user = User.get_cached_by_id(user.id) - followed = User.get_cached_by_ap_id(followed.ap_id) - - assert followed.follower_count == 1 - assert user.following_count == 1 - - assert User.ap_followers(followed) in User.following(user) - end - - test "can't follow a deactivated users" do - user = insert(:user) - followed = insert(:user, %{deactivated: true}) - - {:error, _} = User.follow(user, followed) - end - - test "can't follow a user who blocked us" do - blocker = insert(:user) - blockee = insert(:user) - - {:ok, _user_relationship} = User.block(blocker, blockee) - - {:error, _} = User.follow(blockee, blocker) - end - - test "can't subscribe to a user who blocked us" do - blocker = insert(:user) - blocked = insert(:user) - - {:ok, _user_relationship} = User.block(blocker, blocked) - - {:error, _} = User.subscribe(blocked, blocker) - end - - test "local users do not automatically follow local locked accounts" do - follower = insert(:user, locked: true) - followed = insert(:user, locked: true) - - {:ok, follower} = User.maybe_direct_follow(follower, followed) - - refute User.following?(follower, followed) - end - - describe "unfollow/2" do - setup do: clear_config([:instance, :external_user_synchronization]) - - test "unfollow with syncronizes external user" do - Pleroma.Config.put([:instance, :external_user_synchronization], true) - - followed = - insert(:user, - nickname: "fuser1", - follower_address: "http://localhost:4001/users/fuser1/followers", - following_address: "http://localhost:4001/users/fuser1/following", - ap_id: "http://localhost:4001/users/fuser1" - ) - - user = - insert(:user, %{ - local: false, - nickname: "fuser2", - ap_id: "http://localhost:4001/users/fuser2", - follower_address: "http://localhost:4001/users/fuser2/followers", - following_address: "http://localhost:4001/users/fuser2/following" - }) - - {:ok, user} = User.follow(user, followed, :follow_accept) - - {:ok, user, _activity} = User.unfollow(user, followed) - - user = User.get_cached_by_id(user.id) - - assert User.following(user) == [] - end - - test "unfollow takes a user and another user" do - followed = insert(:user) - user = insert(:user) - - {:ok, user} = User.follow(user, followed, :follow_accept) - - assert User.following(user) == [user.follower_address, followed.follower_address] - - {:ok, user, _activity} = User.unfollow(user, followed) - - assert User.following(user) == [user.follower_address] - end - - test "unfollow doesn't unfollow yourself" do - user = insert(:user) - - {:error, _} = User.unfollow(user, user) - - assert User.following(user) == [user.follower_address] - end - end - - test "test if a user is following another user" do - followed = insert(:user) - user = insert(:user) - User.follow(user, followed, :follow_accept) - - assert User.following?(user, followed) - refute User.following?(followed, user) - end - - test "fetches correct profile for nickname beginning with number" do - # Use old-style integer ID to try to reproduce the problem - user = insert(:user, %{id: 1080}) - user_with_numbers = insert(:user, %{nickname: "#{user.id}garbage"}) - assert user_with_numbers == User.get_cached_by_nickname_or_id(user_with_numbers.nickname) - end - - describe "user registration" do - @full_user_data %{ - bio: "A guy", - name: "my name", - nickname: "nick", - password: "test", - password_confirmation: "test", - email: "email@example.com" - } - - setup do: clear_config([:instance, :autofollowed_nicknames]) - setup do: clear_config([:welcome]) - setup do: clear_config([:instance, :account_activation_required]) - - test "it autofollows accounts that are set for it" do - user = insert(:user) - remote_user = insert(:user, %{local: false}) - - Pleroma.Config.put([:instance, :autofollowed_nicknames], [ - user.nickname, - remote_user.nickname - ]) - - cng = User.register_changeset(%User{}, @full_user_data) - - {:ok, registered_user} = User.register(cng) - - assert User.following?(registered_user, user) - refute User.following?(registered_user, remote_user) - end - - test "it sends a welcome message if it is set" do - welcome_user = insert(:user) - Pleroma.Config.put([:welcome, :direct_message, :enabled], true) - Pleroma.Config.put([:welcome, :direct_message, :sender_nickname], welcome_user.nickname) - Pleroma.Config.put([:welcome, :direct_message, :message], "Hello, this is a direct message") - - cng = User.register_changeset(%User{}, @full_user_data) - {:ok, registered_user} = User.register(cng) - ObanHelpers.perform_all() - - activity = Repo.one(Pleroma.Activity) - assert registered_user.ap_id in activity.recipients - assert Object.normalize(activity).data["content"] =~ "direct message" - assert activity.actor == welcome_user.ap_id - end - - test "it sends a welcome chat message if it is set" do - welcome_user = insert(:user) - Pleroma.Config.put([:welcome, :chat_message, :enabled], true) - Pleroma.Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname) - Pleroma.Config.put([:welcome, :chat_message, :message], "Hello, this is a chat message") - - cng = User.register_changeset(%User{}, @full_user_data) - {:ok, registered_user} = User.register(cng) - ObanHelpers.perform_all() - - activity = Repo.one(Pleroma.Activity) - assert registered_user.ap_id in activity.recipients - assert Object.normalize(activity).data["content"] =~ "chat message" - assert activity.actor == welcome_user.ap_id - end - - setup do: - clear_config(:mrf_simple, - media_removal: [], - media_nsfw: [], - federated_timeline_removal: [], - report_removal: [], - reject: [], - followers_only: [], - accept: [], - avatar_removal: [], - banner_removal: [], - reject_deletes: [] - ) - - 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 - Pleroma.Config.put([:mrf_simple, :media_nsfw], ["localhost"]) - - welcome_user = insert(:user) - Pleroma.Config.put([:welcome, :chat_message, :enabled], true) - Pleroma.Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname) - Pleroma.Config.put([:welcome, :chat_message, :message], "Hello, this is a chat message") - - cng = User.register_changeset(%User{}, @full_user_data) - {:ok, registered_user} = User.register(cng) - ObanHelpers.perform_all() - - activity = Repo.one(Pleroma.Activity) - assert registered_user.ap_id in activity.recipients - assert Object.normalize(activity).data["content"] =~ "chat message" - assert activity.actor == welcome_user.ap_id - end - - test "it sends a welcome email message if it is set" do - welcome_user = insert(:user) - Pleroma.Config.put([:welcome, :email, :enabled], true) - Pleroma.Config.put([:welcome, :email, :sender], welcome_user.email) - - Pleroma.Config.put( - [:welcome, :email, :subject], - "Hello, welcome to cool site: <%= instance_name %>" - ) - - instance_name = Pleroma.Config.get([:instance, :name]) - - cng = User.register_changeset(%User{}, @full_user_data) - {:ok, registered_user} = User.register(cng) - ObanHelpers.perform_all() - - assert_email_sent( - from: {instance_name, welcome_user.email}, - to: {registered_user.name, registered_user.email}, - subject: "Hello, welcome to cool site: #{instance_name}", - html_body: "Welcome to #{instance_name}" - ) - end - - test "it sends a confirm email" do - Pleroma.Config.put([:instance, :account_activation_required], true) - - cng = User.register_changeset(%User{}, @full_user_data) - {:ok, registered_user} = User.register(cng) - ObanHelpers.perform_all() - - Pleroma.Emails.UserEmail.account_confirmation_email(registered_user) - # temporary hackney fix until hackney max_connections bug is fixed - # https://git.pleroma.social/pleroma/pleroma/-/issues/2101 - |> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]]) - |> assert_email_sent() - end - - test "it requires an email, name, nickname and password, bio is optional when account_activation_required is enabled" do - Pleroma.Config.put([:instance, :account_activation_required], true) - - @full_user_data - |> Map.keys() - |> Enum.each(fn key -> - params = Map.delete(@full_user_data, key) - changeset = User.register_changeset(%User{}, params) - - assert if key == :bio, do: changeset.valid?, else: not changeset.valid? - end) - end - - test "it requires an name, nickname and password, bio and email are optional when account_activation_required is disabled" do - Pleroma.Config.put([:instance, :account_activation_required], false) - - @full_user_data - |> Map.keys() - |> Enum.each(fn key -> - params = Map.delete(@full_user_data, key) - changeset = User.register_changeset(%User{}, params) - - assert if key in [:bio, :email], do: changeset.valid?, else: not changeset.valid? - end) - end - - test "it restricts certain nicknames" do - [restricted_name | _] = Pleroma.Config.get([User, :restricted_nicknames]) - - assert is_bitstring(restricted_name) - - params = - @full_user_data - |> Map.put(:nickname, restricted_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"]) - - # Block with 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) - refute changeset.valid? - - # Pass with different domains that are similar - params = Map.put(@full_user_data, :email, "troll@gnomestrolling.world") - changeset = User.register_changeset(%User{}, params) - assert changeset.valid? - - params = Map.put(@full_user_data, :email, "troll@trolling.world.us") - changeset = User.register_changeset(%User{}, params) - assert changeset.valid? - end - - test "it sets the password_hash and ap_id" do - changeset = User.register_changeset(%User{}, @full_user_data) - - assert changeset.valid? - - assert is_binary(changeset.changes[:password_hash]) - 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 - - test "it sets the 'accepts_chat_messages' set to true" do - changeset = User.register_changeset(%User{}, @full_user_data) - assert changeset.valid? - - {:ok, user} = Repo.insert(changeset) - - assert user.accepts_chat_messages - end - - test "it creates a confirmed user" do - changeset = User.register_changeset(%User{}, @full_user_data) - assert changeset.valid? - - {:ok, user} = Repo.insert(changeset) - - refute user.confirmation_pending - end - end - - describe "user registration, with :account_activation_required" do - @full_user_data %{ - bio: "A guy", - name: "my name", - nickname: "nick", - password: "test", - password_confirmation: "test", - email: "email@example.com" - } - setup do: clear_config([:instance, :account_activation_required], true) - - test "it creates unconfirmed user" do - changeset = User.register_changeset(%User{}, @full_user_data) - assert changeset.valid? - - {:ok, user} = Repo.insert(changeset) - - assert user.confirmation_pending - assert user.confirmation_token - end - - test "it creates confirmed user if :confirmed option is given" do - changeset = User.register_changeset(%User{}, @full_user_data, need_confirmation: false) - assert changeset.valid? - - {:ok, user} = Repo.insert(changeset) - - refute user.confirmation_pending - refute user.confirmation_token - end - end - - describe "user registration, with :account_approval_required" do - @full_user_data %{ - bio: "A guy", - name: "my name", - nickname: "nick", - password: "test", - password_confirmation: "test", - email: "email@example.com", - registration_reason: "I'm a cool guy :)" - } - setup do: clear_config([:instance, :account_approval_required], true) - - test "it creates unapproved user" do - changeset = User.register_changeset(%User{}, @full_user_data) - assert changeset.valid? - - {:ok, user} = Repo.insert(changeset) - - assert user.approval_pending - assert user.registration_reason == "I'm a cool guy :)" - end - - test "it restricts length of registration reason" do - reason_limit = Pleroma.Config.get([:instance, :registration_reason_length]) - - assert is_integer(reason_limit) - - params = - @full_user_data - |> Map.put( - :registration_reason, - "Quia et nesciunt dolores numquam ipsam nisi sapiente soluta. Ullam repudiandae nisi quam porro officiis officiis ad. Consequatur animi velit ex quia. Odit voluptatem perferendis quia ut nisi. Dignissimos sit soluta atque aliquid dolorem ut dolorum ut. Labore voluptates iste iusto amet voluptatum earum. Ad fugit illum nam eos ut nemo. Pariatur ea fuga non aspernatur. Dignissimos debitis officia corporis est nisi ab et. Atque itaque alias eius voluptas minus. Accusamus numquam tempore occaecati in." - ) - - changeset = User.register_changeset(%User{}, params) - - refute changeset.valid? - end - end - - describe "get_or_fetch/1" do - test "gets an existing user by nickname" do - user = insert(:user) - {:ok, fetched_user} = User.get_or_fetch(user.nickname) - - assert user == fetched_user - end - - test "gets an existing user by ap_id" do - ap_id = "http://mastodon.example.org/users/admin" - - user = - insert( - :user, - local: false, - nickname: "admin@mastodon.example.org", - ap_id: ap_id - ) - - {:ok, fetched_user} = User.get_or_fetch(ap_id) - freshed_user = refresh_record(user) - assert freshed_user == fetched_user - end - end - - describe "fetching a user from nickname or trying to build one" do - test "gets an existing user" do - user = insert(:user) - {:ok, fetched_user} = User.get_or_fetch_by_nickname(user.nickname) - - assert user == fetched_user - end - - test "gets an existing user, case insensitive" do - user = insert(:user, nickname: "nick") - {:ok, fetched_user} = User.get_or_fetch_by_nickname("NICK") - - assert user == fetched_user - end - - test "gets an existing user by fully qualified nickname" do - user = insert(:user) - - {:ok, fetched_user} = - User.get_or_fetch_by_nickname(user.nickname <> "@" <> Pleroma.Web.Endpoint.host()) - - assert user == fetched_user - end - - test "gets an existing user by fully qualified nickname, case insensitive" do - user = insert(:user, nickname: "nick") - casing_altered_fqn = String.upcase(user.nickname <> "@" <> Pleroma.Web.Endpoint.host()) - - {:ok, fetched_user} = User.get_or_fetch_by_nickname(casing_altered_fqn) - - assert user == fetched_user - end - - @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" - 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" - end - - test "updates an existing user, if stale" do - a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) - - orig_user = - insert( - :user, - local: false, - nickname: "admin@mastodon.example.org", - ap_id: "http://mastodon.example.org/users/admin", - last_refreshed_at: a_week_ago - ) - - assert orig_user.last_refreshed_at == a_week_ago - - {:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin") - - assert user.inbox - - refute user.last_refreshed_at == orig_user.last_refreshed_at - end - - test "if nicknames clash, the old user gets a prefix with the old id to the nickname" do - a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) - - orig_user = - insert( - :user, - local: false, - nickname: "admin@mastodon.example.org", - ap_id: "http://mastodon.example.org/users/harinezumigari", - last_refreshed_at: a_week_ago - ) - - assert orig_user.last_refreshed_at == a_week_ago - - {:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin") - - assert user.inbox - - refute user.id == orig_user.id - - orig_user = User.get_by_id(orig_user.id) - - assert orig_user.nickname == "#{orig_user.id}.admin@mastodon.example.org" - end - - @tag capture_log: true - test "it returns the old user if stale, but unfetchable" do - a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) - - orig_user = - insert( - :user, - local: false, - nickname: "admin@mastodon.example.org", - ap_id: "http://mastodon.example.org/users/raymoo", - last_refreshed_at: a_week_ago - ) - - assert orig_user.last_refreshed_at == a_week_ago - - {:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/raymoo") - - assert user.last_refreshed_at == orig_user.last_refreshed_at - end - end - - test "returns an ap_id for a user" do - user = insert(:user) - - assert User.ap_id(user) == - Pleroma.Web.Router.Helpers.user_feed_url( - Pleroma.Web.Endpoint, - :feed_redirect, - user.nickname - ) - end - - test "returns an ap_followers link for a user" do - user = insert(:user) - - assert User.ap_followers(user) == - Pleroma.Web.Router.Helpers.user_feed_url( - Pleroma.Web.Endpoint, - :feed_redirect, - user.nickname - ) <> "/followers" - end - - describe "remote user changeset" do - @valid_remote %{ - bio: "hello", - name: "Someone", - nickname: "a@b.de", - ap_id: "http...", - avatar: %{some: "avatar"} - } - setup do: clear_config([:instance, :user_bio_length]) - setup do: clear_config([:instance, :user_name_length]) - - test "it confirms validity" do - cs = User.remote_user_changeset(@valid_remote) - assert cs.valid? - end - - test "it sets the follower_adress" do - cs = User.remote_user_changeset(@valid_remote) - # remote users get a fake local follower address - assert cs.changes.follower_address == - User.ap_followers(%User{nickname: @valid_remote[:nickname]}) - end - - test "it enforces the fqn format for nicknames" do - cs = User.remote_user_changeset(%{@valid_remote | nickname: "bla"}) - assert Ecto.Changeset.get_field(cs, :local) == false - assert cs.changes.avatar - refute cs.valid? - end - - test "it has required fields" do - [:ap_id] - |> Enum.each(fn field -> - cs = User.remote_user_changeset(Map.delete(@valid_remote, field)) - refute cs.valid? - end) - end - end - - describe "followers and friends" do - test "gets all followers for a given user" do - user = insert(:user) - follower_one = insert(:user) - follower_two = insert(:user) - not_follower = insert(:user) - - {:ok, follower_one} = User.follow(follower_one, user) - {:ok, follower_two} = User.follow(follower_two, user) - - res = User.get_followers(user) - - assert Enum.member?(res, follower_one) - assert Enum.member?(res, follower_two) - refute Enum.member?(res, not_follower) - end - - test "gets all friends (followed users) for a given user" do - user = insert(:user) - followed_one = insert(:user) - followed_two = insert(:user) - not_followed = insert(:user) - - {:ok, user} = User.follow(user, followed_one) - {:ok, user} = User.follow(user, followed_two) - - res = User.get_friends(user) - - followed_one = User.get_cached_by_ap_id(followed_one.ap_id) - followed_two = User.get_cached_by_ap_id(followed_two.ap_id) - assert Enum.member?(res, followed_one) - assert Enum.member?(res, followed_two) - refute Enum.member?(res, not_followed) - end - end - - describe "updating note and follower count" do - test "it sets the note_count property" do - note = insert(:note) - - user = User.get_cached_by_ap_id(note.data["actor"]) - - assert user.note_count == 0 - - {:ok, user} = User.update_note_count(user) - - assert user.note_count == 1 - end - - test "it increases the note_count property" do - note = insert(:note) - user = User.get_cached_by_ap_id(note.data["actor"]) - - assert user.note_count == 0 - - {:ok, user} = User.increase_note_count(user) - - assert user.note_count == 1 - - {:ok, user} = User.increase_note_count(user) - - assert user.note_count == 2 - end - - test "it decreases the note_count property" do - note = insert(:note) - user = User.get_cached_by_ap_id(note.data["actor"]) - - assert user.note_count == 0 - - {:ok, user} = User.increase_note_count(user) - - assert user.note_count == 1 - - {:ok, user} = User.decrease_note_count(user) - - assert user.note_count == 0 - - {:ok, user} = User.decrease_note_count(user) - - assert user.note_count == 0 - end - - test "it sets the follower_count property" do - user = insert(:user) - follower = insert(:user) - - User.follow(follower, user) - - assert user.follower_count == 0 - - {:ok, user} = User.update_follower_count(user) - - assert user.follower_count == 1 - end - end - - describe "mutes" do - test "it mutes people" do - user = insert(:user) - muted_user = insert(:user) - - refute User.mutes?(user, muted_user) - refute User.muted_notifications?(user, muted_user) - - {:ok, _user_relationships} = User.mute(user, muted_user) - - assert User.mutes?(user, muted_user) - assert User.muted_notifications?(user, muted_user) - end - - test "it unmutes users" do - user = insert(:user) - muted_user = insert(:user) - - {:ok, _user_relationships} = User.mute(user, muted_user) - {:ok, _user_mute} = User.unmute(user, muted_user) - - refute User.mutes?(user, muted_user) - refute User.muted_notifications?(user, muted_user) - end - - test "it mutes user without notifications" do - user = insert(:user) - muted_user = insert(:user) - - refute User.mutes?(user, muted_user) - refute User.muted_notifications?(user, muted_user) - - {:ok, _user_relationships} = User.mute(user, muted_user, false) - - assert User.mutes?(user, muted_user) - refute User.muted_notifications?(user, muted_user) - end - end - - describe "blocks" do - test "it blocks people" do - user = insert(:user) - blocked_user = insert(:user) - - refute User.blocks?(user, blocked_user) - - {:ok, _user_relationship} = User.block(user, blocked_user) - - assert User.blocks?(user, blocked_user) - end - - test "it unblocks users" do - user = insert(:user) - blocked_user = insert(:user) - - {:ok, _user_relationship} = User.block(user, blocked_user) - {:ok, _user_block} = User.unblock(user, blocked_user) - - refute User.blocks?(user, blocked_user) - end - - test "blocks tear down cyclical follow relationships" do - blocker = insert(:user) - blocked = insert(:user) - - {:ok, blocker} = User.follow(blocker, blocked) - {:ok, blocked} = User.follow(blocked, blocker) - - assert User.following?(blocker, blocked) - assert User.following?(blocked, blocker) - - {:ok, _user_relationship} = User.block(blocker, blocked) - blocked = User.get_cached_by_id(blocked.id) - - assert User.blocks?(blocker, blocked) - - refute User.following?(blocker, blocked) - refute User.following?(blocked, blocker) - end - - test "blocks tear down blocker->blocked follow relationships" do - blocker = insert(:user) - blocked = insert(:user) - - {:ok, blocker} = User.follow(blocker, blocked) - - assert User.following?(blocker, blocked) - refute User.following?(blocked, blocker) - - {:ok, _user_relationship} = User.block(blocker, blocked) - blocked = User.get_cached_by_id(blocked.id) - - assert User.blocks?(blocker, blocked) - - refute User.following?(blocker, blocked) - refute User.following?(blocked, blocker) - end - - test "blocks tear down blocked->blocker follow relationships" do - blocker = insert(:user) - blocked = insert(:user) - - {:ok, blocked} = User.follow(blocked, blocker) - - refute User.following?(blocker, blocked) - assert User.following?(blocked, blocker) - - {:ok, _user_relationship} = User.block(blocker, blocked) - blocked = User.get_cached_by_id(blocked.id) - - assert User.blocks?(blocker, blocked) - - refute User.following?(blocker, blocked) - refute User.following?(blocked, blocker) - end - - test "blocks tear down blocked->blocker subscription relationships" do - blocker = insert(:user) - blocked = insert(:user) - - {:ok, _subscription} = User.subscribe(blocked, blocker) - - assert User.subscribed_to?(blocked, blocker) - refute User.subscribed_to?(blocker, blocked) - - {:ok, _user_relationship} = User.block(blocker, blocked) - - assert User.blocks?(blocker, blocked) - refute User.subscribed_to?(blocker, blocked) - refute User.subscribed_to?(blocked, blocker) - end - end - - describe "domain blocking" do - test "blocks domains" do - user = insert(:user) - collateral_user = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"}) - - {:ok, user} = User.block_domain(user, "awful-and-rude-instance.com") - - assert User.blocks?(user, collateral_user) - end - - test "does not block domain with same end" do - user = insert(:user) - - collateral_user = - insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"}) - - {:ok, user} = User.block_domain(user, "awful-and-rude-instance.com") - - refute User.blocks?(user, collateral_user) - end - - test "does not block domain with same end if wildcard added" do - user = insert(:user) - - collateral_user = - insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"}) - - {:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com") - - refute User.blocks?(user, collateral_user) - end - - test "blocks domain with wildcard for subdomain" do - user = insert(:user) - - user_from_subdomain = - insert(:user, %{ap_id: "https://subdomain.awful-and-rude-instance.com/user/bully"}) - - user_with_two_subdomains = - insert(:user, %{ - ap_id: "https://subdomain.second_subdomain.awful-and-rude-instance.com/user/bully" - }) - - user_domain = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"}) - - {:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com") - - assert User.blocks?(user, user_from_subdomain) - assert User.blocks?(user, user_with_two_subdomains) - assert User.blocks?(user, user_domain) - end - - test "unblocks domains" do - user = insert(:user) - collateral_user = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"}) - - {:ok, user} = User.block_domain(user, "awful-and-rude-instance.com") - {:ok, user} = User.unblock_domain(user, "awful-and-rude-instance.com") - - refute User.blocks?(user, collateral_user) - end - - test "follows take precedence over domain blocks" do - user = insert(:user) - good_eggo = insert(:user, %{ap_id: "https://meanies.social/user/cuteposter"}) - - {:ok, user} = User.block_domain(user, "meanies.social") - {:ok, user} = User.follow(user, good_eggo) - - refute User.blocks?(user, good_eggo) - end - end - - describe "get_recipients_from_activity" do - test "works for announces" do - actor = insert(:user) - user = insert(:user, local: true) - - {:ok, activity} = CommonAPI.post(actor, %{status: "hello"}) - {:ok, announce} = CommonAPI.repeat(activity.id, user) - - recipients = User.get_recipients_from_activity(announce) - - assert user in recipients - end - - test "get recipients" do - actor = insert(:user) - user = insert(:user, local: true) - user_two = insert(:user, local: false) - addressed = insert(:user, local: true) - addressed_remote = insert(:user, local: false) - - {:ok, activity} = - CommonAPI.post(actor, %{ - status: "hey @#{addressed.nickname} @#{addressed_remote.nickname}" - }) - - assert Enum.map([actor, addressed], & &1.ap_id) -- - Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == [] - - {:ok, user} = User.follow(user, actor) - {:ok, _user_two} = User.follow(user_two, actor) - recipients = User.get_recipients_from_activity(activity) - assert length(recipients) == 3 - assert user in recipients - assert addressed in recipients - end - - test "has following" do - actor = insert(:user) - user = insert(:user) - user_two = insert(:user) - addressed = insert(:user, local: true) - - {:ok, activity} = - CommonAPI.post(actor, %{ - status: "hey @#{addressed.nickname}" - }) - - assert Enum.map([actor, addressed], & &1.ap_id) -- - Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == [] - - {:ok, _actor} = User.follow(actor, user) - {:ok, _actor} = User.follow(actor, user_two) - recipients = User.get_recipients_from_activity(activity) - assert length(recipients) == 2 - assert addressed in recipients - end - end - - describe ".deactivate" do - test "can de-activate then re-activate a user" do - user = insert(:user) - assert false == user.deactivated - {:ok, user} = User.deactivate(user) - assert true == user.deactivated - {:ok, user} = User.deactivate(user, false) - assert false == user.deactivated - end - - test "hide a user from followers" do - user = insert(:user) - user2 = insert(:user) - - {:ok, user} = User.follow(user, user2) - {:ok, _user} = User.deactivate(user) - - user2 = User.get_cached_by_id(user2.id) - - assert user2.follower_count == 0 - assert [] = User.get_followers(user2) - end - - test "hide a user from friends" do - user = insert(:user) - user2 = insert(:user) - - {:ok, user2} = User.follow(user2, user) - assert user2.following_count == 1 - assert User.following_count(user2) == 1 - - {:ok, _user} = User.deactivate(user) - - user2 = User.get_cached_by_id(user2.id) - - assert refresh_record(user2).following_count == 0 - assert user2.following_count == 0 - assert User.following_count(user2) == 0 - assert [] = User.get_friends(user2) - end - - test "hide a user's statuses from timelines and notifications" do - user = insert(:user) - user2 = insert(:user) - - {:ok, user2} = User.follow(user2, user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{user2.nickname}"}) - - activity = Repo.preload(activity, :bookmark) - - [notification] = Pleroma.Notification.for_user(user2) - assert notification.activity.id == activity.id - - assert [activity] == ActivityPub.fetch_public_activities(%{}) |> Repo.preload(:bookmark) - - assert [%{activity | thread_muted?: CommonAPI.thread_muted?(user2, activity)}] == - ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{ - user: user2 - }) - - {:ok, _user} = User.deactivate(user) - - assert [] == ActivityPub.fetch_public_activities(%{}) - assert [] == Pleroma.Notification.for_user(user2) - - assert [] == - ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{ - user: user2 - }) - end - end - - describe "approve" do - test "approves a user" do - user = insert(:user, approval_pending: true) - assert true == user.approval_pending - {:ok, user} = User.approve(user) - assert false == user.approval_pending - end - - test "approves a list of users" do - unapproved_users = [ - insert(:user, approval_pending: true), - insert(:user, approval_pending: true), - insert(:user, approval_pending: true) - ] - - {:ok, users} = User.approve(unapproved_users) - - assert Enum.count(users) == 3 - - Enum.each(users, fn user -> - assert false == user.approval_pending - end) - end - end - - describe "delete" do - setup do - {:ok, user} = insert(:user) |> User.set_cache() - - [user: user] - end - - setup do: clear_config([:instance, :federating]) - - test ".delete_user_activities deletes all create activities", %{user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "2hu"}) - - User.delete_user_activities(user) - - # TODO: Test removal favorites, repeats, delete activities. - refute Activity.get_by_id(activity.id) - end - - test "it deactivates a user, all follow relationships and all activities", %{user: user} do - follower = insert(:user) - {:ok, follower} = User.follow(follower, user) - - locked_user = insert(:user, name: "locked", locked: true) - {:ok, _} = User.follow(user, locked_user, :follow_pending) - - object = insert(:note, user: user) - activity = insert(:note_activity, user: user, note: object) - - object_two = insert(:note, user: follower) - activity_two = insert(:note_activity, user: follower, note: object_two) - - {:ok, like} = CommonAPI.favorite(user, activity_two.id) - {:ok, like_two} = CommonAPI.favorite(follower, activity.id) - {:ok, repeat} = CommonAPI.repeat(activity_two.id, user) - - {:ok, job} = User.delete(user) - {:ok, _user} = ObanHelpers.perform(job) - - follower = User.get_cached_by_id(follower.id) - - refute User.following?(follower, user) - assert %{deactivated: true} = User.get_by_id(user.id) - - assert [] == User.get_follow_requests(locked_user) - - user_activities = - user.ap_id - |> Activity.Queries.by_actor() - |> Repo.all() - |> Enum.map(fn act -> act.data["type"] end) - - assert Enum.all?(user_activities, fn act -> act in ~w(Delete Undo) end) - - refute Activity.get_by_id(activity.id) - refute Activity.get_by_id(like.id) - refute Activity.get_by_id(like_two.id) - refute Activity.get_by_id(repeat.id) - end - end - - describe "delete/1 when confirmation is pending" do - setup do - user = insert(:user, confirmation_pending: true) - {:ok, user: user} - end - - test "deletes user from database when activation required", %{user: user} do - clear_config([:instance, :account_activation_required], true) - - {:ok, job} = User.delete(user) - {:ok, _} = ObanHelpers.perform(job) - - refute User.get_cached_by_id(user.id) - refute User.get_by_id(user.id) - end - - test "deactivates user when activation is not required", %{user: user} do - clear_config([:instance, :account_activation_required], false) - - {:ok, job} = User.delete(user) - {:ok, _} = ObanHelpers.perform(job) - - assert %{deactivated: true} = User.get_cached_by_id(user.id) - assert %{deactivated: true} = User.get_by_id(user.id) - end - end - - test "delete/1 when approval is pending deletes the user" do - user = insert(:user, approval_pending: true) - - {:ok, job} = User.delete(user) - {:ok, _} = ObanHelpers.perform(job) - - refute User.get_cached_by_id(user.id) - refute User.get_by_id(user.id) - end - - test "delete/1 purges a user when they wouldn't be fully deleted" do - user = - insert(:user, %{ - bio: "eyy lmao", - name: "qqqqqqq", - password_hash: "pdfk2$1b3n159001", - keys: "RSA begin buplic key", - public_key: "--PRIVATE KEYE--", - avatar: %{"a" => "b"}, - tags: ["qqqqq"], - banner: %{"a" => "b"}, - background: %{"a" => "b"}, - note_count: 9, - follower_count: 9, - following_count: 9001, - locked: true, - confirmation_pending: true, - password_reset_pending: true, - approval_pending: true, - registration_reason: "ahhhhh", - confirmation_token: "qqqq", - domain_blocks: ["lain.com"], - deactivated: true, - ap_enabled: true, - is_moderator: true, - is_admin: true, - mastofe_settings: %{"a" => "b"}, - mascot: %{"a" => "b"}, - emoji: %{"a" => "b"}, - pleroma_settings_store: %{"q" => "x"}, - fields: [%{"gg" => "qq"}], - raw_fields: [%{"gg" => "qq"}], - discoverable: true, - also_known_as: ["https://lol.olo/users/loll"] - }) - - {:ok, job} = User.delete(user) - {:ok, _} = ObanHelpers.perform(job) - user = User.get_by_id(user.id) - - assert %User{ - bio: "", - raw_bio: nil, - email: nil, - name: nil, - password_hash: nil, - keys: nil, - public_key: nil, - avatar: %{}, - tags: [], - last_refreshed_at: nil, - last_digest_emailed_at: nil, - banner: %{}, - background: %{}, - note_count: 0, - follower_count: 0, - following_count: 0, - locked: false, - confirmation_pending: false, - password_reset_pending: false, - approval_pending: false, - registration_reason: nil, - confirmation_token: nil, - domain_blocks: [], - deactivated: true, - ap_enabled: false, - is_moderator: false, - is_admin: false, - mastofe_settings: nil, - mascot: nil, - emoji: %{}, - pleroma_settings_store: %{}, - fields: [], - raw_fields: [], - discoverable: false, - also_known_as: [] - } = user - 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") - end - - describe "per-user rich-text filtering" do - test "html_filter_policy returns default policies, when rich-text is enabled" do - user = insert(:user) - - assert Pleroma.Config.get([:markup, :scrub_policy]) == User.html_filter_policy(user) - end - - test "html_filter_policy returns TwitterText scrubber when rich-text is disabled" do - user = insert(:user, no_rich_text: true) - - assert Pleroma.HTML.Scrubber.TwitterText == User.html_filter_policy(user) - end - end - - describe "caching" do - test "invalidate_cache works" do - user = insert(:user) - - User.set_cache(user) - User.invalidate_cache(user) - - {:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") - {:ok, nil} = Cachex.get(:user_cache, "nickname:#{user.nickname}") - end - - test "User.delete() plugs any possible zombie objects" do - user = insert(:user) - - {:ok, job} = User.delete(user) - {:ok, _} = ObanHelpers.perform(job) - - {:ok, cached_user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") - - assert cached_user != user - - {:ok, cached_user} = Cachex.get(:user_cache, "nickname:#{user.ap_id}") - - assert cached_user != user - end - end - - describe "account_status/1" do - setup do: clear_config([:instance, :account_activation_required]) - - test "return confirmation_pending for unconfirm user" do - Pleroma.Config.put([:instance, :account_activation_required], true) - user = insert(:user, confirmation_pending: true) - assert User.account_status(user) == :confirmation_pending - end - - test "return active for confirmed user" do - Pleroma.Config.put([:instance, :account_activation_required], true) - user = insert(:user, confirmation_pending: false) - assert User.account_status(user) == :active - end - - test "return active for remote user" do - user = insert(:user, local: false) - assert User.account_status(user) == :active - end - - test "returns :password_reset_pending for user with reset password" do - user = insert(:user, password_reset_pending: true) - assert User.account_status(user) == :password_reset_pending - end - - test "returns :deactivated for deactivated user" do - user = insert(:user, local: true, confirmation_pending: false, deactivated: true) - assert User.account_status(user) == :deactivated - end - - test "returns :approval_pending for unapproved user" do - user = insert(:user, local: true, approval_pending: true) - assert User.account_status(user) == :approval_pending - - user = insert(:user, local: true, confirmation_pending: true, approval_pending: true) - assert User.account_status(user) == :approval_pending - end - end - - describe "superuser?/1" do - test "returns false for unprivileged users" do - user = insert(:user, local: true) - - refute User.superuser?(user) - 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) - end - - test "returns true for local moderators" do - user = insert(:user, local: true, is_moderator: true) - - assert User.superuser?(user) - end - - test "returns true for local admins" do - user = insert(:user, local: true, is_admin: true) - - assert User.superuser?(user) - end - end - - describe "invisible?/1" do - test "returns true for an invisible user" do - user = insert(:user, local: true, invisible: true) - - assert User.invisible?(user) - end - - test "returns false for a non-invisible user" do - user = insert(:user, local: true) - - refute User.invisible?(user) - end - end - - describe "visible_for/2" do - test "returns true when the account is itself" do - user = insert(:user, local: true) - - assert User.visible_for(user, user) == :visible - end - - test "returns false when the account is unconfirmed and confirmation is required" do - Pleroma.Config.put([:instance, :account_activation_required], true) - - user = insert(:user, local: true, confirmation_pending: true) - other_user = insert(:user, local: true) - - refute User.visible_for(user, other_user) == :visible - end - - test "returns true when the account is unconfirmed and confirmation is required but the account is remote" do - Pleroma.Config.put([:instance, :account_activation_required], true) - - user = insert(:user, local: false, confirmation_pending: true) - other_user = insert(:user, local: true) - - assert User.visible_for(user, other_user) == :visible - end - - test "returns true when the account is unconfirmed and confirmation is not required" do - user = insert(:user, local: true, confirmation_pending: true) - other_user = insert(:user, local: true) - - 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 - Pleroma.Config.put([:instance, :account_activation_required], true) - - user = insert(:user, local: true, confirmation_pending: true) - other_user = insert(:user, local: true, is_admin: true) - - assert User.visible_for(user, other_user) == :visible - end - end - - describe "parse_bio/2" do - test "preserves hosts in user links text" do - remote_user = insert(:user, local: false, nickname: "nick@domain.com") - user = insert(:user) - bio = "A.k.a. @nick@domain.com" - - expected_text = - ~s(A.k.a. <span class="h-card"><a class="u-url mention" data-user="#{remote_user.id}" href="#{ - remote_user.ap_id - }" rel="ugc">@<span>nick@domain.com</span></a></span>) - - assert expected_text == User.parse_bio(bio, user) - end - - test "Adds rel=me on linkbacked urls" do - user = insert(:user, ap_id: "https://social.example.org/users/lain") - - bio = "http://example.com/rel_me/null" - expected_text = "<a href=\"#{bio}\">#{bio}</a>" - assert expected_text == User.parse_bio(bio, user) - - bio = "http://example.com/rel_me/link" - expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>" - assert expected_text == User.parse_bio(bio, user) - - bio = "http://example.com/rel_me/anchor" - expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>" - assert expected_text == User.parse_bio(bio, user) - end - end - - test "follower count is updated when a follower is blocked" do - user = insert(:user) - follower = insert(:user) - follower2 = insert(:user) - follower3 = insert(:user) - - {:ok, follower} = User.follow(follower, user) - {:ok, _follower2} = User.follow(follower2, user) - {:ok, _follower3} = User.follow(follower3, user) - - {:ok, _user_relationship} = User.block(user, follower) - user = refresh_record(user) - - assert user.follower_count == 2 - end - - describe "list_inactive_users_query/1" do - defp days_ago(days) do - NaiveDateTime.add( - NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), - -days * 60 * 60 * 24, - :second - ) - end - - test "Users are inactive by default" do - total = 10 - - users = - Enum.map(1..total, fn _ -> - insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false) - end) - - inactive_users_ids = - Pleroma.User.list_inactive_users_query() - |> Pleroma.Repo.all() - |> Enum.map(& &1.id) - - Enum.each(users, fn user -> - assert user.id in inactive_users_ids - end) - end - - test "Only includes users who has no recent activity" do - total = 10 - - users = - Enum.map(1..total, fn _ -> - insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false) - end) - - {inactive, active} = Enum.split(users, trunc(total / 2)) - - Enum.map(active, fn user -> - to = Enum.random(users -- [user]) - - {:ok, _} = - CommonAPI.post(user, %{ - status: "hey @#{to.nickname}" - }) - end) - - inactive_users_ids = - Pleroma.User.list_inactive_users_query() - |> Pleroma.Repo.all() - |> Enum.map(& &1.id) - - Enum.each(active, fn user -> - refute user.id in inactive_users_ids - end) - - Enum.each(inactive, fn user -> - assert user.id in inactive_users_ids - end) - end - - test "Only includes users with no read notifications" do - total = 10 - - users = - Enum.map(1..total, fn _ -> - insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false) - end) - - [sender | recipients] = users - {inactive, active} = Enum.split(recipients, trunc(total / 2)) - - Enum.each(recipients, fn to -> - {:ok, _} = - CommonAPI.post(sender, %{ - status: "hey @#{to.nickname}" - }) - - {:ok, _} = - CommonAPI.post(sender, %{ - status: "hey again @#{to.nickname}" - }) - end) - - Enum.each(active, fn user -> - [n1, _n2] = Pleroma.Notification.for_user(user) - {:ok, _} = Pleroma.Notification.read_one(user, n1.id) - end) - - inactive_users_ids = - Pleroma.User.list_inactive_users_query() - |> Pleroma.Repo.all() - |> Enum.map(& &1.id) - - Enum.each(active, fn user -> - refute user.id in inactive_users_ids - end) - - Enum.each(inactive, fn user -> - assert user.id in inactive_users_ids - end) - end - end - - describe "toggle_confirmation/1" do - test "if user is confirmed" do - user = insert(:user, confirmation_pending: false) - {:ok, user} = User.toggle_confirmation(user) - - assert user.confirmation_pending - assert user.confirmation_token - end - - test "if user is unconfirmed" do - user = insert(:user, confirmation_pending: true, confirmation_token: "some token") - {:ok, user} = User.toggle_confirmation(user) - - refute user.confirmation_pending - refute user.confirmation_token - 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) - user_two = insert(:user) - - ap_ids = User.get_ap_ids_by_nicknames([user.nickname, user_two.nickname, "nonexistent"]) - assert length(ap_ids) == 2 - assert user.ap_id in ap_ids - assert user_two.ap_id in ap_ids - end - end - - describe "sync followers count" do - setup do - user1 = insert(:user, local: false, ap_id: "http://localhost:4001/users/masto_closed") - user2 = insert(:user, local: false, ap_id: "http://localhost:4001/users/fuser2") - insert(:user, local: true) - insert(:user, local: false, deactivated: true) - {:ok, user1: user1, user2: user2} - end - - test "external_users/1 external active users with limit", %{user1: user1, user2: user2} do - [fdb_user1] = User.external_users(limit: 1) - - assert fdb_user1.ap_id - assert fdb_user1.ap_id == user1.ap_id - assert fdb_user1.id == user1.id - - [fdb_user2] = User.external_users(max_id: fdb_user1.id, limit: 1) - - assert fdb_user2.ap_id - assert fdb_user2.ap_id == user2.ap_id - assert fdb_user2.id == user2.id - - assert User.external_users(max_id: fdb_user2.id, limit: 1) == [] - end - end - - describe "is_internal_user?/1" do - test "non-internal user returns false" do - user = insert(:user) - refute User.is_internal_user?(user) - end - - test "user with no nickname returns true" do - user = insert(:user, %{nickname: nil}) - assert User.is_internal_user?(user) - end - - test "user with internal-prefixed nickname returns true" do - user = insert(:user, %{nickname: "internal.test"}) - assert User.is_internal_user?(user) - end - end - - describe "update_and_set_cache/1" do - test "returns error when user is stale instead Ecto.StaleEntryError" do - user = insert(:user) - - changeset = Ecto.Changeset.change(user, bio: "test") - - Repo.delete(user) - - assert {:error, %Ecto.Changeset{errors: [id: {"is stale", [stale: true]}], valid?: false}} = - User.update_and_set_cache(changeset) - end - - test "performs update cache if user updated" do - user = insert(:user) - assert {:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") - - changeset = Ecto.Changeset.change(user, bio: "test-bio") - - assert {:ok, %User{bio: "test-bio"} = user} = User.update_and_set_cache(changeset) - 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 - end - - describe "following/followers synchronization" do - setup do: clear_config([:instance, :external_user_synchronization]) - - test "updates the counters normally on following/getting a follow when disabled" do - Pleroma.Config.put([:instance, :external_user_synchronization], false) - user = insert(:user) - - other_user = - 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 - ) - - assert other_user.following_count == 0 - assert other_user.follower_count == 0 - - {:ok, user} = Pleroma.User.follow(user, other_user) - other_user = Pleroma.User.get_by_id(other_user.id) - - assert user.following_count == 1 - assert other_user.follower_count == 1 - end - - test "syncronizes the counters with the remote instance for the followed when enabled" do - Pleroma.Config.put([:instance, :external_user_synchronization], false) - - user = insert(:user) - - other_user = - 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 - ) - - assert other_user.following_count == 0 - assert other_user.follower_count == 0 - - Pleroma.Config.put([:instance, :external_user_synchronization], true) - {:ok, _user} = User.follow(user, other_user) - other_user = User.get_by_id(other_user.id) - - assert other_user.follower_count == 437 - end - - test "syncronizes the counters with the remote instance for the follower when enabled" do - Pleroma.Config.put([:instance, :external_user_synchronization], false) - - user = insert(:user) - - other_user = - 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 - ) - - assert other_user.following_count == 0 - assert other_user.follower_count == 0 - - Pleroma.Config.put([:instance, :external_user_synchronization], true) - {:ok, other_user} = User.follow(other_user, user) - - assert other_user.following_count == 152 - end - end - - describe "change_email/2" do - setup do - [user: insert(:user)] - end - - test "blank email returns error", %{user: user} do - assert {:error, %{errors: [email: {"can't be blank", _}]}} = User.change_email(user, "") - assert {:error, %{errors: [email: {"can't be blank", _}]}} = User.change_email(user, nil) - end - - test "non unique email returns error", %{user: user} do - %{email: email} = insert(:user) - - assert {:error, %{errors: [email: {"has already been taken", _}]}} = - User.change_email(user, email) - end - - test "invalid email returns error", %{user: user} do - assert {:error, %{errors: [email: {"has invalid format", _}]}} = - User.change_email(user, "cofe") - end - - test "changes email", %{user: user} do - assert {:ok, %User{email: "cofe@cofe.party"}} = User.change_email(user, "cofe@cofe.party") - end - end - - describe "get_cached_by_nickname_or_id" do - setup do - local_user = insert(:user) - remote_user = insert(:user, nickname: "nickname@example.com", local: false) - - [local_user: local_user, remote_user: remote_user] - end - - setup do: clear_config([:instance, :limit_to_local_content]) - - test "allows getting remote users by id no matter what :limit_to_local_content is set to", %{ - remote_user: remote_user - } do - Pleroma.Config.put([:instance, :limit_to_local_content], false) - assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id) - - Pleroma.Config.put([:instance, :limit_to_local_content], true) - assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id) - - Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) - assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id) - end - - test "disallows getting remote users by nickname without authentication when :limit_to_local_content is set to :unauthenticated", - %{remote_user: remote_user} do - Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) - assert nil == User.get_cached_by_nickname_or_id(remote_user.nickname) - end - - test "allows getting remote users by nickname with authentication when :limit_to_local_content is set to :unauthenticated", - %{remote_user: remote_user, local_user: local_user} do - Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) - assert %User{} = User.get_cached_by_nickname_or_id(remote_user.nickname, for: local_user) - end - - test "disallows getting remote users by nickname when :limit_to_local_content is set to true", - %{remote_user: remote_user} do - Pleroma.Config.put([:instance, :limit_to_local_content], true) - assert nil == User.get_cached_by_nickname_or_id(remote_user.nickname) - end - - test "allows getting local users by nickname no matter what :limit_to_local_content is set to", - %{local_user: local_user} do - Pleroma.Config.put([:instance, :limit_to_local_content], false) - assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname) - - Pleroma.Config.put([:instance, :limit_to_local_content], true) - assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname) - - Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) - assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname) - end - end - - describe "update_email_notifications/2" do - setup do - user = insert(:user, email_notifications: %{"digest" => true}) - - {:ok, user: user} - end - - test "Notifications are updated", %{user: user} do - true = user.email_notifications["digest"] - assert {:ok, result} = User.update_email_notifications(user, %{"digest" => false}) - assert result.email_notifications["digest"] == false - end - end - - test "avatar fallback" do - user = insert(:user) - assert User.avatar_url(user) =~ "/images/avi.png" - - clear_config([:assets, :default_user_avatar], "avatar.png") - - user = User.get_cached_by_nickname_or_id(user.nickname) - assert User.avatar_url(user) =~ "avatar.png" - - assert User.avatar_url(user, no_default: true) == nil - end -end diff --git a/test/utils_test.exs b/test/utils_test.exs @@ -1,15 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.UtilsTest do - use ExUnit.Case, async: true - - describe "tmp_dir/1" do - test "returns unique temporary directory" do - {:ok, path} = Pleroma.Utils.tmp_dir("emoji") - assert path =~ ~r/\/emoji-(.*)-#{:os.getpid()}-(.*)/ - File.rm_rf(path) - end - end -end diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs @@ -1,1550 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do - use Pleroma.Web.ConnCase - use Oban.Testing, repo: Pleroma.Repo - - alias Pleroma.Activity - alias Pleroma.Config - alias Pleroma.Delivery - alias Pleroma.Instances - alias Pleroma.Object - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.ObjectView - alias Pleroma.Web.ActivityPub.Relay - alias Pleroma.Web.ActivityPub.UserView - alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Endpoint - alias Pleroma.Workers.ReceiverWorker - - import Pleroma.Factory - - require Pleroma.Constants - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup do: clear_config([:instance, :federating], true) - - describe "/relay" do - setup do: clear_config([:instance, :allow_relay]) - - test "with the relay active, it returns the relay user", %{conn: conn} do - res = - conn - |> get(activity_pub_path(conn, :relay)) - |> json_response(200) - - assert res["id"] =~ "/relay" - end - - test "with the relay disabled, it returns 404", %{conn: conn} do - Config.put([:instance, :allow_relay], false) - - conn - |> get(activity_pub_path(conn, :relay)) - |> json_response(404) - end - - test "on non-federating instance, it returns 404", %{conn: conn} do - Config.put([:instance, :federating], false) - user = insert(:user) - - conn - |> assign(:user, user) - |> get(activity_pub_path(conn, :relay)) - |> json_response(404) - end - end - - describe "/internal/fetch" do - test "it returns the internal fetch user", %{conn: conn} do - res = - conn - |> get(activity_pub_path(conn, :internal_fetch)) - |> json_response(200) - - assert res["id"] =~ "/fetch" - end - - test "on non-federating instance, it returns 404", %{conn: conn} do - Config.put([:instance, :federating], false) - user = insert(:user) - - conn - |> assign(:user, user) - |> get(activity_pub_path(conn, :internal_fetch)) - |> json_response(404) - end - end - - describe "/users/:nickname" do - test "it returns a json representation of the user with accept application/json", %{ - conn: conn - } do - user = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/json") - |> get("/users/#{user.nickname}") - - user = User.get_cached_by_id(user.id) - - assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) - end - - test "it returns a json representation of the user with accept application/activity+json", %{ - conn: conn - } do - user = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/users/#{user.nickname}") - - user = User.get_cached_by_id(user.id) - - assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) - end - - test "it returns a json representation of the user with accept application/ld+json", %{ - conn: conn - } do - user = insert(:user) - - conn = - conn - |> put_req_header( - "accept", - "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" - ) - |> get("/users/#{user.nickname}") - - user = User.get_cached_by_id(user.id) - - assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) - end - - test "it returns 404 for remote users", %{ - conn: conn - } do - user = insert(:user, local: false, nickname: "remoteuser@example.com") - - conn = - conn - |> put_req_header("accept", "application/json") - |> get("/users/#{user.nickname}.json") - - assert json_response(conn, 404) - end - - test "it returns error when user is not found", %{conn: conn} do - response = - conn - |> put_req_header("accept", "application/json") - |> get("/users/jimm") - |> json_response(404) - - assert response == "Not found" - end - - test "it requires authentication if instance is NOT federating", %{ - conn: conn - } do - user = insert(:user) - - conn = - put_req_header( - conn, - "accept", - "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" - ) - - ensure_federating_or_authenticated(conn, "/users/#{user.nickname}.json", user) - end - end - - describe "mastodon compatibility routes" do - test "it returns a json representation of the object with accept application/json", %{ - conn: conn - } do - {:ok, object} = - %{ - "type" => "Note", - "content" => "hey", - "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999", - "actor" => Endpoint.url() <> "/users/raymoo", - "to" => [Pleroma.Constants.as_public()] - } - |> Object.create() - - conn = - conn - |> put_req_header("accept", "application/json") - |> get("/users/raymoo/statuses/999999999") - - assert json_response(conn, 200) == ObjectView.render("object.json", %{object: object}) - end - - test "it returns a json representation of the activity with accept application/json", %{ - conn: conn - } do - {:ok, object} = - %{ - "type" => "Note", - "content" => "hey", - "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999", - "actor" => Endpoint.url() <> "/users/raymoo", - "to" => [Pleroma.Constants.as_public()] - } - |> Object.create() - - {:ok, activity, _} = - %{ - "id" => object.data["id"] <> "/activity", - "type" => "Create", - "object" => object.data["id"], - "actor" => object.data["actor"], - "to" => object.data["to"] - } - |> ActivityPub.persist(local: true) - - conn = - conn - |> put_req_header("accept", "application/json") - |> get("/users/raymoo/statuses/999999999/activity") - - assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity}) - end - end - - describe "/objects/:uuid" do - test "it returns a json representation of the object with accept application/json", %{ - conn: conn - } do - note = insert(:note) - uuid = String.split(note.data["id"], "/") |> List.last() - - conn = - conn - |> put_req_header("accept", "application/json") - |> get("/objects/#{uuid}") - - assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) - end - - test "it returns a json representation of the object with accept application/activity+json", - %{conn: conn} do - note = insert(:note) - uuid = String.split(note.data["id"], "/") |> List.last() - - conn = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/objects/#{uuid}") - - assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) - end - - test "it returns a json representation of the object with accept application/ld+json", %{ - conn: conn - } do - note = insert(:note) - uuid = String.split(note.data["id"], "/") |> List.last() - - conn = - conn - |> put_req_header( - "accept", - "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" - ) - |> get("/objects/#{uuid}") - - assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) - end - - test "it returns 404 for non-public messages", %{conn: conn} do - note = insert(:direct_note) - uuid = String.split(note.data["id"], "/") |> List.last() - - conn = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/objects/#{uuid}") - - assert json_response(conn, 404) - end - - test "it returns 404 for tombstone objects", %{conn: conn} do - tombstone = insert(:tombstone) - uuid = String.split(tombstone.data["id"], "/") |> List.last() - - conn = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/objects/#{uuid}") - - assert json_response(conn, 404) - end - - test "it caches a response", %{conn: conn} do - note = insert(:note) - uuid = String.split(note.data["id"], "/") |> List.last() - - conn1 = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/objects/#{uuid}") - - assert json_response(conn1, :ok) - assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) - - conn2 = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/objects/#{uuid}") - - assert json_response(conn1, :ok) == json_response(conn2, :ok) - assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"})) - end - - test "cached purged after object deletion", %{conn: conn} do - note = insert(:note) - uuid = String.split(note.data["id"], "/") |> List.last() - - conn1 = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/objects/#{uuid}") - - assert json_response(conn1, :ok) - assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) - - Object.delete(note) - - conn2 = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/objects/#{uuid}") - - assert "Not found" == json_response(conn2, :not_found) - end - - test "it requires authentication if instance is NOT federating", %{ - conn: conn - } do - user = insert(:user) - note = insert(:note) - uuid = String.split(note.data["id"], "/") |> List.last() - - conn = put_req_header(conn, "accept", "application/activity+json") - - ensure_federating_or_authenticated(conn, "/objects/#{uuid}", user) - end - end - - describe "/activities/:uuid" do - test "it returns a json representation of the activity", %{conn: conn} do - activity = insert(:note_activity) - uuid = String.split(activity.data["id"], "/") |> List.last() - - conn = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/activities/#{uuid}") - - assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity}) - end - - test "it returns 404 for non-public activities", %{conn: conn} do - activity = insert(:direct_note_activity) - uuid = String.split(activity.data["id"], "/") |> List.last() - - conn = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/activities/#{uuid}") - - assert json_response(conn, 404) - end - - test "it caches a response", %{conn: conn} do - activity = insert(:note_activity) - uuid = String.split(activity.data["id"], "/") |> List.last() - - conn1 = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/activities/#{uuid}") - - assert json_response(conn1, :ok) - assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) - - conn2 = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/activities/#{uuid}") - - assert json_response(conn1, :ok) == json_response(conn2, :ok) - assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"})) - end - - test "cached purged after activity deletion", %{conn: conn} do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "cofe"}) - - uuid = String.split(activity.data["id"], "/") |> List.last() - - conn1 = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/activities/#{uuid}") - - assert json_response(conn1, :ok) - assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) - - Activity.delete_all_by_object_ap_id(activity.object.data["id"]) - - conn2 = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/activities/#{uuid}") - - assert "Not found" == json_response(conn2, :not_found) - end - - test "it requires authentication if instance is NOT federating", %{ - conn: conn - } do - user = insert(:user) - activity = insert(:note_activity) - uuid = String.split(activity.data["id"], "/") |> List.last() - - conn = put_req_header(conn, "accept", "application/activity+json") - - ensure_federating_or_authenticated(conn, "/activities/#{uuid}", user) - end - end - - describe "/inbox" do - test "it inserts an incoming activity into the database", %{conn: conn} do - data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/inbox", data) - - assert "ok" == json_response(conn, 200) - - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - assert Activity.get_by_ap_id(data["id"]) - end - - @tag capture_log: true - test "it inserts an incoming activity into the database" <> - "even if we can't fetch the user but have it in our db", - %{conn: conn} do - user = - insert(:user, - ap_id: "https://mastodon.example.org/users/raymoo", - ap_enabled: true, - local: false, - last_refreshed_at: nil - ) - - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - |> Map.put("actor", user.ap_id) - |> put_in(["object", "attridbutedTo"], user.ap_id) - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/inbox", data) - - assert "ok" == json_response(conn, 200) - - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - assert Activity.get_by_ap_id(data["id"]) - end - - test "it clears `unreachable` federation status of the sender", %{conn: conn} do - data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() - - sender_url = data["actor"] - Instances.set_consistently_unreachable(sender_url) - refute Instances.reachable?(sender_url) - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/inbox", data) - - assert "ok" == json_response(conn, 200) - assert Instances.reachable?(sender_url) - end - - test "accept follow activity", %{conn: conn} do - Pleroma.Config.put([:instance, :federating], true) - relay = Relay.get_actor() - - assert {:ok, %Activity{} = activity} = Relay.follow("https://relay.mastodon.host/actor") - - followed_relay = Pleroma.User.get_by_ap_id("https://relay.mastodon.host/actor") - relay = refresh_record(relay) - - accept = - File.read!("test/fixtures/relay/accept-follow.json") - |> String.replace("{{ap_id}}", relay.ap_id) - |> String.replace("{{activity_id}}", activity.data["id"]) - - assert "ok" == - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/inbox", accept) - |> json_response(200) - - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - - assert Pleroma.FollowingRelationship.following?( - relay, - followed_relay - ) - - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - - :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) - assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]} - end - - @tag capture_log: true - test "without valid signature, " <> - "it only accepts Create activities and requires enabled federation", - %{conn: conn} do - data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() - non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!() - - conn = put_req_header(conn, "content-type", "application/activity+json") - - Config.put([:instance, :federating], false) - - conn - |> post("/inbox", data) - |> json_response(403) - - conn - |> post("/inbox", non_create_data) - |> json_response(403) - - Config.put([:instance, :federating], true) - - ret_conn = post(conn, "/inbox", data) - assert "ok" == json_response(ret_conn, 200) - - conn - |> post("/inbox", non_create_data) - |> json_response(400) - end - end - - describe "/users/:nickname/inbox" do - setup do - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - - [data: data] - end - - test "it inserts an incoming activity into the database", %{conn: conn, data: data} do - user = insert(:user) - data = Map.put(data, "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 "ok" == json_response(conn, 200) - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - assert Activity.get_by_ap_id(data["id"]) - end - - test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do - user = insert(:user) - - data = - Map.put(data, "to", user.ap_id) - |> Map.delete("cc") - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/inbox", data) - - assert "ok" == json_response(conn, 200) - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - assert Activity.get_by_ap_id(data["id"]) - end - - test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do - user = insert(:user) - - data = - Map.put(data, "cc", user.ap_id) - |> Map.delete("to") - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/inbox", data) - - assert "ok" == json_response(conn, 200) - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - %Activity{} = activity = Activity.get_by_ap_id(data["id"]) - assert user.ap_id in activity.recipients - end - - test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do - user = insert(:user) - - data = - Map.put(data, "bcc", user.ap_id) - |> Map.delete("to") - |> Map.delete("cc") - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/inbox", data) - - assert "ok" == json_response(conn, 200) - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - assert Activity.get_by_ap_id(data["id"]) - end - - test "it accepts announces with to as string instead of array", %{conn: conn} do - user = insert(:user) - - {:ok, post} = CommonAPI.post(user, %{status: "hey"}) - announcer = insert(:user, local: false) - - data = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "actor" => announcer.ap_id, - "id" => "#{announcer.ap_id}/statuses/19512778738411822/activity", - "object" => post.data["object"], - "to" => "https://www.w3.org/ns/activitystreams#Public", - "cc" => [user.ap_id], - "type" => "Announce" - } - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/inbox", data) - - assert "ok" == json_response(conn, 200) - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - %Activity{} = activity = Activity.get_by_ap_id(data["id"]) - assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients - end - - test "it accepts messages from actors that are followed by the user", %{ - conn: conn, - data: data - } do - recipient = insert(:user) - actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"}) - - {:ok, recipient} = User.follow(recipient, actor) - - object = - data["object"] - |> Map.put("attributedTo", actor.ap_id) - - data = - data - |> Map.put("actor", actor.ap_id) - |> Map.put("object", object) - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{recipient.nickname}/inbox", data) - - assert "ok" == json_response(conn, 200) - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - assert Activity.get_by_ap_id(data["id"]) - end - - test "it rejects reads from other users", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - - conn = - conn - |> assign(:user, other_user) - |> put_req_header("accept", "application/activity+json") - |> get("/users/#{user.nickname}/inbox") - - assert json_response(conn, 403) - end - - test "it returns a note activity in a collection", %{conn: conn} do - note_activity = insert(:direct_note_activity) - note_object = Object.normalize(note_activity) - user = User.get_cached_by_ap_id(hd(note_activity.data["to"])) - - conn = - conn - |> assign(:user, user) - |> put_req_header("accept", "application/activity+json") - |> get("/users/#{user.nickname}/inbox?page=true") - - assert response(conn, 200) =~ note_object.data["content"] - end - - test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do - user = insert(:user) - data = Map.put(data, "bcc", [user.ap_id]) - - sender_host = URI.parse(data["actor"]).host - Instances.set_consistently_unreachable(sender_host) - refute Instances.reachable?(sender_host) - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/inbox", data) - - assert "ok" == json_response(conn, 200) - assert Instances.reachable?(sender_host) - end - - test "it removes all follower collections but actor's", %{conn: conn} do - [actor, recipient] = insert_pair(:user) - - data = - File.read!("test/fixtures/activitypub-client-post-activity.json") - |> Poison.decode!() - - object = Map.put(data["object"], "attributedTo", actor.ap_id) - - data = - data - |> Map.put("id", Utils.generate_object_id()) - |> Map.put("actor", actor.ap_id) - |> Map.put("object", object) - |> Map.put("cc", [ - recipient.follower_address, - actor.follower_address - ]) - |> Map.put("to", [ - recipient.ap_id, - recipient.follower_address, - "https://www.w3.org/ns/activitystreams#Public" - ]) - - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{recipient.nickname}/inbox", data) - |> json_response(200) - - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - - activity = Activity.get_by_ap_id(data["id"]) - - assert activity.id - assert actor.follower_address in activity.recipients - assert actor.follower_address in activity.data["cc"] - - refute recipient.follower_address in activity.recipients - refute recipient.follower_address in activity.data["cc"] - refute recipient.follower_address in activity.data["to"] - end - - test "it requires authentication", %{conn: conn} do - user = insert(:user) - conn = put_req_header(conn, "accept", "application/activity+json") - - ret_conn = get(conn, "/users/#{user.nickname}/inbox") - assert json_response(ret_conn, 403) - - ret_conn = - conn - |> assign(:user, user) - |> get("/users/#{user.nickname}/inbox") - - assert json_response(ret_conn, 200) - end - end - - describe "GET /users/:nickname/outbox" do - test "it paginates correctly", %{conn: conn} do - user = insert(:user) - conn = assign(conn, :user, user) - outbox_endpoint = user.ap_id <> "/outbox" - - _posts = - for i <- 0..25 do - {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"}) - activity - end - - result = - conn - |> put_req_header("accept", "application/activity+json") - |> get(outbox_endpoint <> "?page=true") - |> json_response(200) - - result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end) - assert length(result["orderedItems"]) == 20 - assert length(result_ids) == 20 - assert result["next"] - assert String.starts_with?(result["next"], outbox_endpoint) - - result_next = - conn - |> put_req_header("accept", "application/activity+json") - |> get(result["next"]) - |> json_response(200) - - result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end) - assert length(result_next["orderedItems"]) == 6 - assert length(result_next_ids) == 6 - refute Enum.find(result_next_ids, fn x -> x in result_ids end) - refute Enum.find(result_ids, fn x -> x in result_next_ids end) - assert String.starts_with?(result["id"], outbox_endpoint) - - result_next_again = - conn - |> put_req_header("accept", "application/activity+json") - |> get(result_next["id"]) - |> json_response(200) - - assert result_next == result_next_again - end - - test "it returns 200 even if there're no activities", %{conn: conn} do - user = insert(:user) - outbox_endpoint = user.ap_id <> "/outbox" - - conn = - conn - |> assign(:user, user) - |> put_req_header("accept", "application/activity+json") - |> get(outbox_endpoint) - - result = json_response(conn, 200) - assert outbox_endpoint == result["id"] - end - - test "it returns a note activity in a collection", %{conn: conn} do - note_activity = insert(:note_activity) - note_object = Object.normalize(note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - conn = - conn - |> assign(:user, user) - |> put_req_header("accept", "application/activity+json") - |> get("/users/#{user.nickname}/outbox?page=true") - - assert response(conn, 200) =~ note_object.data["content"] - end - - test "it returns an announce activity in a collection", %{conn: conn} do - announce_activity = insert(:announce_activity) - user = User.get_cached_by_ap_id(announce_activity.data["actor"]) - - conn = - conn - |> assign(:user, user) - |> put_req_header("accept", "application/activity+json") - |> get("/users/#{user.nickname}/outbox?page=true") - - assert response(conn, 200) =~ announce_activity.data["object"] - end - - test "it requires authentication if instance is NOT federating", %{ - conn: conn - } do - user = insert(:user) - conn = put_req_header(conn, "accept", "application/activity+json") - - ensure_federating_or_authenticated(conn, "/users/#{user.nickname}/outbox", user) - end - end - - describe "POST /users/:nickname/outbox (C2S)" do - setup do: clear_config([:instance, :limit]) - - setup do - [ - activity: %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Create", - "object" => %{"type" => "Note", "content" => "AP C2S test"}, - "to" => "https://www.w3.org/ns/activitystreams#Public", - "cc" => [] - } - ] - end - - test "it rejects posts from other users / unauthenticated users", %{ - conn: conn, - activity: activity - } do - user = insert(:user) - other_user = insert(:user) - conn = put_req_header(conn, "content-type", "application/activity+json") - - conn - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(403) - - conn - |> assign(:user, other_user) - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(403) - end - - test "it inserts an incoming create activity into the database", %{ - conn: conn, - activity: activity - } do - user = insert(:user) - - result = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(201) - - assert Activity.get_by_ap_id(result["id"]) - assert result["object"] - assert %Object{data: object} = Object.normalize(result["object"]) - assert object["content"] == activity["object"]["content"] - end - - test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do - user = insert(:user) - - activity = - activity - |> put_in(["object", "type"], "Benis") - - _result = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(400) - end - - test "it inserts an incoming sensitive activity into the database", %{ - conn: conn, - activity: activity - } do - user = insert(:user) - conn = assign(conn, :user, user) - object = Map.put(activity["object"], "sensitive", true) - activity = Map.put(activity, "object", object) - - response = - conn - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(201) - - assert Activity.get_by_ap_id(response["id"]) - assert response["object"] - assert %Object{data: response_object} = Object.normalize(response["object"]) - assert response_object["sensitive"] == true - assert response_object["content"] == activity["object"]["content"] - - representation = - conn - |> put_req_header("accept", "application/activity+json") - |> get(response["id"]) - |> json_response(200) - - assert representation["object"]["sensitive"] == true - end - - test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do - user = insert(:user) - activity = Map.put(activity, "type", "BadType") - - conn = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", activity) - - assert json_response(conn, 400) - end - - test "it erects a tombstone when receiving a delete activity", %{conn: conn} do - note_activity = insert(:note_activity) - note_object = Object.normalize(note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - data = %{ - type: "Delete", - object: %{ - id: note_object.data["id"] - } - } - - conn = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", data) - - result = json_response(conn, 201) - assert Activity.get_by_ap_id(result["id"]) - - assert object = Object.get_by_ap_id(note_object.data["id"]) - assert object.data["type"] == "Tombstone" - end - - test "it rejects delete activity of object from other actor", %{conn: conn} do - note_activity = insert(:note_activity) - note_object = Object.normalize(note_activity) - user = insert(:user) - - data = %{ - type: "Delete", - object: %{ - id: note_object.data["id"] - } - } - - conn = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", data) - - assert json_response(conn, 400) - end - - test "it increases like count when receiving a like action", %{conn: conn} do - note_activity = insert(:note_activity) - note_object = Object.normalize(note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - data = %{ - type: "Like", - object: %{ - id: note_object.data["id"] - } - } - - conn = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", data) - - result = json_response(conn, 201) - assert Activity.get_by_ap_id(result["id"]) - - assert object = Object.get_by_ap_id(note_object.data["id"]) - assert object.data["like_count"] == 1 - end - - test "it doesn't spreads faulty attributedTo or actor fields", %{ - conn: conn, - activity: activity - } do - reimu = insert(:user, nickname: "reimu") - cirno = insert(:user, nickname: "cirno") - - assert reimu.ap_id - assert cirno.ap_id - - activity = - activity - |> put_in(["object", "actor"], reimu.ap_id) - |> put_in(["object", "attributedTo"], reimu.ap_id) - |> put_in(["actor"], reimu.ap_id) - |> put_in(["attributedTo"], reimu.ap_id) - - _reimu_outbox = - conn - |> assign(:user, cirno) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{reimu.nickname}/outbox", activity) - |> json_response(403) - - cirno_outbox = - conn - |> assign(:user, cirno) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{cirno.nickname}/outbox", activity) - |> json_response(201) - - assert cirno_outbox["attributedTo"] == nil - assert cirno_outbox["actor"] == cirno.ap_id - - assert cirno_object = Object.normalize(cirno_outbox["object"]) - assert cirno_object.data["actor"] == cirno.ap_id - assert cirno_object.data["attributedTo"] == cirno.ap_id - end - - test "Character limitation", %{conn: conn, activity: activity} do - Pleroma.Config.put([:instance, :limit], 5) - user = insert(:user) - - result = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(400) - - assert result == "Note is over the character limit" - end - end - - describe "/relay/followers" do - test "it returns relay followers", %{conn: conn} do - relay_actor = Relay.get_actor() - user = insert(:user) - User.follow(user, relay_actor) - - result = - conn - |> get("/relay/followers") - |> json_response(200) - - assert result["first"]["orderedItems"] == [user.ap_id] - end - - test "on non-federating instance, it returns 404", %{conn: conn} do - Config.put([:instance, :federating], false) - user = insert(:user) - - conn - |> assign(:user, user) - |> get("/relay/followers") - |> json_response(404) - end - end - - describe "/relay/following" do - test "it returns relay following", %{conn: conn} do - result = - conn - |> get("/relay/following") - |> json_response(200) - - assert result["first"]["orderedItems"] == [] - end - - test "on non-federating instance, it returns 404", %{conn: conn} do - Config.put([:instance, :federating], false) - user = insert(:user) - - conn - |> assign(:user, user) - |> get("/relay/following") - |> json_response(404) - end - end - - describe "/users/:nickname/followers" do - test "it returns the followers in a collection", %{conn: conn} do - user = insert(:user) - user_two = insert(:user) - User.follow(user, user_two) - - result = - conn - |> assign(:user, user_two) - |> get("/users/#{user_two.nickname}/followers") - |> json_response(200) - - assert result["first"]["orderedItems"] == [user.ap_id] - end - - test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do - user = insert(:user) - user_two = insert(:user, hide_followers: true) - User.follow(user, user_two) - - result = - conn - |> assign(:user, user) - |> get("/users/#{user_two.nickname}/followers") - |> json_response(200) - - assert is_binary(result["first"]) - end - - test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user", - %{conn: conn} do - user = insert(:user) - other_user = insert(:user, hide_followers: true) - - result = - conn - |> assign(:user, user) - |> get("/users/#{other_user.nickname}/followers?page=1") - - assert result.status == 403 - assert result.resp_body == "" - end - - test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user", - %{conn: conn} do - user = insert(:user, hide_followers: true) - other_user = insert(:user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - - result = - conn - |> assign(:user, user) - |> get("/users/#{user.nickname}/followers?page=1") - |> json_response(200) - - assert result["totalItems"] == 1 - assert result["orderedItems"] == [other_user.ap_id] - end - - test "it works for more than 10 users", %{conn: conn} do - user = insert(:user) - - Enum.each(1..15, fn _ -> - other_user = insert(:user) - User.follow(other_user, user) - end) - - result = - conn - |> assign(:user, user) - |> get("/users/#{user.nickname}/followers") - |> json_response(200) - - assert length(result["first"]["orderedItems"]) == 10 - assert result["first"]["totalItems"] == 15 - assert result["totalItems"] == 15 - - result = - conn - |> assign(:user, user) - |> get("/users/#{user.nickname}/followers?page=2") - |> json_response(200) - - assert length(result["orderedItems"]) == 5 - assert result["totalItems"] == 15 - end - - test "does not require authentication", %{conn: conn} do - user = insert(:user) - - conn - |> get("/users/#{user.nickname}/followers") - |> json_response(200) - end - end - - describe "/users/:nickname/following" do - test "it returns the following in a collection", %{conn: conn} do - user = insert(:user) - user_two = insert(:user) - User.follow(user, user_two) - - result = - conn - |> assign(:user, user) - |> get("/users/#{user.nickname}/following") - |> json_response(200) - - assert result["first"]["orderedItems"] == [user_two.ap_id] - end - - test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do - user = insert(:user) - user_two = insert(:user, hide_follows: true) - User.follow(user, user_two) - - result = - conn - |> assign(:user, user) - |> get("/users/#{user_two.nickname}/following") - |> json_response(200) - - assert is_binary(result["first"]) - end - - test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user", - %{conn: conn} do - user = insert(:user) - user_two = insert(:user, hide_follows: true) - - result = - conn - |> assign(:user, user) - |> get("/users/#{user_two.nickname}/following?page=1") - - assert result.status == 403 - assert result.resp_body == "" - end - - test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user", - %{conn: conn} do - user = insert(:user, hide_follows: true) - other_user = insert(:user) - {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) - - result = - conn - |> assign(:user, user) - |> get("/users/#{user.nickname}/following?page=1") - |> json_response(200) - - assert result["totalItems"] == 1 - assert result["orderedItems"] == [other_user.ap_id] - end - - test "it works for more than 10 users", %{conn: conn} do - user = insert(:user) - - Enum.each(1..15, fn _ -> - user = User.get_cached_by_id(user.id) - other_user = insert(:user) - User.follow(user, other_user) - end) - - result = - conn - |> assign(:user, user) - |> get("/users/#{user.nickname}/following") - |> json_response(200) - - assert length(result["first"]["orderedItems"]) == 10 - assert result["first"]["totalItems"] == 15 - assert result["totalItems"] == 15 - - result = - conn - |> assign(:user, user) - |> get("/users/#{user.nickname}/following?page=2") - |> json_response(200) - - assert length(result["orderedItems"]) == 5 - assert result["totalItems"] == 15 - end - - test "does not require authentication", %{conn: conn} do - user = insert(:user) - - conn - |> get("/users/#{user.nickname}/following") - |> json_response(200) - end - end - - describe "delivery tracking" do - test "it tracks a signed object fetch", %{conn: conn} do - user = insert(:user, local: false) - activity = insert(:note_activity) - object = Object.normalize(activity) - - object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url()) - - conn - |> put_req_header("accept", "application/activity+json") - |> assign(:user, user) - |> get(object_path) - |> json_response(200) - - assert Delivery.get(object.id, user.id) - end - - test "it tracks a signed activity fetch", %{conn: conn} do - user = insert(:user, local: false) - activity = insert(:note_activity) - object = Object.normalize(activity) - - activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url()) - - conn - |> put_req_header("accept", "application/activity+json") - |> assign(:user, user) - |> get(activity_path) - |> json_response(200) - - assert Delivery.get(object.id, user.id) - end - - test "it tracks a signed object fetch when the json is cached", %{conn: conn} do - user = insert(:user, local: false) - other_user = insert(:user, local: false) - activity = insert(:note_activity) - object = Object.normalize(activity) - - object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url()) - - conn - |> put_req_header("accept", "application/activity+json") - |> assign(:user, user) - |> get(object_path) - |> json_response(200) - - build_conn() - |> put_req_header("accept", "application/activity+json") - |> assign(:user, other_user) - |> get(object_path) - |> json_response(200) - - assert Delivery.get(object.id, user.id) - assert Delivery.get(object.id, other_user.id) - end - - test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do - user = insert(:user, local: false) - other_user = insert(:user, local: false) - activity = insert(:note_activity) - object = Object.normalize(activity) - - activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url()) - - conn - |> put_req_header("accept", "application/activity+json") - |> assign(:user, user) - |> get(activity_path) - |> json_response(200) - - build_conn() - |> put_req_header("accept", "application/activity+json") - |> assign(:user, other_user) - |> get(activity_path) - |> json_response(200) - - assert Delivery.get(object.id, user.id) - assert Delivery.get(object.id, other_user.id) - end - end - - describe "Additional ActivityPub C2S endpoints" do - test "GET /api/ap/whoami", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> get("/api/ap/whoami") - - user = User.get_cached_by_id(user.id) - - assert UserView.render("user.json", %{user: user}) == json_response(conn, 200) - - conn - |> get("/api/ap/whoami") - |> json_response(403) - end - - setup do: clear_config([:media_proxy]) - setup do: clear_config([Pleroma.Upload]) - - test "POST /api/ap/upload_media", %{conn: conn} do - user = insert(:user) - - desc = "Description of the image" - - image = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - object = - conn - |> assign(:user, user) - |> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) - |> json_response(:created) - - assert object["name"] == desc - assert object["type"] == "Document" - assert object["actor"] == user.ap_id - assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"] - assert is_binary(object_href) - assert object_mediatype == "image/jpeg" - - activity_request = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Create", - "object" => %{ - "type" => "Note", - "content" => "AP C2S test, attachment", - "attachment" => [object] - }, - "to" => "https://www.w3.org/ns/activitystreams#Public", - "cc" => [] - } - - activity_response = - conn - |> assign(:user, user) - |> post("/users/#{user.nickname}/outbox", activity_request) - |> json_response(:created) - - assert activity_response["id"] - assert activity_response["object"] - assert activity_response["actor"] == user.ap_id - - assert %Object{data: %{"attachment" => [attachment]}} = - Object.normalize(activity_response["object"]) - - assert attachment["type"] == "Document" - assert attachment["name"] == desc - - assert [ - %{ - "href" => ^object_href, - "type" => "Link", - "mediaType" => ^object_mediatype - } - ] = attachment["url"] - - # Fails if unauthenticated - conn - |> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) - |> json_response(403) - end - end -end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs @@ -1,2260 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ActivityPubTest do - use Pleroma.DataCase - use Oban.Testing, repo: Pleroma.Repo - - alias Pleroma.Activity - alias Pleroma.Builders.ActivityBuilder - alias Pleroma.Config - alias Pleroma.Notification - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.AdminAPI.AccountView - alias Pleroma.Web.CommonAPI - - import ExUnit.CaptureLog - import Mock - import Pleroma.Factory - import Tesla.Mock - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup do: clear_config([:instance, :federating]) - - describe "streaming out participations" do - test "it streams them out" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) - - {:ok, conversation} = Pleroma.Conversation.create_or_bump_for(activity) - - participations = - conversation.participations - |> Repo.preload(:user) - - with_mock Pleroma.Web.Streamer, - stream: fn _, _ -> nil end do - ActivityPub.stream_out_participations(conversation.participations) - - assert called(Pleroma.Web.Streamer.stream("participation", participations)) - end - end - - test "streams them out on activity creation" do - user_one = insert(:user) - user_two = insert(:user) - - with_mock Pleroma.Web.Streamer, - stream: fn _, _ -> nil end do - {:ok, activity} = - CommonAPI.post(user_one, %{ - status: "@#{user_two.nickname}", - visibility: "direct" - }) - - conversation = - activity.data["context"] - |> Pleroma.Conversation.get_for_ap_id() - |> Repo.preload(participations: :user) - - assert called(Pleroma.Web.Streamer.stream("participation", conversation.participations)) - end - end - end - - describe "fetching restricted by visibility" do - test "it restricts by the appropriate visibility" do - user = insert(:user) - - {:ok, public_activity} = CommonAPI.post(user, %{status: ".", visibility: "public"}) - - {:ok, direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) - - {:ok, unlisted_activity} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) - - {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"}) - - activities = ActivityPub.fetch_activities([], %{visibility: "direct", actor_id: user.ap_id}) - - assert activities == [direct_activity] - - activities = - ActivityPub.fetch_activities([], %{visibility: "unlisted", actor_id: user.ap_id}) - - assert activities == [unlisted_activity] - - activities = - ActivityPub.fetch_activities([], %{visibility: "private", actor_id: user.ap_id}) - - assert activities == [private_activity] - - activities = ActivityPub.fetch_activities([], %{visibility: "public", actor_id: user.ap_id}) - - assert activities == [public_activity] - - activities = - ActivityPub.fetch_activities([], %{ - visibility: ~w[private public], - actor_id: user.ap_id - }) - - assert activities == [public_activity, private_activity] - end - end - - describe "fetching excluded by visibility" do - test "it excludes by the appropriate visibility" do - user = insert(:user) - - {:ok, public_activity} = CommonAPI.post(user, %{status: ".", visibility: "public"}) - - {:ok, direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) - - {:ok, unlisted_activity} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) - - {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"}) - - activities = - ActivityPub.fetch_activities([], %{ - exclude_visibilities: "direct", - actor_id: user.ap_id - }) - - assert public_activity in activities - assert unlisted_activity in activities - assert private_activity in activities - refute direct_activity in activities - - activities = - ActivityPub.fetch_activities([], %{ - exclude_visibilities: "unlisted", - actor_id: user.ap_id - }) - - assert public_activity in activities - refute unlisted_activity in activities - assert private_activity in activities - assert direct_activity in activities - - activities = - ActivityPub.fetch_activities([], %{ - exclude_visibilities: "private", - actor_id: user.ap_id - }) - - assert public_activity in activities - assert unlisted_activity in activities - refute private_activity in activities - assert direct_activity in activities - - activities = - ActivityPub.fetch_activities([], %{ - exclude_visibilities: "public", - actor_id: user.ap_id - }) - - refute public_activity in activities - assert unlisted_activity in activities - assert private_activity in activities - assert direct_activity in activities - end - end - - describe "building a user from his ap id" do - test "it returns a user" do - user_id = "http://mastodon.example.org/users/admin" - {: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 - - test "it returns a user that is invisible" do - user_id = "http://mastodon.example.org/users/relay" - {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) - assert User.invisible?(user) - end - - test "it returns a user that accepts chat messages" do - user_id = "http://mastodon.example.org/users/admin" - {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) - - assert user.accepts_chat_messages - end - end - - test "it fetches the appropriate tag-restricted posts" do - user = insert(:user) - - {:ok, status_one} = CommonAPI.post(user, %{status: ". #test"}) - {:ok, status_two} = CommonAPI.post(user, %{status: ". #essais"}) - {:ok, status_three} = CommonAPI.post(user, %{status: ". #test #reject"}) - - fetch_one = ActivityPub.fetch_activities([], %{type: "Create", tag: "test"}) - - fetch_two = ActivityPub.fetch_activities([], %{type: "Create", tag: ["test", "essais"]}) - - fetch_three = - ActivityPub.fetch_activities([], %{ - type: "Create", - tag: ["test", "essais"], - tag_reject: ["reject"] - }) - - fetch_four = - ActivityPub.fetch_activities([], %{ - type: "Create", - tag: ["test"], - tag_all: ["test", "reject"] - }) - - assert fetch_one == [status_one, status_three] - assert fetch_two == [status_one, status_two, status_three] - assert fetch_three == [status_one, status_two] - assert fetch_four == [status_three] - end - - describe "insertion" do - test "drops activities beyond a certain limit" do - limit = Config.get([:instance, :remote_limit]) - - random_text = - :crypto.strong_rand_bytes(limit + 1) - |> Base.encode64() - |> binary_part(0, limit + 1) - - data = %{ - "ok" => true, - "object" => %{ - "content" => random_text - } - } - - assert {:error, :remote_limit} = ActivityPub.insert(data) - end - - test "doesn't drop activities with content being null" do - user = insert(:user) - - data = %{ - "actor" => user.ap_id, - "to" => [], - "object" => %{ - "actor" => user.ap_id, - "to" => [], - "type" => "Note", - "content" => nil - } - } - - assert {:ok, _} = ActivityPub.insert(data) - end - - test "returns the activity if one with the same id is already in" do - activity = insert(:note_activity) - {:ok, new_activity} = ActivityPub.insert(activity.data) - - assert activity.id == new_activity.id - end - - test "inserts a given map into the activity database, giving it an id if it has none." do - user = insert(:user) - - data = %{ - "actor" => user.ap_id, - "to" => [], - "object" => %{ - "actor" => user.ap_id, - "to" => [], - "type" => "Note", - "content" => "hey" - } - } - - {:ok, %Activity{} = activity} = ActivityPub.insert(data) - assert activity.data["ok"] == data["ok"] - assert is_binary(activity.data["id"]) - - given_id = "bla" - - data = %{ - "id" => given_id, - "actor" => user.ap_id, - "to" => [], - "context" => "blabla", - "object" => %{ - "actor" => user.ap_id, - "to" => [], - "type" => "Note", - "content" => "hey" - } - } - - {:ok, %Activity{} = activity} = ActivityPub.insert(data) - 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 - user = insert(:user) - - data = %{ - "actor" => user.ap_id, - "to" => [], - "object" => %{ - "actor" => user.ap_id, - "to" => [], - "type" => "Note", - "content" => "hey" - } - } - - {:ok, %Activity{} = activity} = ActivityPub.insert(data) - object = Pleroma.Object.normalize(activity) - - 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 - user = insert(:user) - - data = %{ - "actor" => user.ap_id, - "to" => [], - "object" => %{ - "actor" => user.ap_id, - "to" => [], - "type" => "Note", - "content" => "hey" - } - } - - {:ok, %Activity{} = activity} = ActivityPub.insert(data) - assert object = Object.normalize(activity) - assert is_binary(object.data["id"]) - end - end - - describe "listen activities" do - test "does not increase user note count" do - user = insert(:user) - - {:ok, activity} = - ActivityPub.listen(%{ - to: ["https://www.w3.org/ns/activitystreams#Public"], - actor: user, - context: "", - object: %{ - "actor" => user.ap_id, - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "artist" => "lain", - "title" => "lain radio episode 1", - "length" => 180_000, - "type" => "Audio" - } - }) - - assert activity.actor == user.ap_id - - user = User.get_cached_by_id(user.id) - assert user.note_count == 0 - end - - test "can be fetched into a timeline" do - _listen_activity_1 = insert(:listen) - _listen_activity_2 = insert(:listen) - _listen_activity_3 = insert(:listen) - - timeline = ActivityPub.fetch_activities([], %{type: ["Listen"]}) - - assert length(timeline) == 3 - end - end - - describe "create activities" do - setup do - [user: insert(:user)] - end - - test "it reverts create", %{user: user} do - with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do - assert {:error, :reverted} = - ActivityPub.create(%{ - to: ["user1", "user2"], - actor: user, - context: "", - object: %{ - "to" => ["user1", "user2"], - "type" => "Note", - "content" => "testing" - } - }) - end - - assert Repo.aggregate(Activity, :count, :id) == 0 - assert Repo.aggregate(Object, :count, :id) == 0 - end - - test "creates activity if expiration is not configured and expires_at is not passed", %{ - user: user - } do - clear_config([Pleroma.Workers.PurgeExpiredActivity, :enabled], false) - - assert {:ok, _} = - ActivityPub.create(%{ - to: ["user1", "user2"], - actor: user, - context: "", - object: %{ - "to" => ["user1", "user2"], - "type" => "Note", - "content" => "testing" - } - }) - end - - test "rejects activity if expires_at present but expiration is not configured", %{user: user} do - clear_config([Pleroma.Workers.PurgeExpiredActivity, :enabled], false) - - assert {:error, :expired_activities_disabled} = - ActivityPub.create(%{ - to: ["user1", "user2"], - actor: user, - context: "", - object: %{ - "to" => ["user1", "user2"], - "type" => "Note", - "content" => "testing" - }, - additional: %{ - "expires_at" => DateTime.utc_now() - } - }) - - assert Repo.aggregate(Activity, :count, :id) == 0 - assert Repo.aggregate(Object, :count, :id) == 0 - end - - test "removes doubled 'to' recipients", %{user: user} do - {:ok, activity} = - ActivityPub.create(%{ - to: ["user1", "user1", "user2"], - actor: user, - context: "", - object: %{ - "to" => ["user1", "user1", "user2"], - "type" => "Note", - "content" => "testing" - } - }) - - assert activity.data["to"] == ["user1", "user2"] - assert activity.actor == user.ap_id - assert activity.recipients == ["user1", "user2", user.ap_id] - end - - test "increases user note count only for public activities", %{user: user} do - {:ok, _} = - CommonAPI.post(User.get_cached_by_id(user.id), %{ - status: "1", - visibility: "public" - }) - - {:ok, _} = - CommonAPI.post(User.get_cached_by_id(user.id), %{ - status: "2", - visibility: "unlisted" - }) - - {:ok, _} = - CommonAPI.post(User.get_cached_by_id(user.id), %{ - status: "2", - visibility: "private" - }) - - {:ok, _} = - CommonAPI.post(User.get_cached_by_id(user.id), %{ - status: "3", - visibility: "direct" - }) - - user = User.get_cached_by_id(user.id) - assert user.note_count == 2 - end - - test "increases replies count", %{user: user} do - user2 = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "1", visibility: "public"}) - ap_id = activity.data["id"] - reply_data = %{status: "1", in_reply_to_status_id: activity.id} - - # public - {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "public")) - assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) - assert object.data["repliesCount"] == 1 - - # unlisted - {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "unlisted")) - assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) - assert object.data["repliesCount"] == 2 - - # private - {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "private")) - assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) - assert object.data["repliesCount"] == 2 - - # direct - {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "direct")) - assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) - assert object.data["repliesCount"] == 2 - end - end - - describe "fetch activities for recipients" do - test "retrieve the activities for certain recipients" do - {:ok, activity_one} = ActivityBuilder.insert(%{"to" => ["someone"]}) - {:ok, activity_two} = ActivityBuilder.insert(%{"to" => ["someone_else"]}) - {:ok, _activity_three} = ActivityBuilder.insert(%{"to" => ["noone"]}) - - activities = ActivityPub.fetch_activities(["someone", "someone_else"]) - assert length(activities) == 2 - assert activities == [activity_one, activity_two] - end - end - - describe "fetch activities in context" do - test "retrieves activities that have a given context" do - {:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"}) - {:ok, activity_two} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"}) - {:ok, _activity_three} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"}) - {:ok, _activity_four} = ActivityBuilder.insert(%{"type" => "Announce", "context" => "2hu"}) - activity_five = insert(:note_activity) - user = insert(:user) - - {:ok, _user_relationship} = User.block(user, %{ap_id: activity_five.data["actor"]}) - - activities = ActivityPub.fetch_activities_for_context("2hu", %{blocking_user: user}) - assert activities == [activity_two, activity] - end - - test "doesn't return activities with filtered words" do - user = insert(:user) - user_two = insert(:user) - insert(:filter, user: user, phrase: "test", hide: true) - - {:ok, %{id: id1, data: %{"context" => context}}} = CommonAPI.post(user, %{status: "1"}) - - {:ok, %{id: id2}} = CommonAPI.post(user_two, %{status: "2", in_reply_to_status_id: id1}) - - {:ok, %{id: id3} = user_activity} = - CommonAPI.post(user, %{status: "3 test?", in_reply_to_status_id: id2}) - - {:ok, %{id: id4} = filtered_activity} = - CommonAPI.post(user_two, %{status: "4 test!", in_reply_to_status_id: id3}) - - {:ok, _} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4}) - - activities = - context - |> ActivityPub.fetch_activities_for_context(%{user: user}) - |> Enum.map(& &1.id) - - assert length(activities) == 4 - assert user_activity.id in activities - refute filtered_activity.id in activities - end - end - - test "doesn't return blocked activities" do - activity_one = insert(:note_activity) - activity_two = insert(:note_activity) - activity_three = insert(:note_activity) - user = insert(:user) - booster = insert(:user) - {:ok, _user_relationship} = User.block(user, %{ap_id: activity_one.data["actor"]}) - - activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) - - assert Enum.member?(activities, activity_two) - assert Enum.member?(activities, activity_three) - refute Enum.member?(activities, activity_one) - - {:ok, _user_block} = User.unblock(user, %{ap_id: activity_one.data["actor"]}) - - activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) - - assert Enum.member?(activities, activity_two) - assert Enum.member?(activities, activity_three) - assert Enum.member?(activities, activity_one) - - {:ok, _user_relationship} = User.block(user, %{ap_id: activity_three.data["actor"]}) - {:ok, %{data: %{"object" => id}}} = CommonAPI.repeat(activity_three.id, booster) - %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) - activity_three = Activity.get_by_id(activity_three.id) - - activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) - - assert Enum.member?(activities, activity_two) - refute Enum.member?(activities, activity_three) - refute Enum.member?(activities, boost_activity) - assert Enum.member?(activities, activity_one) - - activities = ActivityPub.fetch_activities([], %{blocking_user: nil, skip_preload: true}) - - assert Enum.member?(activities, activity_two) - assert Enum.member?(activities, activity_three) - assert Enum.member?(activities, boost_activity) - assert Enum.member?(activities, activity_one) - end - - test "doesn't return transitive interactions concerning blocked users" do - blocker = insert(:user) - blockee = insert(:user) - friend = insert(:user) - - {:ok, _user_relationship} = User.block(blocker, blockee) - - {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"}) - - {:ok, activity_two} = CommonAPI.post(friend, %{status: "hey! @#{blockee.nickname}"}) - - {:ok, activity_three} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) - - {:ok, activity_four} = CommonAPI.post(blockee, %{status: "hey! @#{blocker.nickname}"}) - - activities = ActivityPub.fetch_activities([], %{blocking_user: blocker}) - - assert Enum.member?(activities, activity_one) - refute Enum.member?(activities, activity_two) - refute Enum.member?(activities, activity_three) - refute Enum.member?(activities, activity_four) - end - - test "doesn't return announce activities with blocked users in 'to'" do - blocker = insert(:user) - blockee = insert(:user) - friend = insert(:user) - - {:ok, _user_relationship} = User.block(blocker, blockee) - - {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"}) - - {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) - - {:ok, activity_three} = CommonAPI.repeat(activity_two.id, friend) - - activities = - ActivityPub.fetch_activities([], %{blocking_user: blocker}) - |> Enum.map(fn act -> act.id end) - - assert Enum.member?(activities, activity_one.id) - refute Enum.member?(activities, activity_two.id) - refute Enum.member?(activities, activity_three.id) - end - - test "doesn't return announce activities with blocked users in 'cc'" do - blocker = insert(:user) - blockee = insert(:user) - friend = insert(:user) - - {:ok, _user_relationship} = User.block(blocker, blockee) - - {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"}) - - {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) - - assert object = Pleroma.Object.normalize(activity_two) - - data = %{ - "actor" => friend.ap_id, - "object" => object.data["id"], - "context" => object.data["context"], - "type" => "Announce", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [blockee.ap_id] - } - - assert {:ok, activity_three} = ActivityPub.insert(data) - - activities = - ActivityPub.fetch_activities([], %{blocking_user: blocker}) - |> Enum.map(fn act -> act.id end) - - assert Enum.member?(activities, activity_one.id) - refute Enum.member?(activities, activity_two.id) - refute Enum.member?(activities, activity_three.id) - end - - test "doesn't return activities from blocked domains" do - domain = "dogwhistle.zone" - domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"}) - note = insert(:note, %{data: %{"actor" => domain_user.ap_id}}) - activity = insert(:note_activity, %{note: note}) - user = insert(:user) - {:ok, user} = User.block_domain(user, domain) - - activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) - - refute activity in activities - - followed_user = insert(:user) - CommonAPI.follow(user, followed_user) - {:ok, repeat_activity} = CommonAPI.repeat(activity.id, followed_user) - - activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) - - refute repeat_activity in activities - end - - test "does return activities from followed users on blocked domains" do - domain = "meanies.social" - domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"}) - blocker = insert(:user) - - {:ok, blocker} = User.follow(blocker, domain_user) - {:ok, blocker} = User.block_domain(blocker, domain) - - assert User.following?(blocker, domain_user) - assert User.blocks_domain?(blocker, domain_user) - refute User.blocks?(blocker, domain_user) - - note = insert(:note, %{data: %{"actor" => domain_user.ap_id}}) - activity = insert(:note_activity, %{note: note}) - - activities = ActivityPub.fetch_activities([], %{blocking_user: blocker, skip_preload: true}) - - assert activity in activities - - # And check that if the guy we DO follow boosts someone else from their domain, - # that should be hidden - another_user = insert(:user, %{ap_id: "https://#{domain}/@meanie2"}) - bad_note = insert(:note, %{data: %{"actor" => another_user.ap_id}}) - bad_activity = insert(:note_activity, %{note: bad_note}) - {:ok, repeat_activity} = CommonAPI.repeat(bad_activity.id, domain_user) - - activities = ActivityPub.fetch_activities([], %{blocking_user: blocker, skip_preload: true}) - - refute repeat_activity in activities - end - - test "doesn't return muted activities" do - activity_one = insert(:note_activity) - activity_two = insert(:note_activity) - activity_three = insert(:note_activity) - user = insert(:user) - booster = insert(:user) - - activity_one_actor = User.get_by_ap_id(activity_one.data["actor"]) - {:ok, _user_relationships} = User.mute(user, activity_one_actor) - - activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) - - assert Enum.member?(activities, activity_two) - assert Enum.member?(activities, activity_three) - refute Enum.member?(activities, activity_one) - - # Calling with 'with_muted' will deliver muted activities, too. - activities = - ActivityPub.fetch_activities([], %{ - muting_user: user, - with_muted: true, - skip_preload: true - }) - - assert Enum.member?(activities, activity_two) - assert Enum.member?(activities, activity_three) - assert Enum.member?(activities, activity_one) - - {:ok, _user_mute} = User.unmute(user, activity_one_actor) - - activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) - - assert Enum.member?(activities, activity_two) - assert Enum.member?(activities, activity_three) - assert Enum.member?(activities, activity_one) - - activity_three_actor = User.get_by_ap_id(activity_three.data["actor"]) - {:ok, _user_relationships} = User.mute(user, activity_three_actor) - {:ok, %{data: %{"object" => id}}} = CommonAPI.repeat(activity_three.id, booster) - %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) - activity_three = Activity.get_by_id(activity_three.id) - - activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) - - assert Enum.member?(activities, activity_two) - refute Enum.member?(activities, activity_three) - refute Enum.member?(activities, boost_activity) - assert Enum.member?(activities, activity_one) - - activities = ActivityPub.fetch_activities([], %{muting_user: nil, skip_preload: true}) - - assert Enum.member?(activities, activity_two) - assert Enum.member?(activities, activity_three) - assert Enum.member?(activities, boost_activity) - assert Enum.member?(activities, activity_one) - end - - test "doesn't return thread muted activities" do - user = insert(:user) - _activity_one = insert(:note_activity) - note_two = insert(:note, data: %{"context" => "suya.."}) - activity_two = insert(:note_activity, note: note_two) - - {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two) - - assert [_activity_one] = ActivityPub.fetch_activities([], %{muting_user: user}) - end - - test "returns thread muted activities when with_muted is set" do - user = insert(:user) - _activity_one = insert(:note_activity) - note_two = insert(:note, data: %{"context" => "suya.."}) - activity_two = insert(:note_activity, note: note_two) - - {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two) - - assert [_activity_two, _activity_one] = - ActivityPub.fetch_activities([], %{muting_user: user, with_muted: true}) - end - - test "does include announces on request" do - activity_three = insert(:note_activity) - user = insert(:user) - booster = insert(:user) - - {:ok, user} = User.follow(user, booster) - - {:ok, announce} = CommonAPI.repeat(activity_three.id, booster) - - [announce_activity] = ActivityPub.fetch_activities([user.ap_id | User.following(user)]) - - assert announce_activity.id == announce.id - end - - test "excludes reblogs on request" do - user = insert(:user) - {:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user}) - {:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user}) - - [activity] = ActivityPub.fetch_user_activities(user, nil, %{exclude_reblogs: true}) - - assert activity == expected_activity - end - - describe "irreversible filters" do - setup do - user = insert(:user) - user_two = insert(:user) - - insert(:filter, user: user_two, phrase: "cofe", hide: true) - insert(:filter, user: user_two, phrase: "ok boomer", hide: true) - insert(:filter, user: user_two, phrase: "test", hide: false) - - params = %{ - type: ["Create", "Announce"], - user: user_two - } - - {:ok, %{user: user, user_two: user_two, params: params}} - end - - test "it returns statuses if they don't contain exact filter words", %{ - user: user, - params: params - } do - {:ok, _} = CommonAPI.post(user, %{status: "hey"}) - {:ok, _} = CommonAPI.post(user, %{status: "got cofefe?"}) - {:ok, _} = CommonAPI.post(user, %{status: "I am not a boomer"}) - {:ok, _} = CommonAPI.post(user, %{status: "ok boomers"}) - {:ok, _} = CommonAPI.post(user, %{status: "ccofee is not a word"}) - {:ok, _} = CommonAPI.post(user, %{status: "this is a test"}) - - activities = ActivityPub.fetch_activities([], params) - - assert Enum.count(activities) == 6 - end - - test "it does not filter user's own statuses", %{user_two: user_two, params: params} do - {:ok, _} = CommonAPI.post(user_two, %{status: "Give me some cofe!"}) - {:ok, _} = CommonAPI.post(user_two, %{status: "ok boomer"}) - - activities = ActivityPub.fetch_activities([], params) - - assert Enum.count(activities) == 2 - end - - test "it excludes statuses with filter words", %{user: user, params: params} do - {:ok, _} = CommonAPI.post(user, %{status: "Give me some cofe!"}) - {:ok, _} = CommonAPI.post(user, %{status: "ok boomer"}) - {:ok, _} = CommonAPI.post(user, %{status: "is it a cOfE?"}) - {:ok, _} = CommonAPI.post(user, %{status: "cofe is all I need"}) - {:ok, _} = CommonAPI.post(user, %{status: "— ok BOOMER\n"}) - - activities = ActivityPub.fetch_activities([], params) - - assert Enum.empty?(activities) - end - - test "it returns all statuses if user does not have any filters" do - another_user = insert(:user) - {:ok, _} = CommonAPI.post(another_user, %{status: "got cofe?"}) - {:ok, _} = CommonAPI.post(another_user, %{status: "test!"}) - - activities = - ActivityPub.fetch_activities([], %{ - type: ["Create", "Announce"], - user: another_user - }) - - assert Enum.count(activities) == 2 - end - end - - describe "public fetch activities" do - test "doesn't retrieve unlisted activities" do - user = insert(:user) - - {:ok, _unlisted_activity} = CommonAPI.post(user, %{status: "yeah", visibility: "unlisted"}) - - {:ok, listed_activity} = CommonAPI.post(user, %{status: "yeah"}) - - [activity] = ActivityPub.fetch_public_activities() - - assert activity == listed_activity - end - - test "retrieves public activities" do - _activities = ActivityPub.fetch_public_activities() - - %{public: public} = ActivityBuilder.public_and_non_public() - - activities = ActivityPub.fetch_public_activities() - assert length(activities) == 1 - assert Enum.at(activities, 0) == public - end - - test "retrieves a maximum of 20 activities" do - ActivityBuilder.insert_list(10) - expected_activities = ActivityBuilder.insert_list(20) - - activities = ActivityPub.fetch_public_activities() - - assert collect_ids(activities) == collect_ids(expected_activities) - assert length(activities) == 20 - end - - test "retrieves ids starting from a since_id" do - activities = ActivityBuilder.insert_list(30) - expected_activities = ActivityBuilder.insert_list(10) - since_id = List.last(activities).id - - activities = ActivityPub.fetch_public_activities(%{since_id: since_id}) - - assert collect_ids(activities) == collect_ids(expected_activities) - assert length(activities) == 10 - end - - test "retrieves ids up to max_id" do - ActivityBuilder.insert_list(10) - expected_activities = ActivityBuilder.insert_list(20) - - %{id: max_id} = - 10 - |> ActivityBuilder.insert_list() - |> List.first() - - activities = ActivityPub.fetch_public_activities(%{max_id: max_id}) - - assert length(activities) == 20 - assert collect_ids(activities) == collect_ids(expected_activities) - end - - test "paginates via offset/limit" do - _first_part_activities = ActivityBuilder.insert_list(10) - second_part_activities = ActivityBuilder.insert_list(10) - - later_activities = ActivityBuilder.insert_list(10) - - activities = ActivityPub.fetch_public_activities(%{page: "2", page_size: "20"}, :offset) - - assert length(activities) == 20 - - assert collect_ids(activities) == - collect_ids(second_part_activities) ++ collect_ids(later_activities) - end - - test "doesn't return reblogs for users for whom reblogs have been muted" do - activity = insert(:note_activity) - user = insert(:user) - booster = insert(:user) - {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster) - - {:ok, activity} = CommonAPI.repeat(activity.id, booster) - - activities = ActivityPub.fetch_activities([], %{muting_user: user}) - - refute Enum.any?(activities, fn %{id: id} -> id == activity.id end) - end - - test "returns reblogs for users for whom reblogs have not been muted" do - activity = insert(:note_activity) - user = insert(:user) - booster = insert(:user) - {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster) - {:ok, _reblog_mute} = CommonAPI.show_reblogs(user, booster) - - {:ok, activity} = CommonAPI.repeat(activity.id, booster) - - activities = ActivityPub.fetch_activities([], %{muting_user: user}) - - assert Enum.any?(activities, fn %{id: id} -> id == activity.id end) - end - end - - describe "uploading files" do - setup do - test_file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - %{test_file: test_file} - 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" - end - - test "it sets the default description depending on the configuration", %{test_file: file} do - clear_config([Pleroma.Upload, :default_description]) - - Pleroma.Config.put([Pleroma.Upload, :default_description], nil) - {:ok, %Object{} = object} = ActivityPub.upload(file) - assert object.data["name"] == "" - - Pleroma.Config.put([Pleroma.Upload, :default_description], :filename) - {:ok, %Object{} = object} = ActivityPub.upload(file) - assert object.data["name"] == "an_image.jpg" - - Pleroma.Config.put([Pleroma.Upload, :default_description], "unnamed attachment") - {:ok, %Object{} = object} = ActivityPub.upload(file) - assert object.data["name"] == "unnamed attachment" - end - - test "copies the file to the configured folder", %{test_file: file} do - clear_config([Pleroma.Upload, :default_description], :filename) - {:ok, %Object{} = object} = ActivityPub.upload(file) - assert object.data["name"] == "an_image.jpg" - end - - test "works with base64 encoded images" do - file = %{ - img: data_uri() - } - - {:ok, %Object{}} = ActivityPub.upload(file) - end - end - - describe "fetch the latest Follow" do - test "fetches the latest Follow activity" do - %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity) - follower = Repo.get_by(User, ap_id: activity.data["actor"]) - followed = Repo.get_by(User, ap_id: activity.data["object"]) - - assert activity == Utils.fetch_latest_follow(follower, followed) - end - end - - describe "unfollowing" do - test "it reverts unfollow activity" do - follower = insert(:user) - followed = insert(:user) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) - - with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do - assert {:error, :reverted} = ActivityPub.unfollow(follower, followed) - end - - activity = Activity.get_by_id(follow_activity.id) - assert activity.data["type"] == "Follow" - assert activity.data["actor"] == follower.ap_id - - assert activity.data["object"] == followed.ap_id - end - - test "creates an undo activity for the last follow" do - follower = insert(:user) - followed = insert(:user) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) - {:ok, activity} = ActivityPub.unfollow(follower, followed) - - assert activity.data["type"] == "Undo" - assert activity.data["actor"] == follower.ap_id - - embedded_object = activity.data["object"] - assert is_map(embedded_object) - assert embedded_object["type"] == "Follow" - assert embedded_object["object"] == followed.ap_id - assert embedded_object["id"] == follow_activity.data["id"] - end - - test "creates an undo activity for a pending follow request" do - follower = insert(:user) - followed = insert(:user, %{locked: true}) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) - {:ok, activity} = ActivityPub.unfollow(follower, followed) - - assert activity.data["type"] == "Undo" - assert activity.data["actor"] == follower.ap_id - - embedded_object = activity.data["object"] - assert is_map(embedded_object) - assert embedded_object["type"] == "Follow" - assert embedded_object["object"] == followed.ap_id - assert embedded_object["id"] == follow_activity.data["id"] - end - end - - describe "timeline post-processing" do - test "it filters broken threads" do - user1 = insert(:user) - user2 = insert(:user) - user3 = insert(:user) - - {:ok, user1} = User.follow(user1, user3) - assert User.following?(user1, user3) - - {:ok, user2} = User.follow(user2, user3) - assert User.following?(user2, user3) - - {:ok, user3} = User.follow(user3, user2) - assert User.following?(user3, user2) - - {:ok, public_activity} = CommonAPI.post(user3, %{status: "hi 1"}) - - {:ok, private_activity_1} = CommonAPI.post(user3, %{status: "hi 2", visibility: "private"}) - - {:ok, private_activity_2} = - CommonAPI.post(user2, %{ - status: "hi 3", - visibility: "private", - in_reply_to_status_id: private_activity_1.id - }) - - {:ok, private_activity_3} = - CommonAPI.post(user3, %{ - status: "hi 4", - visibility: "private", - in_reply_to_status_id: private_activity_2.id - }) - - activities = - ActivityPub.fetch_activities([user1.ap_id | User.following(user1)]) - |> Enum.map(fn a -> a.id end) - - private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"]) - - assert [public_activity.id, private_activity_1.id, private_activity_3.id] == activities - - assert length(activities) == 3 - - activities = - ActivityPub.fetch_activities([user1.ap_id | User.following(user1)], %{user: user1}) - |> Enum.map(fn a -> a.id end) - - assert [public_activity.id, private_activity_1.id] == activities - assert length(activities) == 2 - end - end - - describe "flag/1" do - setup do - reporter = insert(:user) - target_account = insert(:user) - content = "foobar" - {:ok, activity} = CommonAPI.post(target_account, %{status: content}) - context = Utils.generate_context_id() - - reporter_ap_id = reporter.ap_id - target_ap_id = target_account.ap_id - activity_ap_id = activity.data["id"] - - activity_with_object = Activity.get_by_ap_id_with_object(activity_ap_id) - - {:ok, - %{ - reporter: reporter, - context: context, - target_account: target_account, - reported_activity: activity, - content: content, - activity_ap_id: activity_ap_id, - activity_with_object: activity_with_object, - reporter_ap_id: reporter_ap_id, - target_ap_id: target_ap_id - }} - end - - test "it can create a Flag activity", - %{ - reporter: reporter, - context: context, - target_account: target_account, - reported_activity: reported_activity, - content: content, - activity_ap_id: activity_ap_id, - activity_with_object: activity_with_object, - reporter_ap_id: reporter_ap_id, - target_ap_id: target_ap_id - } do - assert {:ok, activity} = - ActivityPub.flag(%{ - actor: reporter, - context: context, - account: target_account, - statuses: [reported_activity], - content: content - }) - - note_obj = %{ - "type" => "Note", - "id" => activity_ap_id, - "content" => content, - "published" => activity_with_object.object.data["published"], - "actor" => - AccountView.render("show.json", %{user: target_account, skip_visibility_check: true}) - } - - assert %Activity{ - actor: ^reporter_ap_id, - data: %{ - "type" => "Flag", - "content" => ^content, - "context" => ^context, - "object" => [^target_ap_id, ^note_obj] - } - } = activity - end - - test_with_mock "strips status data from Flag, before federating it", - %{ - reporter: reporter, - context: context, - target_account: target_account, - reported_activity: reported_activity, - content: content - }, - Utils, - [:passthrough], - [] do - {:ok, activity} = - ActivityPub.flag(%{ - actor: reporter, - context: context, - account: target_account, - statuses: [reported_activity], - content: content - }) - - new_data = - put_in(activity.data, ["object"], [target_account.ap_id, reported_activity.data["id"]]) - - assert_called(Utils.maybe_federate(%{activity | data: new_data})) - end - end - - test "fetch_activities/2 returns activities addressed to a list " do - user = insert(:user) - member = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) - {:ok, list} = Pleroma.List.follow(list, member) - - {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) - - activity = Repo.preload(activity, :bookmark) - activity = %Activity{activity | thread_muted?: !!activity.thread_muted?} - - assert ActivityPub.fetch_activities([], %{user: user}) == [activity] - end - - def data_uri do - File.read!("test/fixtures/avatar_data_uri") - end - - describe "fetch_activities_bounded" do - test "fetches private posts for followed users" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "thought I looked cute might delete later :3", - visibility: "private" - }) - - [result] = ActivityPub.fetch_activities_bounded([user.follower_address], []) - assert result.id == activity.id - end - - test "fetches only public posts for other users" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe", visibility: "public"}) - - {:ok, _private_activity} = - CommonAPI.post(user, %{ - status: "why is tenshi eating a corndog so cute?", - visibility: "private" - }) - - [result] = ActivityPub.fetch_activities_bounded([], [user.follower_address]) - assert result.id == activity.id - end - end - - describe "fetch_follow_information_for_user" do - test "syncronizes following/followers counters" do - user = - insert(:user, - local: false, - follower_address: "http://localhost:4001/users/fuser2/followers", - following_address: "http://localhost:4001/users/fuser2/following" - ) - - {:ok, info} = ActivityPub.fetch_follow_information_for_user(user) - assert info.follower_count == 527 - assert info.following_count == 267 - end - - test "detects hidden followers" do - mock(fn env -> - case env.url do - "http://localhost:4001/users/masto_closed/followers?page=1" -> - %Tesla.Env{status: 403, body: ""} - - _ -> - apply(HttpRequestMock, :request, [env]) - end - end) - - user = - insert(:user, - local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following" - ) - - {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) - assert follow_info.hide_followers == true - assert follow_info.hide_follows == false - end - - test "detects hidden follows" do - mock(fn env -> - case env.url do - "http://localhost:4001/users/masto_closed/following?page=1" -> - %Tesla.Env{status: 403, body: ""} - - _ -> - apply(HttpRequestMock, :request, [env]) - end - end) - - user = - insert(:user, - local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following" - ) - - {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) - assert follow_info.hide_followers == false - assert follow_info.hide_follows == true - end - - test "detects hidden follows/followers for friendica" do - user = - insert(:user, - local: false, - follower_address: "http://localhost:8080/followers/fuser3", - following_address: "http://localhost:8080/following/fuser3" - ) - - {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) - assert follow_info.hide_followers == true - assert follow_info.follower_count == 296 - assert follow_info.following_count == 32 - assert follow_info.hide_follows == true - end - - test "doesn't crash when follower and following counters are hidden" do - mock(fn env -> - case env.url do - "http://localhost:4001/users/masto_hidden_counters/following" -> - json(%{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "http://localhost:4001/users/masto_hidden_counters/followers" - }) - - "http://localhost:4001/users/masto_hidden_counters/following?page=1" -> - %Tesla.Env{status: 403, body: ""} - - "http://localhost:4001/users/masto_hidden_counters/followers" -> - json(%{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "http://localhost:4001/users/masto_hidden_counters/following" - }) - - "http://localhost:4001/users/masto_hidden_counters/followers?page=1" -> - %Tesla.Env{status: 403, body: ""} - end - end) - - user = - insert(:user, - local: false, - follower_address: "http://localhost:4001/users/masto_hidden_counters/followers", - following_address: "http://localhost:4001/users/masto_hidden_counters/following" - ) - - {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) - - assert follow_info.hide_followers == true - assert follow_info.follower_count == 0 - assert follow_info.hide_follows == true - assert follow_info.following_count == 0 - end - end - - describe "fetch_favourites/3" do - test "returns a favourite activities sorted by adds to favorite" do - user = insert(:user) - other_user = insert(:user) - user1 = insert(:user) - user2 = insert(:user) - {:ok, a1} = CommonAPI.post(user1, %{status: "bla"}) - {:ok, _a2} = CommonAPI.post(user2, %{status: "traps are happy"}) - {:ok, a3} = CommonAPI.post(user2, %{status: "Trees Are "}) - {:ok, a4} = CommonAPI.post(user2, %{status: "Agent Smith "}) - {:ok, a5} = CommonAPI.post(user1, %{status: "Red or Blue "}) - - {:ok, _} = CommonAPI.favorite(user, a4.id) - {:ok, _} = CommonAPI.favorite(other_user, a3.id) - {:ok, _} = CommonAPI.favorite(user, a3.id) - {:ok, _} = CommonAPI.favorite(other_user, a5.id) - {:ok, _} = CommonAPI.favorite(user, a5.id) - {:ok, _} = CommonAPI.favorite(other_user, a4.id) - {:ok, _} = CommonAPI.favorite(user, a1.id) - {:ok, _} = CommonAPI.favorite(other_user, a1.id) - result = ActivityPub.fetch_favourites(user) - - assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id] - - result = ActivityPub.fetch_favourites(user, %{limit: 2}) - assert Enum.map(result, & &1.id) == [a1.id, a5.id] - end - end - - describe "Move activity" do - test "create" 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 = insert(:user) - follower_move_opted_out = insert(:user, allow_following_move: false) - - User.follow(follower, old_user) - User.follow(follower_move_opted_out, old_user) - - assert User.following?(follower, old_user) - assert User.following?(follower_move_opted_out, 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}) - - refute User.following?(follower, old_user) - assert User.following?(follower, new_user) - - assert User.following?(follower_move_opted_out, old_user) - refute User.following?(follower_move_opted_out, new_user) - - activity = %Activity{activity | object: nil} - - assert [%Notification{activity: ^activity}] = Notification.for_user(follower) - - assert [%Notification{activity: ^activity}] = Notification.for_user(follower_move_opted_out) - end - - test "old user must be in the new user's `also_known_as` list" do - old_user = insert(:user) - new_user = insert(:user) - - assert {:error, "Target account must have the origin in `alsoKnownAs`"} = - ActivityPub.move(old_user, new_user) - end - end - - test "doesn't retrieve replies activities with exclude_replies" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "yeah"}) - - {:ok, _reply} = CommonAPI.post(user, %{status: "yeah", in_reply_to_status_id: activity.id}) - - [result] = ActivityPub.fetch_public_activities(%{exclude_replies: true}) - - assert result.id == activity.id - - assert length(ActivityPub.fetch_public_activities()) == 2 - end - - describe "replies filtering with public messages" do - setup :public_messages - - test "public timeline", %{users: %{u1: user}} do - activities_ids = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:local_only, false) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:reply_filtering_user, user) - |> ActivityPub.fetch_public_activities() - |> Enum.map(& &1.id) - - assert length(activities_ids) == 16 - end - - test "public timeline with reply_visibility `following`", %{ - users: %{u1: user}, - u1: u1, - u2: u2, - u3: u3, - u4: u4, - activities: activities - } do - activities_ids = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:local_only, false) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:reply_visibility, "following") - |> Map.put(:reply_filtering_user, user) - |> ActivityPub.fetch_public_activities() - |> Enum.map(& &1.id) - - assert length(activities_ids) == 14 - - visible_ids = - Map.values(u1) ++ Map.values(u2) ++ Map.values(u4) ++ Map.values(activities) ++ [u3[:r1]] - - assert Enum.all?(visible_ids, &(&1 in activities_ids)) - end - - test "public timeline with reply_visibility `self`", %{ - users: %{u1: user}, - u1: u1, - u2: u2, - u3: u3, - u4: u4, - activities: activities - } do - activities_ids = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:local_only, false) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:reply_visibility, "self") - |> Map.put(:reply_filtering_user, user) - |> ActivityPub.fetch_public_activities() - |> Enum.map(& &1.id) - - assert length(activities_ids) == 10 - visible_ids = Map.values(u1) ++ [u2[:r1], u3[:r1], u4[:r1]] ++ Map.values(activities) - assert Enum.all?(visible_ids, &(&1 in activities_ids)) - end - - test "home timeline", %{ - users: %{u1: user}, - activities: activities, - u1: u1, - u2: u2, - u3: u3, - u4: u4 - } do - params = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:user, user) - |> Map.put(:reply_filtering_user, user) - - activities_ids = - ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) - |> Enum.map(& &1.id) - - assert length(activities_ids) == 13 - - visible_ids = - Map.values(u1) ++ - Map.values(u3) ++ - [ - activities[:a1], - activities[:a2], - activities[:a4], - u2[:r1], - u2[:r3], - u4[:r1], - u4[:r2] - ] - - assert Enum.all?(visible_ids, &(&1 in activities_ids)) - end - - test "home timeline with reply_visibility `following`", %{ - users: %{u1: user}, - activities: activities, - u1: u1, - u2: u2, - u3: u3, - u4: u4 - } do - params = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:user, user) - |> Map.put(:reply_visibility, "following") - |> Map.put(:reply_filtering_user, user) - - activities_ids = - ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) - |> Enum.map(& &1.id) - - assert length(activities_ids) == 11 - - visible_ids = - Map.values(u1) ++ - [ - activities[:a1], - activities[:a2], - activities[:a4], - u2[:r1], - u2[:r3], - u3[:r1], - u4[:r1], - u4[:r2] - ] - - assert Enum.all?(visible_ids, &(&1 in activities_ids)) - end - - test "home timeline with reply_visibility `self`", %{ - users: %{u1: user}, - activities: activities, - u1: u1, - u2: u2, - u3: u3, - u4: u4 - } do - params = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:user, user) - |> Map.put(:reply_visibility, "self") - |> Map.put(:reply_filtering_user, user) - - activities_ids = - ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) - |> Enum.map(& &1.id) - - assert length(activities_ids) == 9 - - visible_ids = - Map.values(u1) ++ - [ - activities[:a1], - activities[:a2], - activities[:a4], - u2[:r1], - u3[:r1], - u4[:r1] - ] - - assert Enum.all?(visible_ids, &(&1 in activities_ids)) - end - - test "filtering out announces where the user is the actor of the announced message" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - User.follow(user, other_user) - - {:ok, post} = CommonAPI.post(user, %{status: "yo"}) - {:ok, other_post} = CommonAPI.post(third_user, %{status: "yo"}) - {:ok, _announce} = CommonAPI.repeat(post.id, other_user) - {:ok, _announce} = CommonAPI.repeat(post.id, third_user) - {:ok, announce} = CommonAPI.repeat(other_post.id, other_user) - - params = %{ - type: ["Announce"] - } - - results = - [user.ap_id | User.following(user)] - |> ActivityPub.fetch_activities(params) - - assert length(results) == 3 - - params = %{ - type: ["Announce"], - announce_filtering_user: user - } - - [result] = - [user.ap_id | User.following(user)] - |> ActivityPub.fetch_activities(params) - - assert result.id == announce.id - end - end - - describe "replies filtering with private messages" do - setup :private_messages - - test "public timeline", %{users: %{u1: user}} do - activities_ids = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:local_only, false) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:user, user) - |> ActivityPub.fetch_public_activities() - |> Enum.map(& &1.id) - - assert activities_ids == [] - end - - test "public timeline with default reply_visibility `following`", %{users: %{u1: user}} do - activities_ids = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:local_only, false) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:reply_visibility, "following") - |> Map.put(:reply_filtering_user, user) - |> Map.put(:user, user) - |> ActivityPub.fetch_public_activities() - |> Enum.map(& &1.id) - - assert activities_ids == [] - end - - test "public timeline with default reply_visibility `self`", %{users: %{u1: user}} do - activities_ids = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:local_only, false) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:reply_visibility, "self") - |> Map.put(:reply_filtering_user, user) - |> Map.put(:user, user) - |> ActivityPub.fetch_public_activities() - |> Enum.map(& &1.id) - - assert activities_ids == [] - - activities_ids = - %{} - |> Map.put(:reply_visibility, "self") - |> Map.put(:reply_filtering_user, nil) - |> ActivityPub.fetch_public_activities() - - assert activities_ids == [] - end - - test "home timeline", %{users: %{u1: user}} do - params = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:user, user) - - activities_ids = - ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) - |> Enum.map(& &1.id) - - assert length(activities_ids) == 12 - end - - test "home timeline with default reply_visibility `following`", %{users: %{u1: user}} do - params = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:user, user) - |> Map.put(:reply_visibility, "following") - |> Map.put(:reply_filtering_user, user) - - activities_ids = - ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) - |> Enum.map(& &1.id) - - assert length(activities_ids) == 12 - end - - test "home timeline with default reply_visibility `self`", %{ - users: %{u1: user}, - activities: activities, - u1: u1, - u2: u2, - u3: u3, - u4: u4 - } do - params = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:user, user) - |> Map.put(:reply_visibility, "self") - |> Map.put(:reply_filtering_user, user) - - activities_ids = - ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) - |> Enum.map(& &1.id) - - assert length(activities_ids) == 10 - - visible_ids = - Map.values(u1) ++ Map.values(u4) ++ [u2[:r1], u3[:r1]] ++ Map.values(activities) - - assert Enum.all?(visible_ids, &(&1 in activities_ids)) - end - end - - defp public_messages(_) do - [u1, u2, u3, u4] = insert_list(4, :user) - {:ok, u1} = User.follow(u1, u2) - {:ok, u2} = User.follow(u2, u1) - {:ok, u1} = User.follow(u1, u4) - {:ok, u4} = User.follow(u4, u1) - - {:ok, u2} = User.follow(u2, u3) - {:ok, u3} = User.follow(u3, u2) - - {:ok, a1} = CommonAPI.post(u1, %{status: "Status"}) - - {:ok, r1_1} = - CommonAPI.post(u2, %{ - status: "@#{u1.nickname} reply from u2 to u1", - in_reply_to_status_id: a1.id - }) - - {:ok, r1_2} = - CommonAPI.post(u3, %{ - status: "@#{u1.nickname} reply from u3 to u1", - in_reply_to_status_id: a1.id - }) - - {:ok, r1_3} = - CommonAPI.post(u4, %{ - status: "@#{u1.nickname} reply from u4 to u1", - in_reply_to_status_id: a1.id - }) - - {:ok, a2} = CommonAPI.post(u2, %{status: "Status"}) - - {:ok, r2_1} = - CommonAPI.post(u1, %{ - status: "@#{u2.nickname} reply from u1 to u2", - in_reply_to_status_id: a2.id - }) - - {:ok, r2_2} = - CommonAPI.post(u3, %{ - status: "@#{u2.nickname} reply from u3 to u2", - in_reply_to_status_id: a2.id - }) - - {:ok, r2_3} = - CommonAPI.post(u4, %{ - status: "@#{u2.nickname} reply from u4 to u2", - in_reply_to_status_id: a2.id - }) - - {:ok, a3} = CommonAPI.post(u3, %{status: "Status"}) - - {:ok, r3_1} = - CommonAPI.post(u1, %{ - status: "@#{u3.nickname} reply from u1 to u3", - in_reply_to_status_id: a3.id - }) - - {:ok, r3_2} = - CommonAPI.post(u2, %{ - status: "@#{u3.nickname} reply from u2 to u3", - in_reply_to_status_id: a3.id - }) - - {:ok, r3_3} = - CommonAPI.post(u4, %{ - status: "@#{u3.nickname} reply from u4 to u3", - in_reply_to_status_id: a3.id - }) - - {:ok, a4} = CommonAPI.post(u4, %{status: "Status"}) - - {:ok, r4_1} = - CommonAPI.post(u1, %{ - status: "@#{u4.nickname} reply from u1 to u4", - in_reply_to_status_id: a4.id - }) - - {:ok, r4_2} = - CommonAPI.post(u2, %{ - status: "@#{u4.nickname} reply from u2 to u4", - in_reply_to_status_id: a4.id - }) - - {:ok, r4_3} = - CommonAPI.post(u3, %{ - status: "@#{u4.nickname} reply from u3 to u4", - in_reply_to_status_id: a4.id - }) - - {:ok, - users: %{u1: u1, u2: u2, u3: u3, u4: u4}, - activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id}, - u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id}, - u2: %{r1: r2_1.id, r2: r2_2.id, r3: r2_3.id}, - u3: %{r1: r3_1.id, r2: r3_2.id, r3: r3_3.id}, - u4: %{r1: r4_1.id, r2: r4_2.id, r3: r4_3.id}} - end - - defp private_messages(_) do - [u1, u2, u3, u4] = insert_list(4, :user) - {:ok, u1} = User.follow(u1, u2) - {:ok, u2} = User.follow(u2, u1) - {:ok, u1} = User.follow(u1, u3) - {:ok, u3} = User.follow(u3, u1) - {:ok, u1} = User.follow(u1, u4) - {:ok, u4} = User.follow(u4, u1) - - {:ok, u2} = User.follow(u2, u3) - {:ok, u3} = User.follow(u3, u2) - - {:ok, a1} = CommonAPI.post(u1, %{status: "Status", visibility: "private"}) - - {:ok, r1_1} = - CommonAPI.post(u2, %{ - status: "@#{u1.nickname} reply from u2 to u1", - in_reply_to_status_id: a1.id, - visibility: "private" - }) - - {:ok, r1_2} = - CommonAPI.post(u3, %{ - status: "@#{u1.nickname} reply from u3 to u1", - in_reply_to_status_id: a1.id, - visibility: "private" - }) - - {:ok, r1_3} = - CommonAPI.post(u4, %{ - status: "@#{u1.nickname} reply from u4 to u1", - in_reply_to_status_id: a1.id, - visibility: "private" - }) - - {:ok, a2} = CommonAPI.post(u2, %{status: "Status", visibility: "private"}) - - {:ok, r2_1} = - CommonAPI.post(u1, %{ - status: "@#{u2.nickname} reply from u1 to u2", - in_reply_to_status_id: a2.id, - visibility: "private" - }) - - {:ok, r2_2} = - CommonAPI.post(u3, %{ - status: "@#{u2.nickname} reply from u3 to u2", - in_reply_to_status_id: a2.id, - visibility: "private" - }) - - {:ok, a3} = CommonAPI.post(u3, %{status: "Status", visibility: "private"}) - - {:ok, r3_1} = - CommonAPI.post(u1, %{ - status: "@#{u3.nickname} reply from u1 to u3", - in_reply_to_status_id: a3.id, - visibility: "private" - }) - - {:ok, r3_2} = - CommonAPI.post(u2, %{ - status: "@#{u3.nickname} reply from u2 to u3", - in_reply_to_status_id: a3.id, - visibility: "private" - }) - - {:ok, a4} = CommonAPI.post(u4, %{status: "Status", visibility: "private"}) - - {:ok, r4_1} = - CommonAPI.post(u1, %{ - status: "@#{u4.nickname} reply from u1 to u4", - in_reply_to_status_id: a4.id, - visibility: "private" - }) - - {:ok, - users: %{u1: u1, u2: u2, u3: u3, u4: u4}, - activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id}, - u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id}, - u2: %{r1: r2_1.id, r2: r2_2.id}, - u3: %{r1: r3_1.id, r2: r3_2.id}, - u4: %{r1: r4_1.id}} - end - - describe "maybe_update_follow_information/1" do - setup do - clear_config([:instance, :external_user_synchronization], true) - - user = %{ - local: false, - ap_id: "https://gensokyo.2hu/users/raymoo", - following_address: "https://gensokyo.2hu/users/following", - follower_address: "https://gensokyo.2hu/users/followers", - type: "Person" - } - - %{user: user} - end - - test "logs an error when it can't fetch the info", %{user: user} do - assert capture_log(fn -> - ActivityPub.maybe_update_follow_information(user) - end) =~ "Follower/Following counter update for #{user.ap_id} failed" - end - - test "just returns the input if the user type is Application", %{ - user: user - } do - user = - user - |> Map.put(:type, "Application") - - refute capture_log(fn -> - assert ^user = ActivityPub.maybe_update_follow_information(user) - end) =~ "Follower/Following counter update for #{user.ap_id} failed" - end - - test "it just returns the input if the user has no following/follower addresses", %{ - user: user - } do - user = - user - |> Map.put(:following_address, nil) - |> Map.put(:follower_address, nil) - - refute capture_log(fn -> - assert ^user = ActivityPub.maybe_update_follow_information(user) - end) =~ "Follower/Following counter update for #{user.ap_id} failed" - end - end - - describe "global activity expiration" do - test "creates an activity expiration for local Create activities" do - clear_config([:mrf, :policies], Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy) - - {:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"}) - {:ok, follow} = ActivityBuilder.insert(%{"type" => "Follow", "context" => "3hu"}) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: activity.id}, - scheduled_at: - activity.inserted_at - |> DateTime.from_naive!("Etc/UTC") - |> Timex.shift(days: 365) - ) - - refute_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: follow.id} - ) - end - end - - describe "handling of clashing nicknames" do - test "renames an existing user with a clashing nickname and a different ap id" do - orig_user = - insert( - :user, - local: false, - nickname: "admin@mastodon.example.org", - ap_id: "http://mastodon.example.org/users/harinezumigari" - ) - - %{ - nickname: orig_user.nickname, - ap_id: orig_user.ap_id <> "part_2" - } - |> ActivityPub.maybe_handle_clashing_nickname() - - user = User.get_by_id(orig_user.id) - - assert user.nickname == "#{orig_user.id}.admin@mastodon.example.org" - end - - test "does nothing with a clashing nickname and the same ap id" do - orig_user = - insert( - :user, - local: false, - nickname: "admin@mastodon.example.org", - ap_id: "http://mastodon.example.org/users/harinezumigari" - ) - - %{ - nickname: orig_user.nickname, - ap_id: orig_user.ap_id - } - |> ActivityPub.maybe_handle_clashing_nickname() - - user = User.get_by_id(orig_user.id) - - assert user.nickname == orig_user.nickname - end - end - - describe "reply filtering" do - test "`following` still contains announcements by friends" do - user = insert(:user) - followed = insert(:user) - not_followed = insert(:user) - - User.follow(user, followed) - - {:ok, followed_post} = CommonAPI.post(followed, %{status: "Hello"}) - - {:ok, not_followed_to_followed} = - CommonAPI.post(not_followed, %{ - status: "Also hello", - in_reply_to_status_id: followed_post.id - }) - - {:ok, retoot} = CommonAPI.repeat(not_followed_to_followed.id, followed) - - params = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:reply_filtering_user, user) - |> Map.put(:reply_visibility, "following") - |> Map.put(:announce_filtering_user, user) - |> Map.put(:user, user) - - activities = - [user.ap_id | User.following(user)] - |> ActivityPub.fetch_activities(params) - - followed_post_id = followed_post.id - retoot_id = retoot.id - - assert [%{id: ^followed_post_id}, %{id: ^retoot_id}] = activities - - assert length(activities) == 2 - end - - # This test is skipped because, while this is the desired behavior, - # there seems to be no good way to achieve it with the method that - # we currently use for detecting to who a reply is directed. - # This is a TODO and should be fixed by a later rewrite of the code - # in question. - @tag skip: true - test "`following` still contains self-replies by friends" do - user = insert(:user) - followed = insert(:user) - not_followed = insert(:user) - - User.follow(user, followed) - - {:ok, followed_post} = CommonAPI.post(followed, %{status: "Hello"}) - {:ok, not_followed_post} = CommonAPI.post(not_followed, %{status: "Also hello"}) - - {:ok, _followed_to_not_followed} = - CommonAPI.post(followed, %{status: "sup", in_reply_to_status_id: not_followed_post.id}) - - {:ok, _followed_self_reply} = - CommonAPI.post(followed, %{status: "Also cofe", in_reply_to_status_id: followed_post.id}) - - params = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:reply_filtering_user, user) - |> Map.put(:reply_visibility, "following") - |> Map.put(:announce_filtering_user, user) - |> Map.put(:user, user) - - activities = - [user.ap_id | User.following(user)] - |> ActivityPub.fetch_activities(params) - - assert length(activities) == 2 - end - end -end diff --git a/test/web/activity_pub/mrf/activity_expiration_policy_test.exs b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs @@ -1,84 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do - use ExUnit.Case, async: true - alias Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy - - @id Pleroma.Web.Endpoint.url() <> "/activities/cofe" - @local_actor Pleroma.Web.Endpoint.url() <> "/users/cofe" - - test "adds `expires_at` property" do - assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} = - ActivityExpirationPolicy.filter(%{ - "id" => @id, - "actor" => @local_actor, - "type" => "Create", - "object" => %{"type" => "Note"} - }) - - assert Timex.diff(expires_at, DateTime.utc_now(), :days) == 364 - end - - test "keeps existing `expires_at` if it less than the config setting" do - expires_at = DateTime.utc_now() |> Timex.shift(days: 1) - - assert {:ok, %{"type" => "Create", "expires_at" => ^expires_at}} = - ActivityExpirationPolicy.filter(%{ - "id" => @id, - "actor" => @local_actor, - "type" => "Create", - "expires_at" => expires_at, - "object" => %{"type" => "Note"} - }) - end - - test "overwrites existing `expires_at` if it greater than the config setting" do - too_distant_future = DateTime.utc_now() |> Timex.shift(years: 2) - - assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} = - ActivityExpirationPolicy.filter(%{ - "id" => @id, - "actor" => @local_actor, - "type" => "Create", - "expires_at" => too_distant_future, - "object" => %{"type" => "Note"} - }) - - assert Timex.diff(expires_at, DateTime.utc_now(), :days) == 364 - end - - test "ignores remote activities" do - assert {:ok, activity} = - ActivityExpirationPolicy.filter(%{ - "id" => "https://example.com/123", - "actor" => "https://example.com/users/cofe", - "type" => "Create", - "object" => %{"type" => "Note"} - }) - - refute Map.has_key?(activity, "expires_at") - end - - test "ignores non-Create/Note activities" do - assert {:ok, activity} = - ActivityExpirationPolicy.filter(%{ - "id" => "https://example.com/123", - "actor" => "https://example.com/users/cofe", - "type" => "Follow" - }) - - refute Map.has_key?(activity, "expires_at") - - assert {:ok, activity} = - ActivityExpirationPolicy.filter(%{ - "id" => "https://example.com/123", - "actor" => "https://example.com/users/cofe", - "type" => "Create", - "object" => %{"type" => "Cofe"} - }) - - refute Map.has_key?(activity, "expires_at") - end -end diff --git a/test/web/activity_pub/mrf/anti_followbot_policy_test.exs b/test/web/activity_pub/mrf/anti_followbot_policy_test.exs @@ -1,72 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicyTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy - - describe "blocking based on attributes" do - test "matches followbots by nickname" do - actor = insert(:user, %{nickname: "followbot@example.com"}) - target = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Follow", - "actor" => actor.ap_id, - "object" => target.ap_id, - "id" => "https://example.com/activities/1234" - } - - assert {:reject, "[AntiFollowbotPolicy]" <> _} = AntiFollowbotPolicy.filter(message) - end - - test "matches followbots by display name" do - actor = insert(:user, %{name: "Federation Bot"}) - target = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Follow", - "actor" => actor.ap_id, - "object" => target.ap_id, - "id" => "https://example.com/activities/1234" - } - - assert {:reject, "[AntiFollowbotPolicy]" <> _} = AntiFollowbotPolicy.filter(message) - end - end - - test "it allows non-followbots" do - actor = insert(:user) - target = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Follow", - "actor" => actor.ap_id, - "object" => target.ap_id, - "id" => "https://example.com/activities/1234" - } - - {:ok, _} = AntiFollowbotPolicy.filter(message) - end - - test "it gracefully handles nil display names" do - actor = insert(:user, %{name: nil}) - target = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Follow", - "actor" => actor.ap_id, - "object" => target.ap_id, - "id" => "https://example.com/activities/1234" - } - - {:ok, _} = AntiFollowbotPolicy.filter(message) - end -end diff --git a/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs b/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs @@ -1,166 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do - use Pleroma.DataCase - import Pleroma.Factory - import ExUnit.CaptureLog - - alias Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy - - @linkless_message %{ - "type" => "Create", - "object" => %{ - "content" => "hi world!" - } - } - - @linkful_message %{ - "type" => "Create", - "object" => %{ - "content" => "<a href='https://example.com'>hi world!</a>" - } - } - - @response_message %{ - "type" => "Create", - "object" => %{ - "name" => "yes", - "type" => "Answer" - } - } - - describe "with new user" do - test "it allows posts without links" do - user = insert(:user, local: false) - - assert user.note_count == 0 - - message = - @linkless_message - |> Map.put("actor", user.ap_id) - - {:ok, _message} = AntiLinkSpamPolicy.filter(message) - end - - test "it disallows posts with links" do - user = insert(:user, local: false) - - assert user.note_count == 0 - - message = - @linkful_message - |> Map.put("actor", user.ap_id) - - {:reject, _} = AntiLinkSpamPolicy.filter(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) - - {:ok, _message} = AntiLinkSpamPolicy.filter(message) - end - end - - describe "with old user" do - test "it allows posts without links" do - user = insert(:user, note_count: 1) - - assert user.note_count == 1 - - message = - @linkless_message - |> Map.put("actor", user.ap_id) - - {:ok, _message} = AntiLinkSpamPolicy.filter(message) - end - - test "it allows posts with links" do - user = insert(:user, note_count: 1) - - assert user.note_count == 1 - - message = - @linkful_message - |> Map.put("actor", user.ap_id) - - {:ok, _message} = AntiLinkSpamPolicy.filter(message) - end - end - - describe "with followed new user" do - test "it allows posts without links" do - user = insert(:user, follower_count: 1) - - assert user.follower_count == 1 - - message = - @linkless_message - |> Map.put("actor", user.ap_id) - - {:ok, _message} = AntiLinkSpamPolicy.filter(message) - end - - test "it allows posts with links" do - user = insert(:user, follower_count: 1) - - assert user.follower_count == 1 - - message = - @linkful_message - |> Map.put("actor", user.ap_id) - - {:ok, _message} = AntiLinkSpamPolicy.filter(message) - end - end - - describe "with unknown actors" do - setup do - Tesla.Mock.mock(fn - %{method: :get, url: "http://invalid.actor"} -> - %Tesla.Env{status: 500, body: ""} - end) - - :ok - end - - test "it rejects posts without links" do - message = - @linkless_message - |> Map.put("actor", "http://invalid.actor") - - assert capture_log(fn -> - {:reject, _} = AntiLinkSpamPolicy.filter(message) - end) =~ "[error] Could not decode user at fetch http://invalid.actor" - end - - test "it rejects posts with links" do - message = - @linkful_message - |> Map.put("actor", "http://invalid.actor") - - assert capture_log(fn -> - {:reject, _} = AntiLinkSpamPolicy.filter(message) - end) =~ "[error] Could not decode user at fetch http://invalid.actor" - end - end - - describe "with contentless-objects" do - test "it does not reject them or error out" do - user = insert(:user, note_count: 1) - - message = - @response_message - |> Map.put("actor", user.ap_id) - - {:ok, _message} = AntiLinkSpamPolicy.filter(message) - end - end -end diff --git a/test/web/activity_pub/mrf/ensure_re_prepended_test.exs b/test/web/activity_pub/mrf/ensure_re_prepended_test.exs @@ -1,92 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrependedTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.MRF.EnsureRePrepended - - describe "rewrites summary" do - test "it adds `re:` to summary object when child summary and parent summary equal" do - message = %{ - "type" => "Create", - "object" => %{ - "summary" => "object-summary", - "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "object-summary"}}} - } - } - - assert {:ok, res} = EnsureRePrepended.filter(message) - 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 - message = %{ - "type" => "Create", - "object" => %{ - "summary" => "object-summary", - "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "re: object-summary"}}} - } - } - - assert {:ok, res} = EnsureRePrepended.filter(message) - assert res["object"]["summary"] == "re: object-summary" - end - end - - describe "skip filter" do - test "it skip if type isn't 'Create'" do - message = %{ - "type" => "Annotation", - "object" => %{"summary" => "object-summary"} - } - - assert {:ok, res} = EnsureRePrepended.filter(message) - assert res == message - end - - test "it skip if summary is empty" do - message = %{ - "type" => "Create", - "object" => %{ - "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "summary"}}} - } - } - - assert {:ok, res} = EnsureRePrepended.filter(message) - assert res == message - end - - test "it skip if inReplyTo is empty" do - message = %{"type" => "Create", "object" => %{"summary" => "summary"}} - assert {:ok, res} = EnsureRePrepended.filter(message) - assert res == message - end - - test "it skip if parent and child summary isn't equal" do - message = %{ - "type" => "Create", - "object" => %{ - "summary" => "object-summary", - "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "summary"}}} - } - } - - assert {:ok, res} = EnsureRePrepended.filter(message) - assert res == message - end - - test "it skips if the object is only a reference" do - message = %{ - "type" => "Create", - "object" => "somereference" - } - - assert {:ok, res} = EnsureRePrepended.filter(message) - assert res == message - end - end -end diff --git a/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs b/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs @@ -1,60 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicyTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy - @public "https://www.w3.org/ns/activitystreams#Public" - - defp generate_messages(actor) do - {%{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{}, - "to" => [@public, "f"], - "cc" => [actor.follower_address, "d"] - }, - %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d", @public]}, - "to" => ["f", actor.follower_address], - "cc" => ["d", @public] - }} - end - - test "removes from the federated timeline by nickname heuristics 1" do - actor = insert(:user, %{nickname: "annoying_ebooks@example.com"}) - - {message, except_message} = generate_messages(actor) - - assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} - end - - test "removes from the federated timeline by nickname heuristics 2" do - actor = insert(:user, %{nickname: "cirnonewsnetworkbot@meow.cat"}) - - {message, except_message} = generate_messages(actor) - - assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} - end - - test "removes from the federated timeline by actor type Application" do - actor = insert(:user, %{actor_type: "Application"}) - - {message, except_message} = generate_messages(actor) - - assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} - end - - test "removes from the federated timeline by actor type Service" do - actor = insert(:user, %{actor_type: "Service"}) - - {message, except_message} = generate_messages(actor) - - assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} - end -end diff --git a/test/web/activity_pub/mrf/hellthread_policy_test.exs b/test/web/activity_pub/mrf/hellthread_policy_test.exs @@ -1,92 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicyTest do - use Pleroma.DataCase - import Pleroma.Factory - - import Pleroma.Web.ActivityPub.MRF.HellthreadPolicy - - alias Pleroma.Web.CommonAPI - - setup do - user = insert(:user) - - message = %{ - "actor" => user.ap_id, - "cc" => [user.follower_address], - "type" => "Create", - "to" => [ - "https://www.w3.org/ns/activitystreams#Public", - "https://instance.tld/users/user1", - "https://instance.tld/users/user2", - "https://instance.tld/users/user3" - ], - "object" => %{ - "type" => "Note" - } - } - - [user: user, message: message] - end - - setup do: clear_config(:mrf_hellthread) - - test "doesn't die on chat messages" do - Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 2, reject_threshold: 0}) - - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post_chat_message(user, other_user, "moin") - - assert {:ok, _} = filter(activity.data) - end - - describe "reject" do - test "rejects the message if the recipient count is above reject_threshold", %{ - message: message - } do - Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 2}) - - assert {:reject, "[HellthreadPolicy] 3 recipients is over the limit of 2"} == - filter(message) - end - - test "does not reject the message if the recipient count is below reject_threshold", %{ - message: message - } do - Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 3}) - - assert {:ok, ^message} = filter(message) - end - end - - describe "delist" do - test "delists the message if the recipient count is above delist_threshold", %{ - user: user, - message: message - } do - Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 2, reject_threshold: 0}) - - {:ok, message} = filter(message) - assert user.follower_address in message["to"] - assert "https://www.w3.org/ns/activitystreams#Public" in message["cc"] - end - - test "does not delist the message if the recipient count is below delist_threshold", %{ - message: message - } do - Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 4, reject_threshold: 0}) - - assert {:ok, ^message} = filter(message) - end - end - - test "excludes follower collection and public URI from threshold count", %{message: message} do - Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 3}) - - assert {:ok, ^message} = filter(message) - end -end diff --git a/test/web/activity_pub/mrf/keyword_policy_test.exs b/test/web/activity_pub/mrf/keyword_policy_test.exs @@ -1,225 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicyTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.MRF.KeywordPolicy - - setup do: clear_config(:mrf_keyword) - - setup do - Pleroma.Config.put([:mrf_keyword], %{reject: [], federated_timeline_removal: [], replace: []}) - end - - describe "rejecting based on keywords" do - test "rejects if string matches in content" do - Pleroma.Config.put([:mrf_keyword, :reject], ["pun"]) - - message = %{ - "type" => "Create", - "object" => %{ - "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 if string matches in summary" do - Pleroma.Config.put([:mrf_keyword, :reject], ["pun"]) - - message = %{ - "type" => "Create", - "object" => %{ - "summary" => "just a daily reminder that compLAINer is a good pun", - "content" => "" - } - } - - assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} = - KeywordPolicy.filter(message) - end - - test "rejects if regex matches in content" do - Pleroma.Config.put([:mrf_keyword, :reject], [~r/comp[lL][aA][iI][nN]er/]) - - assert true == - Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> - message = %{ - "type" => "Create", - "object" => %{ - "content" => "just a daily reminder that #{content} is a good pun", - "summary" => "" - } - } - - {:reject, "[KeywordPolicy] Matches with rejected keyword"} == - KeywordPolicy.filter(message) - end) - end - - test "rejects if regex matches in summary" do - Pleroma.Config.put([:mrf_keyword, :reject], [~r/comp[lL][aA][iI][nN]er/]) - - assert true == - Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> - message = %{ - "type" => "Create", - "object" => %{ - "summary" => "just a daily reminder that #{content} is a good pun", - "content" => "" - } - } - - {:reject, "[KeywordPolicy] Matches with rejected keyword"} == - KeywordPolicy.filter(message) - end) - end - end - - describe "delisting from ftl based on keywords" do - test "delists if string matches in content" do - Pleroma.Config.put([: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 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 - - test "delists if string matches in summary" do - Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], ["pun"]) - - message = %{ - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "type" => "Create", - "object" => %{ - "summary" => "just a daily reminder that compLAINer is a good pun", - "content" => "" - } - } - - {: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 - - test "delists if regex matches in content" do - Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], [~r/comp[lL][aA][iI][nN]er/]) - - assert true == - Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> - message = %{ - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => %{ - "content" => "just a daily reminder that #{content} is a good pun", - "summary" => "" - } - } - - {:ok, result} = KeywordPolicy.filter(message) - - ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] and - not (["https://www.w3.org/ns/activitystreams#Public"] == result["to"]) - end) - end - - test "delists if regex matches in summary" do - Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], [~r/comp[lL][aA][iI][nN]er/]) - - assert true == - Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> - message = %{ - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => %{ - "summary" => "just a daily reminder that #{content} is a good pun", - "content" => "" - } - } - - {:ok, result} = KeywordPolicy.filter(message) - - ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] and - not (["https://www.w3.org/ns/activitystreams#Public"] == result["to"]) - end) - end - end - - describe "replacing keywords" do - test "replaces keyword if string matches in content" do - Pleroma.Config.put([:mrf_keyword, :replace], [{"opensource", "free software"}]) - - message = %{ - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => %{"content" => "ZFS is opensource", "summary" => ""} - } - - {:ok, %{"object" => %{"content" => result}}} = KeywordPolicy.filter(message) - assert result == "ZFS is free software" - end - - test "replaces keyword if string matches in summary" do - Pleroma.Config.put([:mrf_keyword, :replace], [{"opensource", "free software"}]) - - message = %{ - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => %{"summary" => "ZFS is opensource", "content" => ""} - } - - {:ok, %{"object" => %{"summary" => result}}} = KeywordPolicy.filter(message) - assert result == "ZFS is free software" - end - - test "replaces keyword if regex matches in content" do - Pleroma.Config.put([:mrf_keyword, :replace], [ - {~r/open(-|\s)?source\s?(software)?/, "free software"} - ]) - - assert true == - Enum.all?(["opensource", "open-source", "open source"], fn content -> - message = %{ - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => %{"content" => "ZFS is #{content}", "summary" => ""} - } - - {:ok, %{"object" => %{"content" => result}}} = KeywordPolicy.filter(message) - result == "ZFS is free software" - end) - end - - test "replaces keyword if regex matches in summary" do - Pleroma.Config.put([:mrf_keyword, :replace], [ - {~r/open(-|\s)?source\s?(software)?/, "free software"} - ]) - - assert true == - Enum.all?(["opensource", "open-source", "open source"], fn content -> - message = %{ - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => %{"summary" => "ZFS is #{content}", "content" => ""} - } - - {:ok, %{"object" => %{"summary" => result}}} = KeywordPolicy.filter(message) - result == "ZFS is free software" - end) - end - end -end diff --git a/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs b/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs @@ -1,53 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do - use Pleroma.DataCase - - alias Pleroma.HTTP - alias Pleroma.Tests.ObanHelpers - alias Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy - - import Mock - - @message %{ - "type" => "Create", - "object" => %{ - "type" => "Note", - "content" => "content", - "attachment" => [ - %{"url" => [%{"href" => "http://example.com/image.jpg"}]} - ] - } - } - - setup do: clear_config([:media_proxy, :enabled], true) - - test "it prefetches media proxy URIs" do - with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do - MediaProxyWarmingPolicy.filter(@message) - - ObanHelpers.perform_all() - # Performing jobs which has been just enqueued - ObanHelpers.perform_all() - - assert called(HTTP.get(:_, :_, :_)) - end - end - - test "it does nothing when no attachments are present" do - object = - @message["object"] - |> Map.delete("attachment") - - message = - @message - |> Map.put("object", object) - - with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do - MediaProxyWarmingPolicy.filter(message) - refute called(HTTP.get(:_, :_, :_)) - end - end -end diff --git a/test/web/activity_pub/mrf/mention_policy_test.exs b/test/web/activity_pub/mrf/mention_policy_test.exs @@ -1,96 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicyTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.MRF.MentionPolicy - - setup do: clear_config(:mrf_mention) - - test "pass filter if allow list is empty" do - Pleroma.Config.delete([:mrf_mention]) - - message = %{ - "type" => "Create", - "to" => ["https://example.com/ok"], - "cc" => ["https://example.com/blocked"] - } - - assert MentionPolicy.filter(message) == {:ok, message} - end - - describe "allow" do - test "empty" do - Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) - - message = %{ - "type" => "Create" - } - - assert MentionPolicy.filter(message) == {:ok, message} - end - - test "to" do - Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) - - message = %{ - "type" => "Create", - "to" => ["https://example.com/ok"] - } - - assert MentionPolicy.filter(message) == {:ok, message} - end - - test "cc" do - Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) - - message = %{ - "type" => "Create", - "cc" => ["https://example.com/ok"] - } - - assert MentionPolicy.filter(message) == {:ok, message} - end - - test "both" do - Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) - - message = %{ - "type" => "Create", - "to" => ["https://example.com/ok"], - "cc" => ["https://example.com/ok2"] - } - - assert MentionPolicy.filter(message) == {:ok, message} - end - end - - describe "deny" do - test "to" do - Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) - - message = %{ - "type" => "Create", - "to" => ["https://example.com/blocked"] - } - - assert MentionPolicy.filter(message) == - {:reject, "[MentionPolicy] Rejected for mention of https://example.com/blocked"} - end - - test "cc" do - Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) - - message = %{ - "type" => "Create", - "to" => ["https://example.com/ok"], - "cc" => ["https://example.com/blocked"] - } - - assert MentionPolicy.filter(message) == - {:reject, "[MentionPolicy] Rejected for mention of https://example.com/blocked"} - end - end -end diff --git a/test/web/activity_pub/mrf/mrf_test.exs b/test/web/activity_pub/mrf/mrf_test.exs @@ -1,90 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRFTest do - use ExUnit.Case, async: true - use Pleroma.Tests.Helpers - alias Pleroma.Web.ActivityPub.MRF - - test "subdomains_regex/1" do - assert MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) == [ - ~r/^unsafe.tld$/i, - ~r/^(.*\.)*unsafe.tld$/i - ] - end - - describe "subdomain_match/2" do - test "common domains" do - regexes = MRF.subdomains_regex(["unsafe.tld", "unsafe2.tld"]) - - assert regexes == [~r/^unsafe.tld$/i, ~r/^unsafe2.tld$/i] - - assert MRF.subdomain_match?(regexes, "unsafe.tld") - assert MRF.subdomain_match?(regexes, "unsafe2.tld") - - refute MRF.subdomain_match?(regexes, "example.com") - end - - test "wildcard domains with one subdomain" do - regexes = MRF.subdomains_regex(["*.unsafe.tld"]) - - assert regexes == [~r/^(.*\.)*unsafe.tld$/i] - - assert MRF.subdomain_match?(regexes, "unsafe.tld") - assert MRF.subdomain_match?(regexes, "sub.unsafe.tld") - refute MRF.subdomain_match?(regexes, "anotherunsafe.tld") - refute MRF.subdomain_match?(regexes, "unsafe.tldanother") - end - - test "wildcard domains with two subdomains" do - regexes = MRF.subdomains_regex(["*.unsafe.tld"]) - - assert regexes == [~r/^(.*\.)*unsafe.tld$/i] - - assert MRF.subdomain_match?(regexes, "unsafe.tld") - assert MRF.subdomain_match?(regexes, "sub.sub.unsafe.tld") - refute MRF.subdomain_match?(regexes, "sub.anotherunsafe.tld") - refute MRF.subdomain_match?(regexes, "sub.unsafe.tldanother") - end - - test "matches are case-insensitive" do - regexes = MRF.subdomains_regex(["UnSafe.TLD", "UnSAFE2.Tld"]) - - assert regexes == [~r/^UnSafe.TLD$/i, ~r/^UnSAFE2.Tld$/i] - - assert MRF.subdomain_match?(regexes, "UNSAFE.TLD") - assert MRF.subdomain_match?(regexes, "UNSAFE2.TLD") - assert MRF.subdomain_match?(regexes, "unsafe.tld") - assert MRF.subdomain_match?(regexes, "unsafe2.tld") - - refute MRF.subdomain_match?(regexes, "EXAMPLE.COM") - refute MRF.subdomain_match?(regexes, "example.com") - end - end - - describe "describe/0" do - test "it works as expected with noop policy" do - clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.NoOpPolicy]) - - expected = %{ - mrf_policies: ["NoOpPolicy"], - exclusions: false - } - - {:ok, ^expected} = MRF.describe() - end - - test "it works as expected with mock policy" do - clear_config([:mrf, :policies], [MRFModuleMock]) - - expected = %{ - mrf_policies: ["MRFModuleMock"], - mrf_module_mock: "some config data", - exclusions: false - } - - {:ok, ^expected} = MRF.describe() - end - end -end diff --git a/test/web/activity_pub/mrf/no_placeholder_text_policy_test.exs b/test/web/activity_pub/mrf/no_placeholder_text_policy_test.exs @@ -1,37 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicyTest do - use Pleroma.DataCase - alias Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy - - test "it clears content object" do - message = %{ - "type" => "Create", - "object" => %{"content" => ".", "attachment" => "image"} - } - - assert {:ok, res} = NoPlaceholderTextPolicy.filter(message) - assert res["object"]["content"] == "" - - message = put_in(message, ["object", "content"], "<p>.</p>") - assert {:ok, res} = NoPlaceholderTextPolicy.filter(message) - assert res["object"]["content"] == "" - end - - @messages [ - %{ - "type" => "Create", - "object" => %{"content" => "test", "attachment" => "image"} - }, - %{"type" => "Create", "object" => %{"content" => "."}}, - %{"type" => "Create", "object" => %{"content" => "<p>.</p>"}} - ] - test "it skips filter" do - Enum.each(@messages, fn message -> - assert {:ok, res} = NoPlaceholderTextPolicy.filter(message) - assert res == message - end) - end -end diff --git a/test/web/activity_pub/mrf/normalize_markup_test.exs b/test/web/activity_pub/mrf/normalize_markup_test.exs @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkupTest do - use Pleroma.DataCase - alias Pleroma.Web.ActivityPub.MRF.NormalizeMarkup - - @html_sample """ - <b>this is in bold</b> - <p>this is a paragraph</p> - this is a linebreak<br /> - this is a link with allowed "rel" attribute: <a href="http://example.com/" rel="tag">example.com</a> - this is a link with not allowed "rel" attribute: <a href="http://example.com/" rel="tag noallowed">example.com</a> - this is an image: <img src="http://example.com/image.jpg"><br /> - <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;) - """ - - message = %{"type" => "Create", "object" => %{"content" => @html_sample}} - - assert {:ok, res} = NormalizeMarkup.filter(message) - assert res["object"]["content"] == expected - end - - test "it skips filter if type isn't `Create`" do - message = %{"type" => "Note", "object" => %{}} - - assert {:ok, res} = NormalizeMarkup.filter(message) - assert res == message - end -end diff --git a/test/web/activity_pub/mrf/object_age_policy_test.exs b/test/web/activity_pub/mrf/object_age_policy_test.exs @@ -1,148 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do - use Pleroma.DataCase - alias Pleroma.Config - alias Pleroma.User - alias Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy - alias Pleroma.Web.ActivityPub.Visibility - - setup do: - clear_config(:mrf_object_age, - threshold: 172_800, - actions: [:delist, :strip_followers] - ) - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - defp get_old_message do - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - end - - defp get_new_message do - old_message = get_old_message() - - new_object = - old_message - |> Map.get("object") - |> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601()) - - old_message - |> Map.put("object", new_object) - end - - describe "with reject action" do - test "works with objects with empty to or cc fields" do - Config.put([:mrf_object_age, :actions], [:reject]) - - data = - get_old_message() - |> Map.put("cc", nil) - |> Map.put("to", nil) - - assert match?({:reject, _}, ObjectAgePolicy.filter(data)) - end - - test "it rejects an old post" do - Config.put([:mrf_object_age, :actions], [:reject]) - - data = get_old_message() - - assert match?({:reject, _}, ObjectAgePolicy.filter(data)) - end - - test "it allows a new post" do - Config.put([:mrf_object_age, :actions], [:reject]) - - data = get_new_message() - - assert match?({:ok, _}, ObjectAgePolicy.filter(data)) - end - end - - describe "with delist action" do - test "works with objects with empty to or cc fields" do - Config.put([:mrf_object_age, :actions], [:delist]) - - data = - get_old_message() - |> Map.put("cc", nil) - |> Map.put("to", nil) - - {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"]) - - {:ok, data} = ObjectAgePolicy.filter(data) - - assert Visibility.get_visibility(%{data: data}) == "unlisted" - end - - test "it delists an old post" do - Config.put([:mrf_object_age, :actions], [:delist]) - - data = get_old_message() - - {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"]) - - {:ok, data} = ObjectAgePolicy.filter(data) - - assert Visibility.get_visibility(%{data: data}) == "unlisted" - end - - test "it allows a new post" do - Config.put([:mrf_object_age, :actions], [:delist]) - - data = get_new_message() - - {:ok, _user} = User.get_or_fetch_by_ap_id(data["actor"]) - - assert match?({:ok, ^data}, ObjectAgePolicy.filter(data)) - end - end - - describe "with strip_followers action" do - test "works with objects with empty to or cc fields" do - Config.put([:mrf_object_age, :actions], [:strip_followers]) - - data = - get_old_message() - |> Map.put("cc", nil) - |> Map.put("to", nil) - - {:ok, user} = User.get_or_fetch_by_ap_id(data["actor"]) - - {:ok, data} = ObjectAgePolicy.filter(data) - - refute user.follower_address in data["to"] - refute user.follower_address in data["cc"] - end - - test "it strips followers collections from an old post" do - Config.put([:mrf_object_age, :actions], [:strip_followers]) - - data = get_old_message() - - {:ok, user} = User.get_or_fetch_by_ap_id(data["actor"]) - - {:ok, data} = ObjectAgePolicy.filter(data) - - refute user.follower_address in data["to"] - refute user.follower_address in data["cc"] - end - - test "it allows a new post" do - Config.put([:mrf_object_age, :actions], [:strip_followers]) - - data = get_new_message() - - {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"]) - - assert match?({:ok, ^data}, ObjectAgePolicy.filter(data)) - end - end -end diff --git a/test/web/activity_pub/mrf/reject_non_public_test.exs b/test/web/activity_pub/mrf/reject_non_public_test.exs @@ -1,100 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublicTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Web.ActivityPub.MRF.RejectNonPublic - - setup do: clear_config([:mrf_rejectnonpublic]) - - describe "public message" do - test "it's allowed when address is public" do - actor = insert(:user, follower_address: "test-address") - - message = %{ - "actor" => actor.ap_id, - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], - "type" => "Create" - } - - assert {:ok, message} = RejectNonPublic.filter(message) - end - - test "it's allowed when cc address contain public address" do - actor = insert(:user, follower_address: "test-address") - - message = %{ - "actor" => actor.ap_id, - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], - "type" => "Create" - } - - assert {:ok, message} = RejectNonPublic.filter(message) - end - end - - describe "followers message" do - test "it's allowed when addrer of message in the follower addresses of user and it enabled in config" do - actor = insert(:user, follower_address: "test-address") - - message = %{ - "actor" => actor.ap_id, - "to" => ["test-address"], - "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], - "type" => "Create" - } - - Pleroma.Config.put([:mrf_rejectnonpublic, :allow_followersonly], true) - assert {:ok, message} = RejectNonPublic.filter(message) - end - - test "it's rejected when addrer of message in the follower addresses of user and it disabled in config" do - actor = insert(:user, follower_address: "test-address") - - message = %{ - "actor" => actor.ap_id, - "to" => ["test-address"], - "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], - "type" => "Create" - } - - Pleroma.Config.put([:mrf_rejectnonpublic, :allow_followersonly], false) - assert {:reject, _} = RejectNonPublic.filter(message) - end - end - - describe "direct message" do - test "it's allows when direct messages are allow" do - actor = insert(:user) - - message = %{ - "actor" => actor.ap_id, - "to" => ["https://www.w3.org/ns/activitystreams#Publid"], - "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], - "type" => "Create" - } - - Pleroma.Config.put([:mrf_rejectnonpublic, :allow_direct], true) - assert {:ok, message} = RejectNonPublic.filter(message) - end - - test "it's reject when direct messages aren't allow" do - actor = insert(:user) - - message = %{ - "actor" => actor.ap_id, - "to" => ["https://www.w3.org/ns/activitystreams#Publid~~~"], - "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], - "type" => "Create" - } - - Pleroma.Config.put([:mrf_rejectnonpublic, :allow_direct], false) - assert {:reject, _} = RejectNonPublic.filter(message) - end - end -end diff --git a/test/web/activity_pub/mrf/simple_policy_test.exs b/test/web/activity_pub/mrf/simple_policy_test.exs @@ -1,539 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Config - alias Pleroma.Web.ActivityPub.MRF.SimplePolicy - alias Pleroma.Web.CommonAPI - - setup do: - clear_config(:mrf_simple, - media_removal: [], - media_nsfw: [], - federated_timeline_removal: [], - report_removal: [], - reject: [], - followers_only: [], - accept: [], - avatar_removal: [], - banner_removal: [], - reject_deletes: [] - ) - - describe "when :media_removal" do - test "is empty" do - Config.put([:mrf_simple, :media_removal], []) - media_message = build_media_message() - local_message = build_local_message() - - assert SimplePolicy.filter(media_message) == {:ok, media_message} - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "has a matching host" do - Config.put([:mrf_simple, :media_removal], ["remote.instance"]) - media_message = build_media_message() - local_message = build_local_message() - - assert SimplePolicy.filter(media_message) == - {:ok, - media_message - |> Map.put("object", Map.delete(media_message["object"], "attachment"))} - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "match with wildcard domain" do - Config.put([:mrf_simple, :media_removal], ["*.remote.instance"]) - media_message = build_media_message() - local_message = build_local_message() - - assert SimplePolicy.filter(media_message) == - {:ok, - media_message - |> Map.put("object", Map.delete(media_message["object"], "attachment"))} - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - end - - describe "when :media_nsfw" do - test "is empty" do - Config.put([:mrf_simple, :media_nsfw], []) - media_message = build_media_message() - local_message = build_local_message() - - assert SimplePolicy.filter(media_message) == {:ok, media_message} - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "has a matching host" do - Config.put([:mrf_simple, :media_nsfw], ["remote.instance"]) - media_message = build_media_message() - local_message = build_local_message() - - assert SimplePolicy.filter(media_message) == - {:ok, - media_message - |> put_in(["object", "tag"], ["foo", "nsfw"]) - |> put_in(["object", "sensitive"], true)} - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "match with wildcard domain" do - Config.put([:mrf_simple, :media_nsfw], ["*.remote.instance"]) - media_message = build_media_message() - local_message = build_local_message() - - assert SimplePolicy.filter(media_message) == - {:ok, - media_message - |> put_in(["object", "tag"], ["foo", "nsfw"]) - |> put_in(["object", "sensitive"], true)} - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - end - - defp build_media_message do - %{ - "actor" => "https://remote.instance/users/bob", - "type" => "Create", - "object" => %{ - "attachment" => [%{}], - "tag" => ["foo"], - "sensitive" => false - } - } - end - - describe "when :report_removal" do - test "is empty" do - Config.put([:mrf_simple, :report_removal], []) - report_message = build_report_message() - local_message = build_local_message() - - assert SimplePolicy.filter(report_message) == {:ok, report_message} - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "has a matching host" do - Config.put([:mrf_simple, :report_removal], ["remote.instance"]) - report_message = build_report_message() - local_message = build_local_message() - - assert {:reject, _} = SimplePolicy.filter(report_message) - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "match with wildcard domain" do - Config.put([:mrf_simple, :report_removal], ["*.remote.instance"]) - report_message = build_report_message() - local_message = build_local_message() - - assert {:reject, _} = SimplePolicy.filter(report_message) - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - end - - defp build_report_message do - %{ - "actor" => "https://remote.instance/users/bob", - "type" => "Flag" - } - end - - describe "when :federated_timeline_removal" do - test "is empty" do - Config.put([:mrf_simple, :federated_timeline_removal], []) - {_, ftl_message} = build_ftl_actor_and_message() - local_message = build_local_message() - - assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message} - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "has a matching host" do - {actor, ftl_message} = build_ftl_actor_and_message() - - ftl_message_actor_host = - ftl_message - |> Map.fetch!("actor") - |> URI.parse() - |> Map.fetch!(:host) - - Config.put([:mrf_simple, :federated_timeline_removal], [ftl_message_actor_host]) - local_message = build_local_message() - - assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message) - assert actor.follower_address in ftl_message["to"] - refute actor.follower_address in ftl_message["cc"] - refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"] - assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"] - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "match with wildcard domain" do - {actor, ftl_message} = build_ftl_actor_and_message() - - ftl_message_actor_host = - ftl_message - |> Map.fetch!("actor") - |> URI.parse() - |> Map.fetch!(:host) - - Config.put([:mrf_simple, :federated_timeline_removal], ["*." <> ftl_message_actor_host]) - local_message = build_local_message() - - assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message) - assert actor.follower_address in ftl_message["to"] - refute actor.follower_address in ftl_message["cc"] - refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"] - assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"] - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "has a matching host but only as:Public in to" do - {_actor, ftl_message} = build_ftl_actor_and_message() - - ftl_message_actor_host = - ftl_message - |> Map.fetch!("actor") - |> URI.parse() - |> Map.fetch!(:host) - - ftl_message = Map.put(ftl_message, "cc", []) - - Config.put([:mrf_simple, :federated_timeline_removal], [ftl_message_actor_host]) - - assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message) - refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"] - assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"] - end - end - - defp build_ftl_actor_and_message do - actor = insert(:user) - - {actor, - %{ - "actor" => actor.ap_id, - "to" => ["https://www.w3.org/ns/activitystreams#Public", "http://foo.bar/baz"], - "cc" => [actor.follower_address, "http://foo.bar/qux"] - }} - end - - describe "when :reject" do - test "is empty" do - Config.put([:mrf_simple, :reject], []) - - remote_message = build_remote_message() - - assert SimplePolicy.filter(remote_message) == {:ok, remote_message} - end - - test "activity has a matching host" do - Config.put([:mrf_simple, :reject], ["remote.instance"]) - - remote_message = build_remote_message() - - assert {:reject, _} = SimplePolicy.filter(remote_message) - end - - test "activity matches with wildcard domain" do - Config.put([:mrf_simple, :reject], ["*.remote.instance"]) - - remote_message = build_remote_message() - - assert {:reject, _} = SimplePolicy.filter(remote_message) - end - - test "actor has a matching host" do - Config.put([:mrf_simple, :reject], ["remote.instance"]) - - remote_user = build_remote_user() - - assert {:reject, _} = SimplePolicy.filter(remote_user) - end - end - - describe "when :followers_only" do - test "is empty" do - Config.put([:mrf_simple, :followers_only], []) - {_, ftl_message} = build_ftl_actor_and_message() - local_message = build_local_message() - - assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message} - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "has a matching host" do - actor = insert(:user) - following_user = insert(:user) - non_following_user = insert(:user) - - {:ok, _, _, _} = CommonAPI.follow(following_user, actor) - - activity = %{ - "actor" => actor.ap_id, - "to" => [ - "https://www.w3.org/ns/activitystreams#Public", - following_user.ap_id, - non_following_user.ap_id - ], - "cc" => [actor.follower_address, "http://foo.bar/qux"] - } - - dm_activity = %{ - "actor" => actor.ap_id, - "to" => [ - following_user.ap_id, - non_following_user.ap_id - ], - "cc" => [] - } - - actor_domain = - activity - |> Map.fetch!("actor") - |> URI.parse() - |> Map.fetch!(:host) - - Config.put([:mrf_simple, :followers_only], [actor_domain]) - - assert {:ok, new_activity} = SimplePolicy.filter(activity) - assert actor.follower_address in new_activity["cc"] - assert following_user.ap_id in new_activity["to"] - refute "https://www.w3.org/ns/activitystreams#Public" in new_activity["to"] - refute "https://www.w3.org/ns/activitystreams#Public" in new_activity["cc"] - refute non_following_user.ap_id in new_activity["to"] - refute non_following_user.ap_id in new_activity["cc"] - - assert {:ok, new_dm_activity} = SimplePolicy.filter(dm_activity) - assert new_dm_activity["to"] == [following_user.ap_id] - assert new_dm_activity["cc"] == [] - end - end - - describe "when :accept" do - test "is empty" do - Config.put([:mrf_simple, :accept], []) - - local_message = build_local_message() - remote_message = build_remote_message() - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - assert SimplePolicy.filter(remote_message) == {:ok, remote_message} - end - - test "is not empty but activity doesn't have a matching host" do - Config.put([:mrf_simple, :accept], ["non.matching.remote"]) - - local_message = build_local_message() - remote_message = build_remote_message() - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - assert {:reject, _} = SimplePolicy.filter(remote_message) - end - - test "activity has a matching host" do - Config.put([:mrf_simple, :accept], ["remote.instance"]) - - local_message = build_local_message() - remote_message = build_remote_message() - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - assert SimplePolicy.filter(remote_message) == {:ok, remote_message} - end - - test "activity matches with wildcard domain" do - Config.put([:mrf_simple, :accept], ["*.remote.instance"]) - - local_message = build_local_message() - remote_message = build_remote_message() - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - assert SimplePolicy.filter(remote_message) == {:ok, remote_message} - end - - test "actor has a matching host" do - Config.put([:mrf_simple, :accept], ["remote.instance"]) - - remote_user = build_remote_user() - - assert SimplePolicy.filter(remote_user) == {:ok, remote_user} - end - end - - describe "when :avatar_removal" do - test "is empty" do - Config.put([:mrf_simple, :avatar_removal], []) - - remote_user = build_remote_user() - - assert SimplePolicy.filter(remote_user) == {:ok, remote_user} - end - - test "is not empty but it doesn't have a matching host" do - Config.put([:mrf_simple, :avatar_removal], ["non.matching.remote"]) - - remote_user = build_remote_user() - - assert SimplePolicy.filter(remote_user) == {:ok, remote_user} - end - - test "has a matching host" do - Config.put([:mrf_simple, :avatar_removal], ["remote.instance"]) - - remote_user = build_remote_user() - {:ok, filtered} = SimplePolicy.filter(remote_user) - - refute filtered["icon"] - end - - test "match with wildcard domain" do - Config.put([:mrf_simple, :avatar_removal], ["*.remote.instance"]) - - remote_user = build_remote_user() - {:ok, filtered} = SimplePolicy.filter(remote_user) - - refute filtered["icon"] - end - end - - describe "when :banner_removal" do - test "is empty" do - Config.put([:mrf_simple, :banner_removal], []) - - remote_user = build_remote_user() - - assert SimplePolicy.filter(remote_user) == {:ok, remote_user} - end - - test "is not empty but it doesn't have a matching host" do - Config.put([:mrf_simple, :banner_removal], ["non.matching.remote"]) - - remote_user = build_remote_user() - - assert SimplePolicy.filter(remote_user) == {:ok, remote_user} - end - - test "has a matching host" do - Config.put([:mrf_simple, :banner_removal], ["remote.instance"]) - - remote_user = build_remote_user() - {:ok, filtered} = SimplePolicy.filter(remote_user) - - refute filtered["image"] - end - - test "match with wildcard domain" do - Config.put([:mrf_simple, :banner_removal], ["*.remote.instance"]) - - remote_user = build_remote_user() - {:ok, filtered} = SimplePolicy.filter(remote_user) - - refute filtered["image"] - end - end - - describe "when :reject_deletes is empty" do - setup do: Config.put([:mrf_simple, :reject_deletes], []) - - test "it accepts deletions even from rejected servers" do - Config.put([:mrf_simple, :reject], ["remote.instance"]) - - deletion_message = build_remote_deletion_message() - - assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message} - end - - test "it accepts deletions even from non-whitelisted servers" do - Config.put([:mrf_simple, :accept], ["non.matching.remote"]) - - deletion_message = build_remote_deletion_message() - - assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message} - end - end - - describe "when :reject_deletes is not empty but it doesn't have a matching host" do - setup do: Config.put([:mrf_simple, :reject_deletes], ["non.matching.remote"]) - - test "it accepts deletions even from rejected servers" do - Config.put([:mrf_simple, :reject], ["remote.instance"]) - - deletion_message = build_remote_deletion_message() - - assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message} - end - - test "it accepts deletions even from non-whitelisted servers" do - Config.put([:mrf_simple, :accept], ["non.matching.remote"]) - - deletion_message = build_remote_deletion_message() - - assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message} - end - end - - describe "when :reject_deletes has a matching host" do - setup do: Config.put([:mrf_simple, :reject_deletes], ["remote.instance"]) - - test "it rejects the deletion" do - deletion_message = build_remote_deletion_message() - - assert {:reject, _} = SimplePolicy.filter(deletion_message) - end - end - - describe "when :reject_deletes match with wildcard domain" do - setup do: Config.put([:mrf_simple, :reject_deletes], ["*.remote.instance"]) - - test "it rejects the deletion" do - deletion_message = build_remote_deletion_message() - - assert {:reject, _} = SimplePolicy.filter(deletion_message) - end - end - - defp build_local_message do - %{ - "actor" => "#{Pleroma.Web.base_url()}/users/alice", - "to" => [], - "cc" => [] - } - end - - defp build_remote_message do - %{"actor" => "https://remote.instance/users/bob"} - end - - defp build_remote_user do - %{ - "id" => "https://remote.instance/users/bob", - "icon" => %{ - "url" => "http://example.com/image.jpg", - "type" => "Image" - }, - "image" => %{ - "url" => "http://example.com/image.jpg", - "type" => "Image" - }, - "type" => "Person" - } - end - - defp build_remote_deletion_message do - %{ - "type" => "Delete", - "actor" => "https://remote.instance/users/bob" - } - end -end diff --git a/test/web/activity_pub/mrf/steal_emoji_policy_test.exs b/test/web/activity_pub/mrf/steal_emoji_policy_test.exs @@ -1,68 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup do - emoji_path = Path.join(Config.get([:instance, :static_dir]), "emoji/stolen") - File.rm_rf!(emoji_path) - File.mkdir!(emoji_path) - - Pleroma.Emoji.reload() - - on_exit(fn -> - File.rm_rf!(emoji_path) - end) - - :ok - end - - test "does nothing by default" do - installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) - refute "firedfox" in installed_emoji - - message = %{ - "type" => "Create", - "object" => %{ - "emoji" => [{"firedfox", "https://example.org/emoji/firedfox.png"}], - "actor" => "https://example.org/users/admin" - } - } - - assert {:ok, message} == StealEmojiPolicy.filter(message) - - installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) - refute "firedfox" in installed_emoji - end - - test "Steals emoji on unknown shortcode from allowed remote host" do - installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) - refute "firedfox" in installed_emoji - - message = %{ - "type" => "Create", - "object" => %{ - "emoji" => [{"firedfox", "https://example.org/emoji/firedfox.png"}], - "actor" => "https://example.org/users/admin" - } - } - - clear_config([:mrf_steal_emoji, :hosts], ["example.org"]) - clear_config([:mrf_steal_emoji, :size_limit], 284_468) - - assert {:ok, message} == StealEmojiPolicy.filter(message) - - installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) - assert "firedfox" in installed_emoji - end -end diff --git a/test/web/activity_pub/mrf/subchain_policy_test.exs b/test/web/activity_pub/mrf/subchain_policy_test.exs @@ -1,33 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicyTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.MRF.DropPolicy - alias Pleroma.Web.ActivityPub.MRF.SubchainPolicy - - @message %{ - "actor" => "https://banned.com", - "type" => "Create", - "object" => %{"content" => "hi"} - } - setup do: clear_config([:mrf_subchain, :match_actor]) - - test "it matches and processes subchains when the actor matches a configured target" do - Pleroma.Config.put([:mrf_subchain, :match_actor], %{ - ~r/^https:\/\/banned.com/s => [DropPolicy] - }) - - {:reject, _} = SubchainPolicy.filter(@message) - end - - test "it doesn't match and process subchains when the actor doesn't match a configured target" do - Pleroma.Config.put([:mrf_subchain, :match_actor], %{ - ~r/^https:\/\/borked.com/s => [DropPolicy] - }) - - {:ok, _message} = SubchainPolicy.filter(@message) - end -end diff --git a/test/web/activity_pub/mrf/tag_policy_test.exs b/test/web/activity_pub/mrf/tag_policy_test.exs @@ -1,123 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.TagPolicyTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Web.ActivityPub.MRF.TagPolicy - @public "https://www.w3.org/ns/activitystreams#Public" - - describe "mrf_tag:disable-any-subscription" do - test "rejects message" do - actor = insert(:user, tags: ["mrf_tag:disable-any-subscription"]) - message = %{"object" => actor.ap_id, "type" => "Follow", "actor" => actor.ap_id} - assert {:reject, _} = TagPolicy.filter(message) - end - end - - describe "mrf_tag:disable-remote-subscription" do - test "rejects non-local follow requests" do - actor = insert(:user, tags: ["mrf_tag:disable-remote-subscription"]) - follower = insert(:user, tags: ["mrf_tag:disable-remote-subscription"], local: false) - message = %{"object" => actor.ap_id, "type" => "Follow", "actor" => follower.ap_id} - assert {:reject, _} = TagPolicy.filter(message) - end - - test "allows non-local follow requests" do - actor = insert(:user, tags: ["mrf_tag:disable-remote-subscription"]) - follower = insert(:user, tags: ["mrf_tag:disable-remote-subscription"], local: true) - message = %{"object" => actor.ap_id, "type" => "Follow", "actor" => follower.ap_id} - assert {:ok, message} = TagPolicy.filter(message) - end - end - - describe "mrf_tag:sandbox" do - test "removes from public timelines" do - actor = insert(:user, tags: ["mrf_tag:sandbox"]) - - message = %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{}, - "to" => [@public, "f"], - "cc" => [@public, "d"] - } - - except_message = %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d"]}, - "to" => ["f", actor.follower_address], - "cc" => ["d"] - } - - assert TagPolicy.filter(message) == {:ok, except_message} - end - end - - describe "mrf_tag:force-unlisted" do - test "removes from the federated timeline" do - actor = insert(:user, tags: ["mrf_tag:force-unlisted"]) - - message = %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{}, - "to" => [@public, "f"], - "cc" => [actor.follower_address, "d"] - } - - except_message = %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d", @public]}, - "to" => ["f", actor.follower_address], - "cc" => ["d", @public] - } - - assert TagPolicy.filter(message) == {:ok, except_message} - end - end - - describe "mrf_tag:media-strip" do - test "removes attachments" do - actor = insert(:user, tags: ["mrf_tag:media-strip"]) - - message = %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{"attachment" => ["file1"]} - } - - except_message = %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{} - } - - assert TagPolicy.filter(message) == {:ok, except_message} - end - end - - describe "mrf_tag:media-force-nsfw" do - test "Mark as sensitive on presence of attachments" do - actor = insert(:user, tags: ["mrf_tag:media-force-nsfw"]) - - message = %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{"tag" => ["test"], "attachment" => ["file1"]} - } - - except_message = %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{"tag" => ["test", "nsfw"], "attachment" => ["file1"], "sensitive" => true} - } - - assert TagPolicy.filter(message) == {:ok, except_message} - end - end -end diff --git a/test/web/activity_pub/mrf/user_allowlist_policy_test.exs b/test/web/activity_pub/mrf/user_allowlist_policy_test.exs @@ -1,31 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicyTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy - - setup do: clear_config(:mrf_user_allowlist) - - test "pass filter if allow list is empty" do - actor = insert(:user) - message = %{"actor" => actor.ap_id} - assert UserAllowListPolicy.filter(message) == {:ok, message} - end - - test "pass filter if allow list isn't empty and user in allow list" do - actor = insert(:user) - Pleroma.Config.put([:mrf_user_allowlist], %{"localhost" => [actor.ap_id, "test-ap-id"]}) - message = %{"actor" => actor.ap_id} - assert UserAllowListPolicy.filter(message) == {:ok, message} - end - - test "rejected if allow list isn't empty and user not in allow list" do - actor = insert(:user) - Pleroma.Config.put([:mrf_user_allowlist], %{"localhost" => ["test-ap-id"]}) - message = %{"actor" => actor.ap_id} - assert {:reject, _} = UserAllowListPolicy.filter(message) - end -end diff --git a/test/web/activity_pub/mrf/vocabulary_policy_test.exs b/test/web/activity_pub/mrf/vocabulary_policy_test.exs @@ -1,106 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.MRF.VocabularyPolicy - - describe "accept" do - setup do: clear_config([:mrf_vocabulary, :accept]) - - test "it accepts based on parent activity type" do - Pleroma.Config.put([:mrf_vocabulary, :accept], ["Like"]) - - message = %{ - "type" => "Like", - "object" => "whatever" - } - - {:ok, ^message} = VocabularyPolicy.filter(message) - end - - test "it accepts based on child object type" do - Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"]) - - message = %{ - "type" => "Create", - "object" => %{ - "type" => "Note", - "content" => "whatever" - } - } - - {:ok, ^message} = VocabularyPolicy.filter(message) - end - - test "it does not accept disallowed child objects" do - Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"]) - - message = %{ - "type" => "Create", - "object" => %{ - "type" => "Article", - "content" => "whatever" - } - } - - {:reject, _} = VocabularyPolicy.filter(message) - end - - test "it does not accept disallowed parent types" do - Pleroma.Config.put([:mrf_vocabulary, :accept], ["Announce", "Note"]) - - message = %{ - "type" => "Create", - "object" => %{ - "type" => "Note", - "content" => "whatever" - } - } - - {:reject, _} = VocabularyPolicy.filter(message) - end - end - - describe "reject" do - setup do: clear_config([:mrf_vocabulary, :reject]) - - test "it rejects based on parent activity type" do - Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"]) - - message = %{ - "type" => "Like", - "object" => "whatever" - } - - {:reject, _} = VocabularyPolicy.filter(message) - end - - test "it rejects based on child object type" do - Pleroma.Config.put([:mrf_vocabulary, :reject], ["Note"]) - - message = %{ - "type" => "Create", - "object" => %{ - "type" => "Note", - "content" => "whatever" - } - } - - {:reject, _} = VocabularyPolicy.filter(message) - end - - test "it passes through objects that aren't disallowed" do - Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"]) - - message = %{ - "type" => "Announce", - "object" => "whatever" - } - - {:ok, ^message} = VocabularyPolicy.filter(message) - end - end -end diff --git a/test/web/activity_pub/object_validators/accept_validation_test.exs b/test/web/activity_pub/object_validators/accept_validation_test.exs @@ -1,56 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptValidationTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.ActivityPub.Pipeline - - import Pleroma.Factory - - setup do - follower = insert(:user) - followed = insert(:user, local: false) - - {:ok, follow_data, _} = Builder.follow(follower, followed) - {:ok, follow_activity, _} = Pipeline.common_pipeline(follow_data, local: true) - - {:ok, accept_data, _} = Builder.accept(followed, follow_activity) - - %{accept_data: accept_data, followed: followed} - end - - test "it validates a basic 'accept'", %{accept_data: accept_data} do - assert {:ok, _, _} = ObjectValidator.validate(accept_data, []) - end - - test "it fails when the actor doesn't exist", %{accept_data: accept_data} do - accept_data = - accept_data - |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") - - assert {:error, _} = ObjectValidator.validate(accept_data, []) - end - - test "it fails when the accepted activity doesn't exist", %{accept_data: accept_data} do - accept_data = - accept_data - |> Map.put("object", "https://gensokyo.2hu/users/raymoo/follows/1") - - assert {:error, _} = ObjectValidator.validate(accept_data, []) - end - - test "for an accepted follow, it only validates if the actor of the accept is the followed actor", - %{accept_data: accept_data} do - stranger = insert(:user) - - accept_data = - accept_data - |> Map.put("actor", stranger.ap_id) - - assert {:error, _} = ObjectValidator.validate(accept_data, []) - end -end diff --git a/test/web/activity_pub/object_validators/announce_validation_test.exs b/test/web/activity_pub/object_validators/announce_validation_test.exs @@ -1,106 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnouncValidationTest do - use Pleroma.DataCase - - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "announces" do - setup do - user = insert(:user) - announcer = insert(:user) - {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) - - object = Object.normalize(post_activity, false) - {:ok, valid_announce, []} = Builder.announce(announcer, object) - - %{ - valid_announce: valid_announce, - user: user, - post_activity: post_activity, - announcer: announcer - } - end - - test "returns ok for a valid announce", %{valid_announce: valid_announce} do - assert {:ok, _object, _meta} = ObjectValidator.validate(valid_announce, []) - end - - test "returns an error if the object can't be found", %{valid_announce: valid_announce} do - without_object = - valid_announce - |> Map.delete("object") - - {:error, cng} = ObjectValidator.validate(without_object, []) - - assert {:object, {"can't be blank", [validation: :required]}} in cng.errors - - nonexisting_object = - valid_announce - |> Map.put("object", "https://gensokyo.2hu/objects/99999999") - - {:error, cng} = ObjectValidator.validate(nonexisting_object, []) - - assert {:object, {"can't find object", []}} in cng.errors - end - - test "returns an error if we don't have the actor", %{valid_announce: valid_announce} do - nonexisting_actor = - valid_announce - |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") - - {:error, cng} = ObjectValidator.validate(nonexisting_actor, []) - - assert {:actor, {"can't find user", []}} in cng.errors - end - - test "returns an error if the actor already announced the object", %{ - valid_announce: valid_announce, - announcer: announcer, - post_activity: post_activity - } do - _announce = CommonAPI.repeat(post_activity.id, announcer) - - {:error, cng} = ObjectValidator.validate(valid_announce, []) - - assert {:actor, {"already announced this object", []}} in cng.errors - assert {:object, {"already announced by this actor", []}} in cng.errors - end - - test "returns an error if the actor can't announce the object", %{ - announcer: announcer, - user: user - } do - {:ok, post_activity} = - CommonAPI.post(user, %{status: "a secret post", visibility: "private"}) - - object = Object.normalize(post_activity, false) - - # Another user can't announce it - {:ok, announce, []} = Builder.announce(announcer, object, public: false) - - {:error, cng} = ObjectValidator.validate(announce, []) - - assert {:actor, {"can not announce this object", []}} in cng.errors - - # The actor of the object can announce it - {:ok, announce, []} = Builder.announce(user, object, public: false) - - assert {:ok, _, _} = ObjectValidator.validate(announce, []) - - # The actor of the object can not announce it publicly - {:ok, announce, []} = Builder.announce(user, object, public: true) - - {:error, cng} = ObjectValidator.validate(announce, []) - - assert {:actor, {"can not announce this object publicly", []}} in cng.errors - end - end -end diff --git a/test/web/activity_pub/object_validators/article_note_validator_test.exs b/test/web/activity_pub/object_validators/article_note_validator_test.exs @@ -1,35 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidatorTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator - alias Pleroma.Web.ActivityPub.Utils - - import Pleroma.Factory - - describe "Notes" do - setup do - user = insert(:user) - - note = %{ - "id" => Utils.generate_activity_id(), - "type" => "Note", - "actor" => user.ap_id, - "to" => [user.follower_address], - "cc" => [], - "content" => "Hellow this is content.", - "context" => "xxx", - "summary" => "a post" - } - - %{user: user, note: note} - end - - test "a basic note validates", %{note: note} do - %{valid?: true} = ArticleNoteValidator.cast_and_validate(note) - end - end -end diff --git a/test/web/activity_pub/object_validators/attachment_validator_test.exs b/test/web/activity_pub/object_validators/attachment_validator_test.exs @@ -1,74 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidatorTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator - - import Pleroma.Factory - - describe "attachments" do - test "works with honkerific attachments" do - attachment = %{ - "mediaType" => "", - "name" => "", - "summary" => "298p3RG7j27tfsZ9RQ.jpg", - "type" => "Document", - "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg" - } - - assert {:ok, attachment} = - AttachmentValidator.cast_and_validate(attachment) - |> Ecto.Changeset.apply_action(:insert) - - assert attachment.mediaType == "application/octet-stream" - end - - test "it turns mastodon attachments into our attachments" do - attachment = %{ - "url" => - "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", - "type" => "Document", - "name" => nil, - "mediaType" => "image/jpeg" - } - - {:ok, attachment} = - AttachmentValidator.cast_and_validate(attachment) - |> Ecto.Changeset.apply_action(:insert) - - assert [ - %{ - href: - "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", - type: "Link", - mediaType: "image/jpeg" - } - ] = attachment.url - - assert attachment.mediaType == "image/jpeg" - end - - test "it handles our own uploads" do - user = insert(:user) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) - - {:ok, attachment} = - attachment.data - |> AttachmentValidator.cast_and_validate() - |> Ecto.Changeset.apply_action(:insert) - - assert attachment.mediaType == "image/jpeg" - end - end -end diff --git a/test/web/activity_pub/object_validators/block_validation_test.exs b/test/web/activity_pub/object_validators/block_validation_test.exs @@ -1,39 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.BlockValidationTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - - import Pleroma.Factory - - describe "blocks" do - setup do - user = insert(:user, local: false) - blocked = insert(:user) - - {:ok, valid_block, []} = Builder.block(user, blocked) - - %{user: user, valid_block: valid_block} - end - - test "validates a basic object", %{ - valid_block: valid_block - } do - assert {:ok, _block, []} = ObjectValidator.validate(valid_block, []) - end - - test "returns an error if we don't know the blocked user", %{ - valid_block: valid_block - } do - block = - valid_block - |> Map.put("object", "https://gensokyo.2hu/users/raymoo") - - assert {:error, _cng} = ObjectValidator.validate(block, []) - end - end -end diff --git a/test/web/activity_pub/object_validators/chat_validation_test.exs b/test/web/activity_pub/object_validators/chat_validation_test.exs @@ -1,212 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatValidationTest do - use Pleroma.DataCase - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "chat message create activities" do - test "it is invalid if the object already exists" do - user = insert(:user) - recipient = insert(:user) - {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "hey") - object = Object.normalize(activity, false) - - {:ok, create_data, _} = Builder.create(user, object.data, [recipient.ap_id]) - - {:error, cng} = ObjectValidator.validate(create_data, []) - - assert {:object, {"The object to create already exists", []}} in cng.errors - end - - test "it is invalid if the object data has a different `to` or `actor` field" do - user = insert(:user) - recipient = insert(:user) - {:ok, object_data, _} = Builder.chat_message(recipient, user.ap_id, "Hey") - - {:ok, create_data, _} = Builder.create(user, object_data, [recipient.ap_id]) - - {:error, cng} = ObjectValidator.validate(create_data, []) - - assert {:to, {"Recipients don't match with object recipients", []}} in cng.errors - assert {:actor, {"Actor doesn't match with object actor", []}} in cng.errors - end - end - - describe "chat messages" do - setup do - clear_config([:instance, :remote_limit]) - user = insert(:user) - recipient = insert(:user, local: false) - - {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey :firefox:") - - %{user: user, recipient: recipient, valid_chat_message: valid_chat_message} - end - - test "let's through some basic html", %{user: user, recipient: recipient} do - {:ok, valid_chat_message, _} = - Builder.chat_message( - user, - recipient.ap_id, - "hey <a href='https://example.org'>example</a> <script>alert('uguu')</script>" - ) - - assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) - - assert object["content"] == - "hey <a href=\"https://example.org\">example</a> alert(&#39;uguu&#39;)" - end - - test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do - assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) - - assert Map.put(valid_chat_message, "attachment", nil) == object - assert match?(%{"firefox" => _}, object["emoji"]) - end - - test "validates for a basic object with an attachment", %{ - valid_chat_message: valid_chat_message, - user: user - } do - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) - - valid_chat_message = - valid_chat_message - |> Map.put("attachment", attachment.data) - - assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) - - assert object["attachment"] - end - - test "validates for a basic object with an attachment in an array", %{ - valid_chat_message: valid_chat_message, - user: user - } do - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) - - valid_chat_message = - valid_chat_message - |> Map.put("attachment", [attachment.data]) - - assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) - - assert object["attachment"] - end - - test "validates for a basic object with an attachment but without content", %{ - valid_chat_message: valid_chat_message, - user: user - } do - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) - - valid_chat_message = - valid_chat_message - |> Map.put("attachment", attachment.data) - |> Map.delete("content") - - assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) - - assert object["attachment"] - end - - test "does not validate if the message has no content", %{ - valid_chat_message: valid_chat_message - } do - contentless = - valid_chat_message - |> Map.delete("content") - - refute match?({:ok, _object, _meta}, ObjectValidator.validate(contentless, [])) - end - - test "does not validate if the message is longer than the remote_limit", %{ - valid_chat_message: valid_chat_message - } do - Pleroma.Config.put([:instance, :remote_limit], 2) - refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) - end - - test "does not validate if the recipient is blocking the actor", %{ - valid_chat_message: valid_chat_message, - user: user, - recipient: recipient - } do - Pleroma.User.block(recipient, user) - refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) - end - - test "does not validate if the recipient is not accepting chat messages", %{ - valid_chat_message: valid_chat_message, - recipient: recipient - } do - recipient - |> Ecto.Changeset.change(%{accepts_chat_messages: false}) - |> Pleroma.Repo.update!() - - refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) - end - - test "does not validate if the actor or the recipient is not in our system", %{ - valid_chat_message: valid_chat_message - } do - chat_message = - valid_chat_message - |> Map.put("actor", "https://raymoo.com/raymoo") - - {:error, _} = ObjectValidator.validate(chat_message, []) - - chat_message = - valid_chat_message - |> Map.put("to", ["https://raymoo.com/raymoo"]) - - {:error, _} = ObjectValidator.validate(chat_message, []) - end - - test "does not validate for a message with multiple recipients", %{ - valid_chat_message: valid_chat_message, - user: user, - recipient: recipient - } do - chat_message = - valid_chat_message - |> Map.put("to", [user.ap_id, recipient.ap_id]) - - assert {:error, _} = ObjectValidator.validate(chat_message, []) - end - - test "does not validate if it doesn't concern local users" do - user = insert(:user, local: false) - recipient = insert(:user, local: false) - - {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey") - assert {:error, _} = ObjectValidator.validate(valid_chat_message, []) - end - end -end diff --git a/test/web/activity_pub/object_validators/delete_validation_test.exs b/test/web/activity_pub/object_validators/delete_validation_test.exs @@ -1,106 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidationTest do - use Pleroma.DataCase - - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "deletes" do - setup do - user = insert(:user) - {:ok, post_activity} = CommonAPI.post(user, %{status: "cancel me daddy"}) - - {:ok, valid_post_delete, _} = Builder.delete(user, post_activity.data["object"]) - {:ok, valid_user_delete, _} = Builder.delete(user, user.ap_id) - - %{user: user, valid_post_delete: valid_post_delete, valid_user_delete: valid_user_delete} - end - - test "it is valid for a post deletion", %{valid_post_delete: valid_post_delete} do - {:ok, valid_post_delete, _} = ObjectValidator.validate(valid_post_delete, []) - - assert valid_post_delete["deleted_activity_id"] - end - - test "it is invalid if the object isn't in a list of certain types", %{ - valid_post_delete: valid_post_delete - } do - object = Object.get_by_ap_id(valid_post_delete["object"]) - - data = - object.data - |> Map.put("type", "Like") - - {:ok, _object} = - object - |> Ecto.Changeset.change(%{data: data}) - |> Object.update_and_set_cache() - - {:error, cng} = ObjectValidator.validate(valid_post_delete, []) - assert {:object, {"object not in allowed types", []}} in cng.errors - end - - test "it is valid for a user deletion", %{valid_user_delete: valid_user_delete} do - assert match?({:ok, _, _}, ObjectValidator.validate(valid_user_delete, [])) - end - - test "it's invalid if the id is missing", %{valid_post_delete: valid_post_delete} do - no_id = - valid_post_delete - |> Map.delete("id") - - {:error, cng} = ObjectValidator.validate(no_id, []) - - assert {:id, {"can't be blank", [validation: :required]}} in cng.errors - end - - test "it's invalid if the object doesn't exist", %{valid_post_delete: valid_post_delete} do - missing_object = - valid_post_delete - |> Map.put("object", "http://does.not/exist") - - {:error, cng} = ObjectValidator.validate(missing_object, []) - - assert {:object, {"can't find object", []}} in cng.errors - end - - test "it's invalid if the actor of the object and the actor of delete are from different domains", - %{valid_post_delete: valid_post_delete} do - valid_user = insert(:user) - - valid_other_actor = - valid_post_delete - |> Map.put("actor", valid_user.ap_id) - - assert match?({:ok, _, _}, ObjectValidator.validate(valid_other_actor, [])) - - invalid_other_actor = - valid_post_delete - |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") - - {:error, cng} = ObjectValidator.validate(invalid_other_actor, []) - - 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", - %{valid_post_delete: valid_post_delete} do - user = - insert(:user, local: true, is_moderator: true, ap_id: "https://gensokyo.2hu/users/raymoo") - - valid_other_actor = - valid_post_delete - |> Map.put("actor", user.ap_id) - - {:ok, _, meta} = ObjectValidator.validate(valid_other_actor, []) - assert meta[:do_not_federate] - end - end -end diff --git a/test/web/activity_pub/object_validators/emoji_react_validation_test.exs b/test/web/activity_pub/object_validators/emoji_react_validation_test.exs @@ -1,53 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "EmojiReacts" do - setup do - user = insert(:user) - {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) - - object = Pleroma.Object.get_by_ap_id(post_activity.data["object"]) - - {:ok, valid_emoji_react, []} = Builder.emoji_react(user, object, "👌") - - %{user: user, post_activity: post_activity, valid_emoji_react: valid_emoji_react} - end - - test "it validates a valid EmojiReact", %{valid_emoji_react: valid_emoji_react} do - assert {:ok, _, _} = ObjectValidator.validate(valid_emoji_react, []) - end - - test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emoji_react} do - without_content = - valid_emoji_react - |> Map.delete("content") - - {:error, cng} = ObjectValidator.validate(without_content, []) - - refute cng.valid? - 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 - without_emoji_content = - valid_emoji_react - |> Map.put("content", "x") - - {:error, cng} = ObjectValidator.validate(without_emoji_content, []) - - refute cng.valid? - - assert {:content, {"must be a single character emoji", []}} in cng.errors - end - end -end diff --git a/test/web/activity_pub/object_validators/follow_validation_test.exs b/test/web/activity_pub/object_validators/follow_validation_test.exs @@ -1,26 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidationTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - - import Pleroma.Factory - - describe "Follows" do - setup do - follower = insert(:user) - followed = insert(:user) - - {:ok, valid_follow, []} = Builder.follow(follower, followed) - %{follower: follower, followed: followed, valid_follow: valid_follow} - end - - test "validates a basic follow object", %{valid_follow: valid_follow} do - assert {:ok, _follow, []} = ObjectValidator.validate(valid_follow, []) - end - end -end diff --git a/test/web/activity_pub/object_validators/like_validation_test.exs b/test/web/activity_pub/object_validators/like_validation_test.exs @@ -1,113 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidationTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator - alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "likes" do - setup do - user = insert(:user) - {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) - - valid_like = %{ - "to" => [user.ap_id], - "cc" => [], - "type" => "Like", - "id" => Utils.generate_activity_id(), - "object" => post_activity.data["object"], - "actor" => user.ap_id, - "context" => "a context" - } - - %{valid_like: valid_like, user: user, post_activity: post_activity} - end - - test "returns ok when called in the ObjectValidator", %{valid_like: valid_like} do - {:ok, object, _meta} = ObjectValidator.validate(valid_like, []) - - assert "id" in Map.keys(object) - end - - test "is valid for a valid object", %{valid_like: valid_like} do - assert LikeValidator.cast_and_validate(valid_like).valid? - end - - test "sets the 'to' field to the object actor if no recipients are given", %{ - valid_like: valid_like, - user: user - } do - without_recipients = - valid_like - |> Map.delete("to") - - {:ok, object, _meta} = ObjectValidator.validate(without_recipients, []) - - assert object["to"] == [user.ap_id] - end - - test "sets the context field to the context of the object if no context is given", %{ - valid_like: valid_like, - post_activity: post_activity - } do - without_context = - valid_like - |> Map.delete("context") - - {:ok, object, _meta} = ObjectValidator.validate(without_context, []) - - assert object["context"] == post_activity.data["context"] - end - - test "it errors when the actor is missing or not known", %{valid_like: valid_like} do - without_actor = Map.delete(valid_like, "actor") - - refute LikeValidator.cast_and_validate(without_actor).valid? - - with_invalid_actor = Map.put(valid_like, "actor", "invalidactor") - - refute LikeValidator.cast_and_validate(with_invalid_actor).valid? - end - - test "it errors when the object is missing or not known", %{valid_like: valid_like} do - without_object = Map.delete(valid_like, "object") - - refute LikeValidator.cast_and_validate(without_object).valid? - - with_invalid_object = Map.put(valid_like, "object", "invalidobject") - - refute LikeValidator.cast_and_validate(with_invalid_object).valid? - end - - test "it errors when the actor has already like the object", %{ - valid_like: valid_like, - user: user, - post_activity: post_activity - } do - _like = CommonAPI.favorite(user, post_activity.id) - - refute LikeValidator.cast_and_validate(valid_like).valid? - end - - test "it works when actor or object are wrapped in maps", %{valid_like: valid_like} do - wrapped_like = - valid_like - |> Map.put("actor", %{"id" => valid_like["actor"]}) - |> Map.put("object", %{"id" => valid_like["object"]}) - - validated = LikeValidator.cast_and_validate(wrapped_like) - - assert validated.valid? - - assert {:actor, valid_like["actor"]} in validated.changes - assert {:object, valid_like["object"]} in validated.changes - end - end -end diff --git a/test/web/activity_pub/object_validators/reject_validation_test.exs b/test/web/activity_pub/object_validators/reject_validation_test.exs @@ -1,56 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.RejectValidationTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.ActivityPub.Pipeline - - import Pleroma.Factory - - setup do - follower = insert(:user) - followed = insert(:user, local: false) - - {:ok, follow_data, _} = Builder.follow(follower, followed) - {:ok, follow_activity, _} = Pipeline.common_pipeline(follow_data, local: true) - - {:ok, reject_data, _} = Builder.reject(followed, follow_activity) - - %{reject_data: reject_data, followed: followed} - end - - test "it validates a basic 'reject'", %{reject_data: reject_data} do - assert {:ok, _, _} = ObjectValidator.validate(reject_data, []) - end - - test "it fails when the actor doesn't exist", %{reject_data: reject_data} do - reject_data = - reject_data - |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") - - assert {:error, _} = ObjectValidator.validate(reject_data, []) - end - - test "it fails when the rejected activity doesn't exist", %{reject_data: reject_data} do - reject_data = - reject_data - |> Map.put("object", "https://gensokyo.2hu/users/raymoo/follows/1") - - assert {:error, _} = ObjectValidator.validate(reject_data, []) - end - - test "for an rejected follow, it only validates if the actor of the reject is the followed actor", - %{reject_data: reject_data} do - stranger = insert(:user) - - reject_data = - reject_data - |> Map.put("actor", stranger.ap_id) - - assert {:error, _} = ObjectValidator.validate(reject_data, []) - end -end diff --git a/test/web/activity_pub/object_validators/types/date_time_test.exs b/test/web/activity_pub/object_validators/types/date_time_test.exs @@ -1,36 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.DateTimeTest do - alias Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime - use Pleroma.DataCase - - test "it validates an xsd:Datetime" do - valid_strings = [ - "2004-04-12T13:20:00", - "2004-04-12T13:20:15.5", - "2004-04-12T13:20:00-05:00", - "2004-04-12T13:20:00Z" - ] - - invalid_strings = [ - "2004-04-12T13:00", - "2004-04-1213:20:00", - "99-04-12T13:00", - "2004-04-12" - ] - - assert {:ok, "2004-04-01T12:00:00Z"} == DateTime.cast("2004-04-01T12:00:00Z") - - Enum.each(valid_strings, fn date_time -> - result = DateTime.cast(date_time) - assert {:ok, _} = result - end) - - Enum.each(invalid_strings, fn date_time -> - result = DateTime.cast(date_time) - assert :error == result - end) - end -end diff --git a/test/web/activity_pub/object_validators/types/object_id_test.exs b/test/web/activity_pub/object_validators/types/object_id_test.exs @@ -1,41 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ObjectValidators.Types.ObjectIDTest do - alias Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID - use Pleroma.DataCase - - @uris [ - "http://lain.com/users/lain", - "http://lain.com", - "https://lain.com/object/1" - ] - - @non_uris [ - "https://", - "rin", - 1, - :x, - %{"1" => 2} - ] - - test "it accepts http uris" do - Enum.each(@uris, fn uri -> - assert {:ok, uri} == ObjectID.cast(uri) - end) - end - - test "it accepts an object with a nested uri id" do - Enum.each(@uris, fn uri -> - assert {:ok, uri} == ObjectID.cast(%{"id" => uri}) - end) - end - - test "it rejects non-uri strings" do - Enum.each(@non_uris, fn non_uri -> - assert :error == ObjectID.cast(non_uri) - assert :error == ObjectID.cast(%{"id" => non_uri}) - end) - end -end diff --git a/test/web/activity_pub/object_validators/types/recipients_test.exs b/test/web/activity_pub/object_validators/types/recipients_test.exs @@ -1,31 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ObjectValidators.Types.RecipientsTest do - alias Pleroma.EctoType.ActivityPub.ObjectValidators.Recipients - use Pleroma.DataCase - - test "it asserts that all elements of the list are object ids" do - list = ["https://lain.com/users/lain", "invalid"] - - assert :error == Recipients.cast(list) - end - - test "it works with a list" do - list = ["https://lain.com/users/lain"] - assert {:ok, list} == Recipients.cast(list) - end - - test "it works with a list with whole objects" do - list = ["https://lain.com/users/lain", %{"id" => "https://gensokyo.2hu/users/raymoo"}] - resulting_list = ["https://gensokyo.2hu/users/raymoo", "https://lain.com/users/lain"] - assert {:ok, resulting_list} == Recipients.cast(list) - end - - test "it turns a single string into a list" do - recipient = "https://lain.com/users/lain" - - assert {:ok, [recipient]} == Recipients.cast(recipient) - end -end diff --git a/test/web/activity_pub/object_validators/types/safe_text_test.exs b/test/web/activity_pub/object_validators/types/safe_text_test.exs @@ -1,30 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeTextTest do - use Pleroma.DataCase - - alias Pleroma.EctoType.ActivityPub.ObjectValidators.SafeText - - test "it lets normal text go through" do - text = "hey how are you" - assert {:ok, text} == SafeText.cast(text) - end - - test "it removes html tags from text" do - text = "hey look xss <script>alert('foo')</script>" - assert {:ok, "hey look xss alert(&#39;foo&#39;)"} == SafeText.cast(text) - end - - test "it keeps basic html tags" do - text = "hey <a href='http://gensokyo.2hu'>look</a> xss <script>alert('foo')</script>" - - assert {:ok, "hey <a href=\"http://gensokyo.2hu\">look</a> xss alert(&#39;foo&#39;)"} == - SafeText.cast(text) - end - - test "errors for non-text" do - assert :error == SafeText.cast(1) - end -end diff --git a/test/web/activity_pub/object_validators/undo_validation_test.exs b/test/web/activity_pub/object_validators/undo_validation_test.exs @@ -1,53 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "Undos" do - setup do - user = insert(:user) - {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) - {:ok, like} = CommonAPI.favorite(user, post_activity.id) - {:ok, valid_like_undo, []} = Builder.undo(user, like) - - %{user: user, like: like, valid_like_undo: valid_like_undo} - end - - test "it validates a basic like undo", %{valid_like_undo: valid_like_undo} do - assert {:ok, _, _} = ObjectValidator.validate(valid_like_undo, []) - end - - test "it does not validate if the actor of the undo is not the actor of the object", %{ - valid_like_undo: valid_like_undo - } do - other_user = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo") - - bad_actor = - valid_like_undo - |> Map.put("actor", other_user.ap_id) - - {:error, cng} = ObjectValidator.validate(bad_actor, []) - - assert {:actor, {"not the same as object actor", []}} in cng.errors - end - - test "it does not validate if the object is missing", %{valid_like_undo: valid_like_undo} do - missing_object = - valid_like_undo - |> Map.put("object", "https://gensokyo.2hu/objects/1") - - {:error, cng} = ObjectValidator.validate(missing_object, []) - - assert {:object, {"can't find object", []}} in cng.errors - assert length(cng.errors) == 1 - end - end -end diff --git a/test/web/activity_pub/object_validators/update_validation_test.exs b/test/web/activity_pub/object_validators/update_validation_test.exs @@ -1,44 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - - import Pleroma.Factory - - describe "updates" do - setup do - user = insert(:user) - - object = %{ - "id" => user.ap_id, - "name" => "A new name", - "summary" => "A new bio" - } - - {:ok, valid_update, []} = Builder.update(user, object) - - %{user: user, valid_update: valid_update} - end - - test "validates a basic object", %{valid_update: valid_update} do - assert {:ok, _update, []} = ObjectValidator.validate(valid_update, []) - end - - test "returns an error if the object can't be updated by the actor", %{ - valid_update: valid_update - } do - other_user = insert(:user) - - update = - valid_update - |> Map.put("actor", other_user.ap_id) - - assert {:error, _cng} = ObjectValidator.validate(update, []) - end - end -end diff --git a/test/web/activity_pub/pipeline_test.exs b/test/web/activity_pub/pipeline_test.exs @@ -1,179 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.PipelineTest do - use Pleroma.DataCase - - import Mock - import Pleroma.Factory - - describe "common_pipeline/2" do - setup do - clear_config([:instance, :federating], true) - :ok - end - - test "when given an `object_data` in meta, Federation will receive a the original activity with the `object` field set to this embedded object" do - activity = insert(:note_activity) - object = %{"id" => "1", "type" => "Love"} - meta = [local: true, object_data: object] - - activity_with_object = %{activity | data: Map.put(activity.data, "object", object)} - - with_mocks([ - {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]}, - { - Pleroma.Web.ActivityPub.MRF, - [], - [pipeline_filter: fn o, m -> {:ok, o, m} end] - }, - { - Pleroma.Web.ActivityPub.ActivityPub, - [], - [persist: fn o, m -> {:ok, o, m} end] - }, - { - Pleroma.Web.ActivityPub.SideEffects, - [], - [ - handle: fn o, m -> {:ok, o, m} end, - handle_after_transaction: fn m -> m end - ] - }, - { - Pleroma.Web.Federator, - [], - [publish: fn _o -> :ok end] - } - ]) do - assert {:ok, ^activity, ^meta} = - Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) - - assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) - refute called(Pleroma.Web.Federator.publish(activity)) - assert_called(Pleroma.Web.Federator.publish(activity_with_object)) - end - end - - test "it goes through validation, filtering, persisting, side effects and federation for local activities" do - activity = insert(:note_activity) - meta = [local: true] - - with_mocks([ - {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]}, - { - Pleroma.Web.ActivityPub.MRF, - [], - [pipeline_filter: fn o, m -> {:ok, o, m} end] - }, - { - Pleroma.Web.ActivityPub.ActivityPub, - [], - [persist: fn o, m -> {:ok, o, m} end] - }, - { - Pleroma.Web.ActivityPub.SideEffects, - [], - [ - handle: fn o, m -> {:ok, o, m} end, - handle_after_transaction: fn m -> m end - ] - }, - { - Pleroma.Web.Federator, - [], - [publish: fn _o -> :ok end] - } - ]) do - assert {:ok, ^activity, ^meta} = - Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) - - assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) - assert_called(Pleroma.Web.Federator.publish(activity)) - end - end - - test "it goes through validation, filtering, persisting, side effects without federation for remote activities" do - activity = insert(:note_activity) - meta = [local: false] - - with_mocks([ - {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]}, - { - Pleroma.Web.ActivityPub.MRF, - [], - [pipeline_filter: fn o, m -> {:ok, o, m} end] - }, - { - Pleroma.Web.ActivityPub.ActivityPub, - [], - [persist: fn o, m -> {:ok, o, m} end] - }, - { - Pleroma.Web.ActivityPub.SideEffects, - [], - [handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end] - }, - { - Pleroma.Web.Federator, - [], - [] - } - ]) do - assert {:ok, ^activity, ^meta} = - Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) - - assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) - end - end - - test "it goes through validation, filtering, persisting, side effects without federation for local activities if federation is deactivated" do - clear_config([:instance, :federating], false) - - activity = insert(:note_activity) - meta = [local: true] - - with_mocks([ - {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]}, - { - Pleroma.Web.ActivityPub.MRF, - [], - [pipeline_filter: fn o, m -> {:ok, o, m} end] - }, - { - Pleroma.Web.ActivityPub.ActivityPub, - [], - [persist: fn o, m -> {:ok, o, m} end] - }, - { - Pleroma.Web.ActivityPub.SideEffects, - [], - [handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end] - }, - { - Pleroma.Web.Federator, - [], - [] - } - ]) do - assert {:ok, ^activity, ^meta} = - Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) - - assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) - end - end - end -end diff --git a/test/web/activity_pub/publisher_test.exs b/test/web/activity_pub/publisher_test.exs @@ -1,365 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.PublisherTest do - use Pleroma.Web.ConnCase - - import ExUnit.CaptureLog - import Pleroma.Factory - import Tesla.Mock - import Mock - - alias Pleroma.Activity - alias Pleroma.Instances - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Publisher - alias Pleroma.Web.CommonAPI - - @as_public "https://www.w3.org/ns/activitystreams#Public" - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup_all do: clear_config([:instance, :federating], true) - - describe "gather_webfinger_links/1" do - test "it returns links" do - user = insert(:user) - - expected_links = [ - %{"href" => user.ap_id, "rel" => "self", "type" => "application/activity+json"}, - %{ - "href" => user.ap_id, - "rel" => "self", - "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" - }, - %{ - "rel" => "http://ostatus.org/schema/1.0/subscribe", - "template" => "#{Pleroma.Web.base_url()}/ostatus_subscribe?acct={uri}" - } - ] - - assert expected_links == Publisher.gather_webfinger_links(user) - end - end - - describe "determine_inbox/2" do - test "it returns sharedInbox for messages involving as:Public in to" do - user = insert(:user, %{shared_inbox: "http://example.com/inbox"}) - - activity = %Activity{ - data: %{"to" => [@as_public], "cc" => [user.follower_address]} - } - - assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" - end - - test "it returns sharedInbox for messages involving as:Public in cc" do - user = insert(:user, %{shared_inbox: "http://example.com/inbox"}) - - activity = %Activity{ - data: %{"cc" => [@as_public], "to" => [user.follower_address]} - } - - assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" - end - - test "it returns sharedInbox for messages involving multiple recipients in to" do - user = insert(:user, %{shared_inbox: "http://example.com/inbox"}) - user_two = insert(:user) - user_three = insert(:user) - - activity = %Activity{ - data: %{"cc" => [], "to" => [user.ap_id, user_two.ap_id, user_three.ap_id]} - } - - assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" - end - - test "it returns sharedInbox for messages involving multiple recipients in cc" do - user = insert(:user, %{shared_inbox: "http://example.com/inbox"}) - user_two = insert(:user) - user_three = insert(:user) - - activity = %Activity{ - data: %{"to" => [], "cc" => [user.ap_id, user_two.ap_id, user_three.ap_id]} - } - - assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" - end - - test "it returns sharedInbox for messages involving multiple recipients in total" do - user = - insert(:user, %{ - shared_inbox: "http://example.com/inbox", - inbox: "http://example.com/personal-inbox" - }) - - user_two = insert(:user) - - activity = %Activity{ - data: %{"to" => [user_two.ap_id], "cc" => [user.ap_id]} - } - - assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" - end - - test "it returns inbox for messages involving single recipients in total" do - user = - insert(:user, %{ - shared_inbox: "http://example.com/inbox", - inbox: "http://example.com/personal-inbox" - }) - - activity = %Activity{ - data: %{"to" => [user.ap_id], "cc" => []} - } - - assert Publisher.determine_inbox(activity, user) == "http://example.com/personal-inbox" - end - end - - describe "publish_one/1" do - test "publish to url with with different ports" do - inbox80 = "http://42.site/users/nick1/inbox" - inbox42 = "http://42.site:42/users/nick1/inbox" - - mock(fn - %{method: :post, url: "http://42.site:42/users/nick1/inbox"} -> - {:ok, %Tesla.Env{status: 200, body: "port 42"}} - - %{method: :post, url: "http://42.site/users/nick1/inbox"} -> - {:ok, %Tesla.Env{status: 200, body: "port 80"}} - end) - - actor = insert(:user) - - assert {:ok, %{body: "port 42"}} = - Publisher.publish_one(%{ - inbox: inbox42, - json: "{}", - actor: actor, - id: 1, - unreachable_since: true - }) - - assert {:ok, %{body: "port 80"}} = - Publisher.publish_one(%{ - inbox: inbox80, - json: "{}", - actor: actor, - id: 1, - unreachable_since: true - }) - end - - test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is not specified", - Instances, - [:passthrough], - [] do - actor = insert(:user) - inbox = "http://200.site/users/nick1/inbox" - - assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) - assert called(Instances.set_reachable(inbox)) - end - - test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is set", - Instances, - [:passthrough], - [] do - actor = insert(:user) - inbox = "http://200.site/users/nick1/inbox" - - assert {:ok, _} = - Publisher.publish_one(%{ - inbox: inbox, - json: "{}", - actor: actor, - id: 1, - unreachable_since: NaiveDateTime.utc_now() - }) - - assert called(Instances.set_reachable(inbox)) - end - - test_with_mock "does NOT call `Instances.set_reachable` on successful federation if `unreachable_since` is nil", - Instances, - [:passthrough], - [] do - actor = insert(:user) - inbox = "http://200.site/users/nick1/inbox" - - assert {:ok, _} = - Publisher.publish_one(%{ - inbox: inbox, - json: "{}", - actor: actor, - id: 1, - unreachable_since: nil - }) - - refute called(Instances.set_reachable(inbox)) - end - - test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code", - Instances, - [:passthrough], - [] do - actor = insert(:user) - inbox = "http://404.site/users/nick1/inbox" - - assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) - - assert called(Instances.set_unreachable(inbox)) - end - - test_with_mock "it calls `Instances.set_unreachable` on target inbox on request error of any kind", - Instances, - [:passthrough], - [] do - actor = insert(:user) - inbox = "http://connrefused.site/users/nick1/inbox" - - assert capture_log(fn -> - assert {:error, _} = - Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) - end) =~ "connrefused" - - assert called(Instances.set_unreachable(inbox)) - end - - test_with_mock "does NOT call `Instances.set_unreachable` if target is reachable", - Instances, - [:passthrough], - [] do - actor = insert(:user) - inbox = "http://200.site/users/nick1/inbox" - - assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) - - refute called(Instances.set_unreachable(inbox)) - end - - test_with_mock "does NOT call `Instances.set_unreachable` if target instance has non-nil `unreachable_since`", - Instances, - [:passthrough], - [] do - actor = insert(:user) - inbox = "http://connrefused.site/users/nick1/inbox" - - assert capture_log(fn -> - assert {:error, _} = - Publisher.publish_one(%{ - inbox: inbox, - json: "{}", - actor: actor, - id: 1, - unreachable_since: NaiveDateTime.utc_now() - }) - end) =~ "connrefused" - - refute called(Instances.set_unreachable(inbox)) - end - end - - describe "publish/2" do - test_with_mock "publishes an activity with BCC to all relevant peers.", - Pleroma.Web.Federator.Publisher, - [:passthrough], - [] do - follower = - insert(:user, %{ - local: false, - inbox: "https://domain.com/users/nick1/inbox", - ap_enabled: true - }) - - actor = insert(:user, follower_address: follower.ap_id) - user = insert(:user) - - {:ok, _follower_one} = Pleroma.User.follow(follower, actor) - actor = refresh_record(actor) - - note_activity = - insert(:note_activity, - recipients: [follower.ap_id], - data_attrs: %{"bcc" => [user.ap_id]} - ) - - res = Publisher.publish(actor, note_activity) - 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"] - }) - ) - end - - test_with_mock "publishes a delete activity to peers who signed fetch requests to the create acitvity/object.", - Pleroma.Web.Federator.Publisher, - [:passthrough], - [] do - fetcher = - insert(:user, - local: false, - inbox: "https://domain.com/users/nick1/inbox", - ap_enabled: true - ) - - another_fetcher = - insert(:user, - local: false, - inbox: "https://domain2.com/users/nick1/inbox", - ap_enabled: true - ) - - actor = insert(:user) - - note_activity = insert(:note_activity, user: actor) - object = Object.normalize(note_activity) - - activity_path = String.trim_leading(note_activity.data["id"], Pleroma.Web.Endpoint.url()) - object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url()) - - build_conn() - |> put_req_header("accept", "application/activity+json") - |> assign(:user, fetcher) - |> get(object_path) - |> json_response(200) - - build_conn() - |> put_req_header("accept", "application/activity+json") - |> assign(:user, another_fetcher) - |> get(activity_path) - |> json_response(200) - - {:ok, delete} = CommonAPI.delete(note_activity.id, actor) - - res = Publisher.publish(actor, delete) - 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"] - }) - ) - - assert called( - Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{ - inbox: "https://domain2.com/users/nick1/inbox", - actor_id: actor.id, - id: delete.data["id"] - }) - ) - end - end -end diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs @@ -1,168 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.RelayTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Relay - alias Pleroma.Web.CommonAPI - - import ExUnit.CaptureLog - import Pleroma.Factory - import Mock - - test "gets an actor for the relay" do - user = Relay.get_actor() - assert user.ap_id == "#{Pleroma.Web.Endpoint.url()}/relay" - end - - test "relay actor is invisible" do - user = Relay.get_actor() - assert User.invisible?(user) - end - - describe "follow/1" do - test "returns errors when user not found" do - assert capture_log(fn -> - {:error, _} = Relay.follow("test-ap-id") - end) =~ "Could not decode user at fetch" - end - - test "returns activity" do - user = insert(:user) - service_actor = Relay.get_actor() - assert {:ok, %Activity{} = activity} = Relay.follow(user.ap_id) - assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" - assert user.ap_id in activity.recipients - assert activity.data["type"] == "Follow" - assert activity.data["actor"] == service_actor.ap_id - assert activity.data["object"] == user.ap_id - end - end - - describe "unfollow/1" do - test "returns errors when user not found" do - assert capture_log(fn -> - {:error, _} = Relay.unfollow("test-ap-id") - end) =~ "Could not decode user at fetch" - end - - test "returns activity" do - user = insert(:user) - service_actor = Relay.get_actor() - CommonAPI.follow(service_actor, user) - assert "#{user.ap_id}/followers" in User.following(service_actor) - assert {:ok, %Activity{} = activity} = Relay.unfollow(user.ap_id) - assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" - assert user.ap_id in activity.recipients - assert activity.data["type"] == "Undo" - assert activity.data["actor"] == service_actor.ap_id - assert activity.data["to"] == [user.ap_id] - refute "#{user.ap_id}/followers" in User.following(service_actor) - end - - test "force unfollow when target service is dead" do - user = insert(:user) - user_ap_id = user.ap_id - user_id = user.id - - Tesla.Mock.mock(fn %{method: :get, url: ^user_ap_id} -> - %Tesla.Env{status: 404} - end) - - service_actor = Relay.get_actor() - CommonAPI.follow(service_actor, user) - assert "#{user.ap_id}/followers" in User.following(service_actor) - - assert Pleroma.Repo.get_by( - Pleroma.FollowingRelationship, - follower_id: service_actor.id, - following_id: user_id - ) - - Pleroma.Repo.delete(user) - Cachex.clear(:user_cache) - - assert {:ok, %Activity{} = activity} = Relay.unfollow(user_ap_id, %{force: true}) - - assert refresh_record(service_actor).following_count == 0 - - refute Pleroma.Repo.get_by( - Pleroma.FollowingRelationship, - follower_id: service_actor.id, - following_id: user_id - ) - - assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" - assert user.ap_id in activity.recipients - assert activity.data["type"] == "Undo" - assert activity.data["actor"] == service_actor.ap_id - assert activity.data["to"] == [user_ap_id] - refute "#{user.ap_id}/followers" in User.following(service_actor) - end - end - - describe "publish/1" do - setup do: clear_config([:instance, :federating]) - - test "returns error when activity not `Create` type" do - activity = insert(:like_activity) - assert Relay.publish(activity) == {:error, "Not implemented"} - end - - @tag capture_log: true - test "returns error when activity not public" do - activity = insert(:direct_note_activity) - assert Relay.publish(activity) == {:error, false} - end - - test "returns error when object is unknown" do - activity = - insert(:note_activity, - data: %{ - "type" => "Create", - "object" => "http://mastodon.example.org/eee/99541947525187367" - } - ) - - Tesla.Mock.mock(fn - %{method: :get, url: "http://mastodon.example.org/eee/99541947525187367"} -> - %Tesla.Env{status: 500, body: ""} - end) - - assert capture_log(fn -> - assert Relay.publish(activity) == {:error, false} - end) =~ "[error] error: false" - end - - test_with_mock "returns announce activity and publish to federate", - Pleroma.Web.Federator, - [:passthrough], - [] do - clear_config([:instance, :federating], true) - service_actor = Relay.get_actor() - note = insert(:note_activity) - assert {:ok, %Activity{} = activity} = Relay.publish(note) - assert activity.data["type"] == "Announce" - assert activity.data["actor"] == service_actor.ap_id - assert activity.data["to"] == [service_actor.follower_address] - assert called(Pleroma.Web.Federator.publish(activity)) - end - - test_with_mock "returns announce activity and not publish to federate", - Pleroma.Web.Federator, - [:passthrough], - [] do - clear_config([:instance, :federating], false) - service_actor = Relay.get_actor() - note = insert(:note_activity) - assert {:ok, %Activity{} = activity} = Relay.publish(note) - assert activity.data["type"] == "Announce" - assert activity.data["actor"] == service_actor.ap_id - refute called(Pleroma.Web.Federator.publish(activity)) - end - end -end diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs @@ -1,639 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.SideEffectsTest do - use Oban.Testing, repo: Pleroma.Repo - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Chat - alias Pleroma.Chat.MessageReference - alias Pleroma.Notification - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.SideEffects - alias Pleroma.Web.CommonAPI - - import ExUnit.CaptureLog - import Mock - import Pleroma.Factory - - describe "handle_after_transaction" do - test "it streams out notifications and streams" do - author = insert(:user, local: true) - recipient = insert(:user, local: true) - - {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") - - {:ok, create_activity_data, _meta} = - Builder.create(author, chat_message_data["id"], [recipient.ap_id]) - - {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - - {:ok, _create_activity, meta} = - SideEffects.handle(create_activity, local: false, object_data: chat_message_data) - - assert [notification] = meta[:notifications] - - with_mocks([ - { - Pleroma.Web.Streamer, - [], - [ - stream: fn _, _ -> nil end - ] - }, - { - Pleroma.Web.Push, - [], - [ - send: fn _ -> nil end - ] - } - ]) do - SideEffects.handle_after_transaction(meta) - - assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) - assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) - assert called(Pleroma.Web.Push.send(notification)) - end - end - end - - describe "blocking users" do - setup do - user = insert(:user) - blocked = insert(:user) - User.follow(blocked, user) - User.follow(user, blocked) - - {:ok, block_data, []} = Builder.block(user, blocked) - {:ok, block, _meta} = ActivityPub.persist(block_data, local: true) - - %{user: user, blocked: blocked, block: block} - end - - test "it unfollows and blocks", %{user: user, blocked: blocked, block: block} do - assert User.following?(user, blocked) - assert User.following?(blocked, user) - - {:ok, _, _} = SideEffects.handle(block) - - refute User.following?(user, blocked) - refute User.following?(blocked, user) - assert User.blocks?(user, blocked) - end - - test "it blocks but does not unfollow if the relevant setting is set", %{ - user: user, - blocked: blocked, - block: block - } do - clear_config([:activitypub, :unfollow_blocked], false) - assert User.following?(user, blocked) - assert User.following?(blocked, user) - - {:ok, _, _} = SideEffects.handle(block) - - refute User.following?(user, blocked) - assert User.following?(blocked, user) - assert User.blocks?(user, blocked) - end - end - - describe "update users" do - setup do - user = insert(:user) - {:ok, update_data, []} = Builder.update(user, %{"id" => user.ap_id, "name" => "new name!"}) - {:ok, update, _meta} = ActivityPub.persist(update_data, local: true) - - %{user: user, update_data: update_data, update: update} - end - - test "it updates the user", %{user: user, update: update} do - {:ok, _, _} = SideEffects.handle(update) - user = User.get_by_id(user.id) - assert user.name == "new name!" - end - - test "it uses a given changeset to update", %{user: user, update: update} do - changeset = Ecto.Changeset.change(user, %{default_scope: "direct"}) - - assert user.default_scope == "public" - {:ok, _, _} = SideEffects.handle(update, user_update_changeset: changeset) - user = User.get_by_id(user.id) - assert user.default_scope == "direct" - end - end - - describe "delete objects" do - setup do - user = insert(:user) - other_user = insert(:user) - - {:ok, op} = CommonAPI.post(other_user, %{status: "big oof"}) - {:ok, post} = CommonAPI.post(user, %{status: "hey", in_reply_to_id: op}) - {:ok, favorite} = CommonAPI.favorite(user, post.id) - object = Object.normalize(post) - {:ok, delete_data, _meta} = Builder.delete(user, object.data["id"]) - {:ok, delete_user_data, _meta} = Builder.delete(user, user.ap_id) - {:ok, delete, _meta} = ActivityPub.persist(delete_data, local: true) - {:ok, delete_user, _meta} = ActivityPub.persist(delete_user_data, local: true) - - %{ - user: user, - delete: delete, - post: post, - object: object, - delete_user: delete_user, - op: op, - favorite: favorite - } - end - - test "it handles object deletions", %{ - delete: delete, - post: post, - object: object, - user: user, - op: op, - favorite: favorite - } do - with_mock Pleroma.Web.ActivityPub.ActivityPub, [:passthrough], - stream_out: fn _ -> nil end, - stream_out_participations: fn _, _ -> nil end do - {:ok, delete, _} = SideEffects.handle(delete) - user = User.get_cached_by_ap_id(object.data["actor"]) - - assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out(delete)) - assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out_participations(object, user)) - end - - object = Object.get_by_id(object.id) - assert object.data["type"] == "Tombstone" - refute Activity.get_by_id(post.id) - refute Activity.get_by_id(favorite.id) - - user = User.get_by_id(user.id) - assert user.note_count == 0 - - object = Object.normalize(op.data["object"], false) - - assert object.data["repliesCount"] == 0 - end - - test "it handles object deletions when the object itself has been pruned", %{ - delete: delete, - post: post, - object: object, - user: user, - op: op - } do - with_mock Pleroma.Web.ActivityPub.ActivityPub, [:passthrough], - stream_out: fn _ -> nil end, - stream_out_participations: fn _, _ -> nil end do - {:ok, delete, _} = SideEffects.handle(delete) - user = User.get_cached_by_ap_id(object.data["actor"]) - - assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out(delete)) - assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out_participations(object, user)) - end - - object = Object.get_by_id(object.id) - assert object.data["type"] == "Tombstone" - refute Activity.get_by_id(post.id) - - user = User.get_by_id(user.id) - assert user.note_count == 0 - - object = Object.normalize(op.data["object"], false) - - assert object.data["repliesCount"] == 0 - end - - test "it handles user deletions", %{delete_user: delete, user: user} do - {:ok, _delete, _} = SideEffects.handle(delete) - ObanHelpers.perform_all() - - assert User.get_cached_by_ap_id(user.ap_id).deactivated - end - - test "it logs issues with objects deletion", %{ - delete: delete, - object: object - } do - {:ok, object} = - object - |> Object.change(%{data: Map.delete(object.data, "actor")}) - |> Repo.update() - - Object.invalid_object_cache(object) - - assert capture_log(fn -> - {:error, :no_object_actor} = SideEffects.handle(delete) - end) =~ "object doesn't have an actor" - end - end - - describe "EmojiReact objects" do - setup do - poster = insert(:user) - user = insert(:user) - - {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) - - {:ok, emoji_react_data, []} = Builder.emoji_react(user, post.object, "👌") - {:ok, emoji_react, _meta} = ActivityPub.persist(emoji_react_data, local: true) - - %{emoji_react: emoji_react, user: user, poster: poster} - end - - test "adds the reaction to the object", %{emoji_react: emoji_react, user: user} do - {:ok, emoji_react, _} = SideEffects.handle(emoji_react) - object = Object.get_by_ap_id(emoji_react.data["object"]) - - assert object.data["reaction_count"] == 1 - assert ["👌", [user.ap_id]] in object.data["reactions"] - end - - test "creates a notification", %{emoji_react: emoji_react, poster: poster} do - {:ok, emoji_react, _} = SideEffects.handle(emoji_react) - assert Repo.get_by(Notification, user_id: poster.id, activity_id: emoji_react.id) - end - end - - describe "delete users with confirmation pending" do - setup do - user = insert(:user, confirmation_pending: true) - {:ok, delete_user_data, _meta} = Builder.delete(user, user.ap_id) - {:ok, delete_user, _meta} = ActivityPub.persist(delete_user_data, local: true) - {:ok, delete: delete_user, user: user} - end - - test "when activation is not required", %{delete: delete, user: user} do - clear_config([:instance, :account_activation_required], false) - {:ok, _, _} = SideEffects.handle(delete) - ObanHelpers.perform_all() - - assert User.get_cached_by_id(user.id).deactivated - end - - test "when activation is required", %{delete: delete, user: user} do - clear_config([:instance, :account_activation_required], true) - {:ok, _, _} = SideEffects.handle(delete) - ObanHelpers.perform_all() - - refute User.get_cached_by_id(user.id) - end - end - - describe "Undo objects" do - setup do - poster = insert(:user) - user = insert(:user) - {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) - {:ok, like} = CommonAPI.favorite(user, post.id) - {:ok, reaction} = CommonAPI.react_with_emoji(post.id, user, "👍") - {:ok, announce} = CommonAPI.repeat(post.id, user) - {:ok, block} = CommonAPI.block(user, poster) - - {:ok, undo_data, _meta} = Builder.undo(user, like) - {:ok, like_undo, _meta} = ActivityPub.persist(undo_data, local: true) - - {:ok, undo_data, _meta} = Builder.undo(user, reaction) - {:ok, reaction_undo, _meta} = ActivityPub.persist(undo_data, local: true) - - {:ok, undo_data, _meta} = Builder.undo(user, announce) - {:ok, announce_undo, _meta} = ActivityPub.persist(undo_data, local: true) - - {:ok, undo_data, _meta} = Builder.undo(user, block) - {:ok, block_undo, _meta} = ActivityPub.persist(undo_data, local: true) - - %{ - like_undo: like_undo, - post: post, - like: like, - reaction_undo: reaction_undo, - reaction: reaction, - announce_undo: announce_undo, - announce: announce, - block_undo: block_undo, - block: block, - poster: poster, - user: user - } - end - - test "deletes the original block", %{ - block_undo: block_undo, - block: block - } do - {:ok, _block_undo, _meta} = SideEffects.handle(block_undo) - - refute Activity.get_by_id(block.id) - end - - test "unblocks the blocked user", %{block_undo: block_undo, block: block} do - blocker = User.get_by_ap_id(block.data["actor"]) - blocked = User.get_by_ap_id(block.data["object"]) - - {:ok, _block_undo, _} = SideEffects.handle(block_undo) - refute User.blocks?(blocker, blocked) - end - - test "an announce undo removes the announce from the object", %{ - announce_undo: announce_undo, - post: post - } do - {:ok, _announce_undo, _} = SideEffects.handle(announce_undo) - - object = Object.get_by_ap_id(post.data["object"]) - - assert object.data["announcement_count"] == 0 - assert object.data["announcements"] == [] - end - - test "deletes the original announce", %{announce_undo: announce_undo, announce: announce} do - {:ok, _announce_undo, _} = SideEffects.handle(announce_undo) - refute Activity.get_by_id(announce.id) - end - - test "a reaction undo removes the reaction from the object", %{ - reaction_undo: reaction_undo, - post: post - } do - {:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo) - - object = Object.get_by_ap_id(post.data["object"]) - - assert object.data["reaction_count"] == 0 - assert object.data["reactions"] == [] - end - - test "deletes the original reaction", %{reaction_undo: reaction_undo, reaction: reaction} do - {:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo) - refute Activity.get_by_id(reaction.id) - end - - test "a like undo removes the like from the object", %{like_undo: like_undo, post: post} do - {:ok, _like_undo, _} = SideEffects.handle(like_undo) - - object = Object.get_by_ap_id(post.data["object"]) - - assert object.data["like_count"] == 0 - assert object.data["likes"] == [] - end - - test "deletes the original like", %{like_undo: like_undo, like: like} do - {:ok, _like_undo, _} = SideEffects.handle(like_undo) - refute Activity.get_by_id(like.id) - end - end - - describe "like objects" do - setup do - poster = insert(:user) - user = insert(:user) - {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) - - {:ok, like_data, _meta} = Builder.like(user, post.object) - {:ok, like, _meta} = ActivityPub.persist(like_data, local: true) - - %{like: like, user: user, poster: poster} - end - - test "add the like to the original object", %{like: like, user: user} do - {:ok, like, _} = SideEffects.handle(like) - object = Object.get_by_ap_id(like.data["object"]) - assert object.data["like_count"] == 1 - assert user.ap_id in object.data["likes"] - end - - test "creates a notification", %{like: like, poster: poster} do - {:ok, like, _} = SideEffects.handle(like) - assert Repo.get_by(Notification, user_id: poster.id, activity_id: like.id) - end - end - - describe "creation of ChatMessages" do - test "notifies the recipient" do - author = insert(:user, local: false) - recipient = insert(:user, local: true) - - {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") - - {:ok, create_activity_data, _meta} = - Builder.create(author, chat_message_data["id"], [recipient.ap_id]) - - {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - - {:ok, _create_activity, _meta} = - SideEffects.handle(create_activity, local: false, object_data: chat_message_data) - - assert Repo.get_by(Notification, user_id: recipient.id, activity_id: create_activity.id) - end - - test "it streams the created ChatMessage" do - author = insert(:user, local: true) - recipient = insert(:user, local: true) - - {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") - - {:ok, create_activity_data, _meta} = - Builder.create(author, chat_message_data["id"], [recipient.ap_id]) - - {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - - {:ok, _create_activity, meta} = - SideEffects.handle(create_activity, local: false, object_data: chat_message_data) - - assert [_, _] = meta[:streamables] - end - - test "it creates a Chat and MessageReferences for the local users and bumps the unread count, except for the author" do - author = insert(:user, local: true) - recipient = insert(:user, local: true) - - {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") - - {:ok, create_activity_data, _meta} = - Builder.create(author, chat_message_data["id"], [recipient.ap_id]) - - {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - - with_mocks([ - { - Pleroma.Web.Streamer, - [], - [ - stream: fn _, _ -> nil end - ] - }, - { - Pleroma.Web.Push, - [], - [ - send: fn _ -> nil end - ] - } - ]) do - {:ok, _create_activity, meta} = - SideEffects.handle(create_activity, local: false, object_data: chat_message_data) - - # The notification gets created - assert [notification] = meta[:notifications] - assert notification.activity_id == create_activity.id - - # But it is not sent out - refute called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) - refute called(Pleroma.Web.Push.send(notification)) - - # Same for the user chat stream - assert [{topics, _}, _] = meta[:streamables] - assert topics == ["user", "user:pleroma_chat"] - refute called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) - - chat = Chat.get(author.id, recipient.ap_id) - - [cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all() - - assert cm_ref.object.data["content"] == "hey" - assert cm_ref.unread == false - - chat = Chat.get(recipient.id, author.ap_id) - - [cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all() - - assert cm_ref.object.data["content"] == "hey" - assert cm_ref.unread == true - end - end - - test "it creates a Chat for the local users and bumps the unread count" do - author = insert(:user, local: false) - recipient = insert(:user, local: true) - - {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") - - {:ok, create_activity_data, _meta} = - Builder.create(author, chat_message_data["id"], [recipient.ap_id]) - - {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - - {:ok, _create_activity, _meta} = - SideEffects.handle(create_activity, local: false, object_data: chat_message_data) - - # An object is created - assert Object.get_by_ap_id(chat_message_data["id"]) - - # The remote user won't get a chat - chat = Chat.get(author.id, recipient.ap_id) - refute chat - - # The local user will get a chat - chat = Chat.get(recipient.id, author.ap_id) - assert chat - - author = insert(:user, local: true) - recipient = insert(:user, local: true) - - {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") - - {:ok, create_activity_data, _meta} = - Builder.create(author, chat_message_data["id"], [recipient.ap_id]) - - {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - - {:ok, _create_activity, _meta} = - SideEffects.handle(create_activity, local: false, object_data: chat_message_data) - - # Both users are local and get the chat - chat = Chat.get(author.id, recipient.ap_id) - assert chat - - chat = Chat.get(recipient.id, author.ap_id) - assert chat - end - end - - describe "announce objects" do - setup do - poster = insert(:user) - user = insert(:user) - {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) - {:ok, private_post} = CommonAPI.post(poster, %{status: "hey", visibility: "private"}) - - {:ok, announce_data, _meta} = Builder.announce(user, post.object, public: true) - - {:ok, private_announce_data, _meta} = - Builder.announce(user, private_post.object, public: false) - - {:ok, relay_announce_data, _meta} = - Builder.announce(Pleroma.Web.ActivityPub.Relay.get_actor(), post.object, public: true) - - {:ok, announce, _meta} = ActivityPub.persist(announce_data, local: true) - {:ok, private_announce, _meta} = ActivityPub.persist(private_announce_data, local: true) - {:ok, relay_announce, _meta} = ActivityPub.persist(relay_announce_data, local: true) - - %{ - announce: announce, - user: user, - poster: poster, - private_announce: private_announce, - relay_announce: relay_announce - } - end - - test "adds the announce to the original object", %{announce: announce, user: user} do - {:ok, announce, _} = SideEffects.handle(announce) - object = Object.get_by_ap_id(announce.data["object"]) - assert object.data["announcement_count"] == 1 - assert user.ap_id in object.data["announcements"] - end - - test "does not add the announce to the original object if the actor is a service actor", %{ - relay_announce: announce - } do - {:ok, announce, _} = SideEffects.handle(announce) - object = Object.get_by_ap_id(announce.data["object"]) - assert object.data["announcement_count"] == nil - end - - test "creates a notification", %{announce: announce, poster: poster} 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 -end diff --git a/test/web/activity_pub/transmogrifier/accept_handling_test.exs b/test/web/activity_pub/transmogrifier/accept_handling_test.exs @@ -1,91 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.AcceptHandlingTest do - use Pleroma.DataCase - - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "it works for incoming accepts which were pre-accepted" do - follower = insert(:user) - followed = insert(:user) - - {:ok, follower} = User.follow(follower, followed) - assert User.following?(follower, followed) == true - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) - - accept_data = - File.read!("test/fixtures/mastodon-accept-activity.json") - |> Poison.decode!() - |> Map.put("actor", followed.ap_id) - - object = - accept_data["object"] - |> Map.put("actor", follower.ap_id) - |> Map.put("id", follow_activity.data["id"]) - - accept_data = Map.put(accept_data, "object", object) - - {:ok, activity} = Transmogrifier.handle_incoming(accept_data) - refute activity.local - - assert activity.data["object"] == follow_activity.data["id"] - - assert activity.data["id"] == accept_data["id"] - - follower = User.get_cached_by_id(follower.id) - - assert User.following?(follower, followed) == true - end - - test "it works for incoming accepts which are referenced by IRI only" do - follower = insert(:user) - followed = insert(:user, locked: true) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) - - accept_data = - File.read!("test/fixtures/mastodon-accept-activity.json") - |> Poison.decode!() - |> Map.put("actor", followed.ap_id) - |> Map.put("object", follow_activity.data["id"]) - - {:ok, activity} = Transmogrifier.handle_incoming(accept_data) - assert activity.data["object"] == follow_activity.data["id"] - - follower = User.get_cached_by_id(follower.id) - - assert User.following?(follower, followed) == true - - follower = User.get_by_id(follower.id) - assert follower.following_count == 1 - - followed = User.get_by_id(followed.id) - assert followed.follower_count == 1 - end - - test "it fails for incoming accepts which cannot be correlated" do - follower = insert(:user) - followed = insert(:user, locked: true) - - accept_data = - File.read!("test/fixtures/mastodon-accept-activity.json") - |> Poison.decode!() - |> Map.put("actor", followed.ap_id) - - accept_data = - Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) - - {:error, _} = Transmogrifier.handle_incoming(accept_data) - - follower = User.get_cached_by_id(follower.id) - - refute User.following?(follower, followed) == true - end -end diff --git a/test/web/activity_pub/transmogrifier/announce_handling_test.exs b/test/web/activity_pub/transmogrifier/announce_handling_test.exs @@ -1,172 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnnounceHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "it works for incoming honk announces" do - user = insert(:user, ap_id: "https://honktest/u/test", local: false) - other_user = insert(:user) - {:ok, post} = CommonAPI.post(other_user, %{status: "bonkeronk"}) - - announce = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "actor" => "https://honktest/u/test", - "id" => "https://honktest/u/test/bonk/1793M7B9MQ48847vdx", - "object" => post.data["object"], - "published" => "2019-06-25T19:33:58Z", - "to" => "https://www.w3.org/ns/activitystreams#Public", - "type" => "Announce" - } - - {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(announce) - - object = Object.get_by_ap_id(post.data["object"]) - - assert length(object.data["announcements"]) == 1 - assert user.ap_id in object.data["announcements"] - end - - test "it works for incoming announces with actor being inlined (kroeg)" do - data = File.read!("test/fixtures/kroeg-announce-with-inline-actor.json") |> Poison.decode!() - - _user = insert(:user, local: false, ap_id: data["actor"]["id"]) - other_user = insert(:user) - - {:ok, post} = CommonAPI.post(other_user, %{status: "kroegeroeg"}) - - data = - data - |> put_in(["object", "id"], post.data["object"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "https://puckipedia.com/" - end - - test "it works for incoming announces, fetching the announced object" do - data = - File.read!("test/fixtures/mastodon-announce.json") - |> Poison.decode!() - |> Map.put("object", "http://mastodon.example.org/users/admin/statuses/99541947525187367") - - Tesla.Mock.mock(fn - %{method: :get} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/mastodon-note-object.json")} - end) - - _user = insert(:user, local: false, ap_id: data["actor"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "Announce" - - assert data["id"] == - "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" - - assert data["object"] == - "http://mastodon.example.org/users/admin/statuses/99541947525187367" - - assert(Activity.get_create_by_object_ap_id(data["object"])) - end - - @tag capture_log: true - test "it works for incoming announces with an existing activity" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - - data = - File.read!("test/fixtures/mastodon-announce.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - _user = insert(:user, local: false, ap_id: data["actor"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "Announce" - - assert data["id"] == - "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" - - assert data["object"] == activity.data["object"] - - assert Activity.get_create_by_object_ap_id(data["object"]).id == activity.id - end - - # Ignore inlined activities for now - @tag skip: true - test "it works for incoming announces with an inlined activity" do - data = - File.read!("test/fixtures/mastodon-announce-private.json") - |> Poison.decode!() - - _user = - insert(:user, - local: false, - ap_id: data["actor"], - follower_address: data["actor"] <> "/followers" - ) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "Announce" - - assert data["id"] == - "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" - - object = Object.normalize(data["object"]) - - assert object.data["id"] == "http://mastodon.example.org/@admin/99541947525187368" - assert object.data["content"] == "this is a private toot" - end - - @tag capture_log: true - test "it rejects incoming announces with an inlined activity from another origin" do - Tesla.Mock.mock(fn - %{method: :get} -> %Tesla.Env{status: 404, body: ""} - end) - - data = - File.read!("test/fixtures/bogus-mastodon-announce.json") - |> Poison.decode!() - - _user = insert(:user, local: false, ap_id: data["actor"]) - - assert {:error, e} = Transmogrifier.handle_incoming(data) - end - - test "it does not clobber the addressing on announce activities" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - - data = - File.read!("test/fixtures/mastodon-announce.json") - |> Poison.decode!() - |> Map.put("object", Object.normalize(activity).data["id"]) - |> Map.put("to", ["http://mastodon.example.org/users/admin/followers"]) - |> Map.put("cc", []) - - _user = - insert(:user, - local: false, - ap_id: data["actor"], - follower_address: "http://mastodon.example.org/users/admin/followers" - ) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["to"] == ["http://mastodon.example.org/users/admin/followers"] - end -end diff --git a/test/web/activity_pub/transmogrifier/answer_handling_test.exs b/test/web/activity_pub/transmogrifier/answer_handling_test.exs @@ -1,78 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnswerHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "incoming, rewrites Note to Answer and increments vote counters" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "suya...", - poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10} - }) - - object = Object.normalize(activity) - - data = - File.read!("test/fixtures/mastodon-vote.json") - |> Poison.decode!() - |> Kernel.put_in(["to"], user.ap_id) - |> Kernel.put_in(["object", "inReplyTo"], object.data["id"]) - |> Kernel.put_in(["object", "to"], user.ap_id) - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - answer_object = Object.normalize(activity) - assert answer_object.data["type"] == "Answer" - assert answer_object.data["inReplyTo"] == object.data["id"] - - new_object = Object.get_by_ap_id(object.data["id"]) - assert new_object.data["replies_count"] == object.data["replies_count"] - - assert Enum.any?( - new_object.data["oneOf"], - fn - %{"name" => "suya..", "replies" => %{"totalItems" => 1}} -> true - _ -> false - end - ) - end - - test "outgoing, rewrites Answer to Note" do - user = insert(:user) - - {:ok, poll_activity} = - CommonAPI.post(user, %{ - status: "suya...", - poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10} - }) - - poll_object = Object.normalize(poll_activity) - # TODO: Replace with CommonAPI vote creation when implemented - data = - File.read!("test/fixtures/mastodon-vote.json") - |> Poison.decode!() - |> Kernel.put_in(["to"], user.ap_id) - |> Kernel.put_in(["object", "inReplyTo"], poll_object.data["id"]) - |> Kernel.put_in(["object", "to"], user.ap_id) - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) - - assert data["object"]["type"] == "Note" - end -end diff --git a/test/web/activity_pub/transmogrifier/article_handling_test.exs b/test/web/activity_pub/transmogrifier/article_handling_test.exs @@ -1,75 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.ArticleHandlingTest do - use Oban.Testing, repo: Pleroma.Repo - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Object.Fetcher - alias Pleroma.Web.ActivityPub.Transmogrifier - - test "Pterotype (Wordpress Plugin) Article" do - Tesla.Mock.mock(fn %{url: "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog"} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/wedistribute-user.json")} - end) - - data = - File.read!("test/fixtures/tesla_mock/wedistribute-create-article.json") |> Jason.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - object = Object.normalize(data["object"]) - - assert object.data["name"] == "The end is near: Mastodon plans to drop OStatus support" - - assert object.data["summary"] == - "One of the largest platforms in the federated social web is dropping the protocol that it started with." - - assert object.data["url"] == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/" - end - - test "Plume Article" do - Tesla.Mock.mock(fn - %{url: "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json") - } - - %{url: "https://baptiste.gelez.xyz/@/BaptisteGelez"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-user.json") - } - end) - - {:ok, object} = - Fetcher.fetch_object_from_id( - "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/" - ) - - assert object.data["name"] == "This Month in Plume: June 2018" - - assert object.data["url"] == - "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/" - end - - test "Prismo Article" do - Tesla.Mock.mock(fn %{url: "https://prismo.news/@mxb"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/https___prismo.news__mxb.json") - } - end) - - data = File.read!("test/fixtures/prismo-url-map.json") |> Jason.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - object = Object.normalize(data["object"]) - - assert object.data["url"] == "https://prismo.news/posts/83" - end -end diff --git a/test/web/activity_pub/transmogrifier/audio_handling_test.exs b/test/web/activity_pub/transmogrifier/audio_handling_test.exs @@ -1,83 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.AudioHandlingTest do - use Oban.Testing, repo: Pleroma.Repo - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Transmogrifier - - import Pleroma.Factory - - test "it works for incoming listens" do - _user = insert(:user, ap_id: "http://mastodon.example.org/users/admin") - - data = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "type" => "Listen", - "id" => "http://mastodon.example.org/users/admin/listens/1234/activity", - "actor" => "http://mastodon.example.org/users/admin", - "object" => %{ - "type" => "Audio", - "id" => "http://mastodon.example.org/users/admin/listens/1234", - "attributedTo" => "http://mastodon.example.org/users/admin", - "title" => "lain radio episode 1", - "artist" => "lain", - "album" => "lain radio", - "length" => 180_000 - } - } - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - - object = Object.normalize(activity) - - assert object.data["title"] == "lain radio episode 1" - assert object.data["artist"] == "lain" - assert object.data["album"] == "lain radio" - assert object.data["length"] == 180_000 - end - - test "Funkwhale Audio object" do - Tesla.Mock.mock(fn - %{url: "https://channels.tests.funkwhale.audio/federation/actors/compositions"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/funkwhale_channel.json") - } - end) - - data = File.read!("test/fixtures/tesla_mock/funkwhale_create_audio.json") |> Poison.decode!() - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - - assert object = Object.normalize(activity, false) - - assert object.data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] - - assert object.data["cc"] == [] - - assert object.data["url"] == "https://channels.tests.funkwhale.audio/library/tracks/74" - - assert object.data["attachment"] == [ - %{ - "mediaType" => "audio/ogg", - "type" => "Link", - "name" => nil, - "url" => [ - %{ - "href" => - "https://channels.tests.funkwhale.audio/api/v1/listen/3901e5d8-0445-49d5-9711-e096cf32e515/?upload=42342395-0208-4fee-a38d-259a6dae0871&download=false", - "mediaType" => "audio/ogg", - "type" => "Link" - } - ] - } - ] - end -end diff --git a/test/web/activity_pub/transmogrifier/block_handling_test.exs b/test/web/activity_pub/transmogrifier/block_handling_test.exs @@ -1,63 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.BlockHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Transmogrifier - - import Pleroma.Factory - - test "it works for incoming blocks" do - user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-block-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - blocker = insert(:user, ap_id: data["actor"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Block" - assert data["object"] == user.ap_id - assert data["actor"] == "http://mastodon.example.org/users/admin" - - assert User.blocks?(blocker, user) - end - - test "incoming blocks successfully tear down any follow relationship" do - blocker = insert(:user) - blocked = insert(:user) - - data = - File.read!("test/fixtures/mastodon-block-activity.json") - |> Poison.decode!() - |> Map.put("object", blocked.ap_id) - |> Map.put("actor", blocker.ap_id) - - {:ok, blocker} = User.follow(blocker, blocked) - {:ok, blocked} = User.follow(blocked, blocker) - - assert User.following?(blocker, blocked) - assert User.following?(blocked, blocker) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Block" - assert data["object"] == blocked.ap_id - assert data["actor"] == blocker.ap_id - - blocker = User.get_cached_by_ap_id(data["actor"]) - blocked = User.get_cached_by_ap_id(data["object"]) - - assert User.blocks?(blocker, blocked) - - refute User.following?(blocker, blocked) - refute User.following?(blocked, blocker) - end -end diff --git a/test/web/activity_pub/transmogrifier/chat_message_test.exs b/test/web/activity_pub/transmogrifier/chat_message_test.exs @@ -1,171 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Activity - alias Pleroma.Chat - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Transmogrifier - - describe "handle_incoming" do - test "handles chonks with attachment" do - data = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "actor" => "https://honk.tedunangst.com/u/tedu", - "id" => "https://honk.tedunangst.com/u/tedu/honk/x6gt8X8PcyGkQcXxzg1T", - "object" => %{ - "attachment" => [ - %{ - "mediaType" => "image/jpeg", - "name" => "298p3RG7j27tfsZ9RQ.jpg", - "summary" => "298p3RG7j27tfsZ9RQ.jpg", - "type" => "Document", - "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg" - } - ], - "attributedTo" => "https://honk.tedunangst.com/u/tedu", - "content" => "", - "id" => "https://honk.tedunangst.com/u/tedu/chonk/26L4wl5yCbn4dr4y1b", - "published" => "2020-05-18T01:13:03Z", - "to" => [ - "https://dontbulling.me/users/lain" - ], - "type" => "ChatMessage" - }, - "published" => "2020-05-18T01:13:03Z", - "to" => [ - "https://dontbulling.me/users/lain" - ], - "type" => "Create" - } - - _user = insert(:user, ap_id: data["actor"]) - _user = insert(:user, ap_id: hd(data["to"])) - - assert {:ok, _activity} = Transmogrifier.handle_incoming(data) - end - - test "it rejects messages that don't contain content" do - data = - File.read!("test/fixtures/create-chat-message.json") - |> Poison.decode!() - - object = - data["object"] - |> Map.delete("content") - - data = - data - |> Map.put("object", object) - - _author = - insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) - - _recipient = - insert(:user, - ap_id: List.first(data["to"]), - local: true, - last_refreshed_at: DateTime.utc_now() - ) - - {:error, _} = Transmogrifier.handle_incoming(data) - end - - test "it rejects messages that don't concern local users" do - data = - File.read!("test/fixtures/create-chat-message.json") - |> Poison.decode!() - - _author = - insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) - - _recipient = - insert(:user, - ap_id: List.first(data["to"]), - local: false, - last_refreshed_at: DateTime.utc_now() - ) - - {:error, _} = Transmogrifier.handle_incoming(data) - end - - test "it rejects messages where the `to` field of activity and object don't match" do - data = - File.read!("test/fixtures/create-chat-message.json") - |> Poison.decode!() - - author = insert(:user, ap_id: data["actor"]) - _recipient = insert(:user, ap_id: List.first(data["to"])) - - data = - data - |> Map.put("to", author.ap_id) - - assert match?({:error, _}, Transmogrifier.handle_incoming(data)) - refute Object.get_by_ap_id(data["object"]["id"]) - end - - test "it fetches the actor if they aren't in our system" do - Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - - data = - File.read!("test/fixtures/create-chat-message.json") - |> Poison.decode!() - |> Map.put("actor", "http://mastodon.example.org/users/admin") - |> put_in(["object", "actor"], "http://mastodon.example.org/users/admin") - - _recipient = insert(:user, ap_id: List.first(data["to"]), local: true) - - {:ok, %Activity{} = _activity} = Transmogrifier.handle_incoming(data) - end - - test "it doesn't work for deactivated users" do - data = - File.read!("test/fixtures/create-chat-message.json") - |> Poison.decode!() - - _author = - insert(:user, - ap_id: data["actor"], - local: false, - last_refreshed_at: DateTime.utc_now(), - deactivated: true - ) - - _recipient = insert(:user, ap_id: List.first(data["to"]), local: true) - - assert {:error, _} = Transmogrifier.handle_incoming(data) - end - - test "it inserts it and creates a chat" do - data = - File.read!("test/fixtures/create-chat-message.json") - |> Poison.decode!() - - author = - insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) - - recipient = insert(:user, ap_id: List.first(data["to"]), local: true) - - {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(data) - assert activity.local == false - - assert activity.actor == author.ap_id - assert activity.recipients == [recipient.ap_id, author.ap_id] - - %Object{} = object = Object.get_by_ap_id(activity.data["object"]) - - assert object - assert object.data["content"] == "You expected a cute girl? Too bad. alert(&#39;XSS&#39;)" - assert match?(%{"firefox" => _}, object.data["emoji"]) - - refute Chat.get(author.id, recipient.ap_id) - assert Chat.get(recipient.id, author.ap_id) - end - end -end diff --git a/test/web/activity_pub/transmogrifier/delete_handling_test.exs b/test/web/activity_pub/transmogrifier/delete_handling_test.exs @@ -1,114 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.DeleteHandlingTest do - use Oban.Testing, repo: Pleroma.Repo - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Transmogrifier - - import Pleroma.Factory - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "it works for incoming deletes" do - activity = insert(:note_activity) - deleting_user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-delete.json") - |> Poison.decode!() - |> Map.put("actor", deleting_user.ap_id) - |> put_in(["object", "id"], activity.data["object"]) - - {:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} = - Transmogrifier.handle_incoming(data) - - assert id == data["id"] - - # We delete the Create activity because we base our timelines on it. - # This should be changed after we unify objects and activities - refute Activity.get_by_id(activity.id) - assert actor == deleting_user.ap_id - - # Objects are replaced by a tombstone object. - object = Object.normalize(activity.data["object"]) - assert object.data["type"] == "Tombstone" - end - - test "it works for incoming when the object has been pruned" do - activity = insert(:note_activity) - - {:ok, object} = - Object.normalize(activity.data["object"]) - |> Repo.delete() - - Cachex.del(:object_cache, "object:#{object.data["id"]}") - - deleting_user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-delete.json") - |> Poison.decode!() - |> Map.put("actor", deleting_user.ap_id) - |> put_in(["object", "id"], activity.data["object"]) - - {:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} = - Transmogrifier.handle_incoming(data) - - assert id == data["id"] - - # We delete the Create activity because we base our timelines on it. - # This should be changed after we unify objects and activities - refute Activity.get_by_id(activity.id) - assert actor == deleting_user.ap_id - end - - test "it fails for incoming deletes with spoofed origin" do - activity = insert(:note_activity) - %{ap_id: ap_id} = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo") - - data = - File.read!("test/fixtures/mastodon-delete.json") - |> Poison.decode!() - |> Map.put("actor", ap_id) - |> put_in(["object", "id"], activity.data["object"]) - - assert match?({:error, _}, Transmogrifier.handle_incoming(data)) - end - - @tag capture_log: true - test "it works for incoming user deletes" do - %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin") - - data = - File.read!("test/fixtures/mastodon-delete-user.json") - |> Poison.decode!() - - {:ok, _} = Transmogrifier.handle_incoming(data) - ObanHelpers.perform_all() - - assert User.get_cached_by_ap_id(ap_id).deactivated - end - - test "it fails for incoming user deletes with spoofed origin" do - %{ap_id: ap_id} = insert(:user) - - data = - File.read!("test/fixtures/mastodon-delete-user.json") - |> Poison.decode!() - |> Map.put("actor", ap_id) - - assert match?({:error, _}, Transmogrifier.handle_incoming(data)) - - assert User.get_cached_by_ap_id(ap_id) - end -end diff --git a/test/web/activity_pub/transmogrifier/emoji_react_handling_test.exs b/test/web/activity_pub/transmogrifier/emoji_react_handling_test.exs @@ -1,61 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "it works for incoming emoji reactions" do - user = insert(:user) - other_user = insert(:user, local: false) - {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) - - data = - File.read!("test/fixtures/emoji-reaction.json") - |> Poison.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"] == "http://mastodon.example.org/users/admin#reactions/2" - assert data["object"] == activity.data["object"] - assert data["content"] == "👌" - - object = Object.get_by_ap_id(data["object"]) - - assert object.data["reaction_count"] == 1 - assert match?([["👌", _]], object.data["reactions"]) - end - - test "it reject invalid emoji reactions" do - user = insert(:user) - other_user = insert(:user, local: false) - {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) - - data = - File.read!("test/fixtures/emoji-reaction-too-long.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - |> Map.put("actor", other_user.ap_id) - - assert {:error, _} = Transmogrifier.handle_incoming(data) - - data = - File.read!("test/fixtures/emoji-reaction-no-emoji.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - |> Map.put("actor", other_user.ap_id) - - assert {:error, _} = Transmogrifier.handle_incoming(data) - end -end diff --git a/test/web/activity_pub/transmogrifier/event_handling_test.exs b/test/web/activity_pub/transmogrifier/event_handling_test.exs @@ -1,40 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.EventHandlingTest do - use Oban.Testing, repo: Pleroma.Repo - use Pleroma.DataCase - - alias Pleroma.Object.Fetcher - - test "Mobilizon Event object" do - Tesla.Mock.mock(fn - %{url: "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/mobilizon.org-event.json") - } - - %{url: "https://mobilizon.org/@tcit"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/mobilizon.org-user.json") - } - end) - - assert {:ok, object} = - Fetcher.fetch_object_from_id( - "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" - ) - - assert object.data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] - assert object.data["cc"] == [] - - assert object.data["url"] == - "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" - - assert object.data["published"] == "2019-12-17T11:33:56Z" - assert object.data["name"] == "Mobilizon Launching Party" - end -end diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs @@ -1,208 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do - use Pleroma.DataCase - alias Pleroma.Activity - alias Pleroma.Notification - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.ActivityPub.Utils - - import Pleroma.Factory - import Ecto.Query - import Mock - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe "handle_incoming" do - setup do: clear_config([:user, :deny_follow_blocked]) - - test "it works for osada follow request" do - user = insert(:user) - - data = - File.read!("test/fixtures/osada-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "https://apfed.club/channel/indio" - assert data["type"] == "Follow" - assert data["id"] == "https://apfed.club/follow/9" - - activity = Repo.get(Activity, activity.id) - assert activity.data["state"] == "accept" - assert User.following?(User.get_cached_by_ap_id(data["actor"]), user) - end - - test "it works for incoming follow requests" do - user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "Follow" - assert data["id"] == "http://mastodon.example.org/users/admin#follows/2" - - activity = Repo.get(Activity, activity.id) - assert activity.data["state"] == "accept" - assert User.following?(User.get_cached_by_ap_id(data["actor"]), user) - - [notification] = Notification.for_user(user) - assert notification.type == "follow" - end - - test "with locked accounts, it does create a Follow, but not an Accept" do - user = insert(:user, locked: true) - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["state"] == "pending" - - refute User.following?(User.get_cached_by_ap_id(data["actor"]), user) - - accepts = - from( - a in Activity, - where: fragment("?->>'type' = ?", a.data, "Accept") - ) - |> Repo.all() - - assert Enum.empty?(accepts) - - [notification] = Notification.for_user(user) - assert notification.type == "follow_request" - end - - test "it works for follow requests when you are already followed, creating a new accept activity" do - # This is important because the remote might have the wrong idea about the - # current follow status. This can lead to instance A thinking that x@A is - # followed by y@B, but B thinks they are not. In this case, the follow can - # never go through again because it will never get an Accept. - user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) - - accepts = - from( - a in Activity, - where: fragment("?->>'type' = ?", a.data, "Accept") - ) - |> Repo.all() - - assert length(accepts) == 1 - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("id", String.replace(data["id"], "2", "3")) - |> Map.put("object", user.ap_id) - - {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) - - accepts = - from( - a in Activity, - where: fragment("?->>'type' = ?", a.data, "Accept") - ) - |> Repo.all() - - assert length(accepts) == 2 - end - - test "it rejects incoming follow requests from blocked users when deny_follow_blocked is enabled" do - Pleroma.Config.put([:user, :deny_follow_blocked], true) - - user = insert(:user) - {:ok, target} = User.get_or_fetch("http://mastodon.example.org/users/admin") - - {:ok, _user_relationship} = User.block(user, target) - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - {:ok, %Activity{data: %{"id" => id}}} = Transmogrifier.handle_incoming(data) - - %Activity{} = activity = Activity.get_by_ap_id(id) - - assert activity.data["state"] == "reject" - end - - test "it rejects incoming follow requests if the following errors for some reason" do - user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - with_mock Pleroma.User, [:passthrough], follow: fn _, _, _ -> {:error, :testing} end do - {:ok, %Activity{data: %{"id" => id}}} = Transmogrifier.handle_incoming(data) - - %Activity{} = activity = Activity.get_by_ap_id(id) - - assert activity.data["state"] == "reject" - end - end - - test "it works for incoming follow requests from hubzilla" do - user = insert(:user) - - data = - File.read!("test/fixtures/hubzilla-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - |> Utils.normalize_params() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "https://hubzilla.example.org/channel/kaniini" - assert data["type"] == "Follow" - assert data["id"] == "https://hubzilla.example.org/channel/kaniini#follows/2" - assert User.following?(User.get_cached_by_ap_id(data["actor"]), user) - end - - test "it works for incoming follows to locked account" do - pending_follower = insert(:user, ap_id: "http://mastodon.example.org/users/admin") - user = insert(:user, locked: true) - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Follow" - assert data["object"] == user.ap_id - assert data["state"] == "pending" - assert data["actor"] == "http://mastodon.example.org/users/admin" - - assert [^pending_follower] = User.get_follow_requests(user) - end - end -end diff --git a/test/web/activity_pub/transmogrifier/like_handling_test.exs b/test/web/activity_pub/transmogrifier/like_handling_test.exs @@ -1,78 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "it works for incoming likes" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) - - data = - File.read!("test/fixtures/mastodon-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - _actor = insert(:user, ap_id: data["actor"], local: false) - - {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) - - refute Enum.empty?(activity.recipients) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "Like" - assert data["id"] == "http://mastodon.example.org/users/admin#likes/2" - assert data["object"] == activity.data["object"] - end - - test "it works for incoming misskey likes, turning them into EmojiReacts" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) - - data = - File.read!("test/fixtures/misskey-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - _actor = insert(:user, ap_id: data["actor"], local: false) - - {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data) - - assert activity_data["actor"] == data["actor"] - assert activity_data["type"] == "EmojiReact" - assert activity_data["id"] == data["id"] - assert activity_data["object"] == activity.data["object"] - assert activity_data["content"] == "🍮" - end - - test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReacts" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) - - data = - File.read!("test/fixtures/misskey-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - |> Map.put("_misskey_reaction", "⭐") - - _actor = insert(:user, ap_id: data["actor"], local: false) - - {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data) - - assert activity_data["actor"] == data["actor"] - assert activity_data["type"] == "EmojiReact" - assert activity_data["id"] == data["id"] - assert activity_data["object"] == activity.data["object"] - assert activity_data["content"] == "⭐" - end -end diff --git a/test/web/activity_pub/transmogrifier/question_handling_test.exs b/test/web/activity_pub/transmogrifier/question_handling_test.exs @@ -1,176 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.QuestionHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "Mastodon Question activity" do - data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!() - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - - object = Object.normalize(activity, false) - - assert object.data["url"] == "https://mastodon.sdf.org/@rinpatch/102070944809637304" - - assert object.data["closed"] == "2019-05-11T09:03:36Z" - - assert object.data["context"] == activity.data["context"] - - 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"]) == - Enum.sort([ - %{ - "name" => "25 char limit is dumb", - "replies" => %{"totalItems" => 0, "type" => "Collection"}, - "type" => "Note" - }, - %{ - "name" => "Dunno", - "replies" => %{"totalItems" => 0, "type" => "Collection"}, - "type" => "Note" - }, - %{ - "name" => "Everyone knows that!", - "replies" => %{"totalItems" => 1, "type" => "Collection"}, - "type" => "Note" - }, - %{ - "name" => "I can't even fit a funny", - "replies" => %{"totalItems" => 1, "type" => "Collection"}, - "type" => "Note" - } - ]) - - user = insert(:user) - - {:ok, reply_activity} = CommonAPI.post(user, %{status: "hewwo", in_reply_to_id: activity.id}) - - reply_object = Object.normalize(reply_activity, 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 - options = [ - %{ - "type" => "Note", - "name" => "<input type=\"date\">", - "replies" => %{"totalItems" => 0, "type" => "Collection"} - }, - %{ - "type" => "Note", - "name" => "<input type=\"date\"/>", - "replies" => %{"totalItems" => 0, "type" => "Collection"} - }, - %{ - "type" => "Note", - "name" => "<input type=\"date\" />", - "replies" => %{"totalItems" => 1, "type" => "Collection"} - }, - %{ - "type" => "Note", - "name" => "<input type=\"date\"></input>", - "replies" => %{"totalItems" => 1, "type" => "Collection"} - } - ] - - data = - File.read!("test/fixtures/mastodon-question-activity.json") - |> Poison.decode!() - |> Kernel.put_in(["object", "oneOf"], options) - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - object = Object.normalize(activity, false) - - assert Enum.sort(object.data["oneOf"]) == Enum.sort(options) - end - - test "Mastodon Question activity with custom emojis" do - options = [ - %{ - "type" => "Note", - "name" => ":blobcat:", - "replies" => %{"totalItems" => 0, "type" => "Collection"} - }, - %{ - "type" => "Note", - "name" => ":blobfox:", - "replies" => %{"totalItems" => 0, "type" => "Collection"} - } - ] - - tag = [ - %{ - "icon" => %{ - "type" => "Image", - "url" => "https://blob.cat/emoji/custom/blobcats/blobcat.png" - }, - "id" => "https://blob.cat/emoji/custom/blobcats/blobcat.png", - "name" => ":blobcat:", - "type" => "Emoji", - "updated" => "1970-01-01T00:00:00Z" - }, - %{ - "icon" => %{"type" => "Image", "url" => "https://blob.cat/emoji/blobfox/blobfox.png"}, - "id" => "https://blob.cat/emoji/blobfox/blobfox.png", - "name" => ":blobfox:", - "type" => "Emoji", - "updated" => "1970-01-01T00:00:00Z" - } - ] - - data = - File.read!("test/fixtures/mastodon-question-activity.json") - |> Poison.decode!() - |> Kernel.put_in(["object", "oneOf"], options) - |> Kernel.put_in(["object", "tag"], tag) - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - object = Object.normalize(activity, false) - - assert object.data["oneOf"] == options - - assert object.data["emoji"] == %{ - "blobcat" => "https://blob.cat/emoji/custom/blobcats/blobcat.png", - "blobfox" => "https://blob.cat/emoji/blobfox/blobfox.png" - } - end - - test "returns same activity if received a second time" do - data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!() - - assert {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - - assert {:ok, ^activity} = Transmogrifier.handle_incoming(data) - end - - test "accepts a Question with no content" do - data = - File.read!("test/fixtures/mastodon-question-activity.json") - |> Poison.decode!() - |> Kernel.put_in(["object", "content"], "") - - assert {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) - end -end diff --git a/test/web/activity_pub/transmogrifier/reject_handling_test.exs b/test/web/activity_pub/transmogrifier/reject_handling_test.exs @@ -1,67 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.RejectHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "it fails for incoming rejects which cannot be correlated" do - follower = insert(:user) - followed = insert(:user, locked: true) - - accept_data = - File.read!("test/fixtures/mastodon-reject-activity.json") - |> Poison.decode!() - |> Map.put("actor", followed.ap_id) - - accept_data = - Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) - - {:error, _} = Transmogrifier.handle_incoming(accept_data) - - follower = User.get_cached_by_id(follower.id) - - refute User.following?(follower, followed) == true - end - - test "it works for incoming rejects which are referenced by IRI only" do - follower = insert(:user) - followed = insert(:user, locked: true) - - {:ok, follower} = User.follow(follower, followed) - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) - - assert User.following?(follower, followed) == true - - reject_data = - File.read!("test/fixtures/mastodon-reject-activity.json") - |> Poison.decode!() - |> Map.put("actor", followed.ap_id) - |> Map.put("object", follow_activity.data["id"]) - - {:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data) - - follower = User.get_cached_by_id(follower.id) - - assert User.following?(follower, followed) == false - end - - test "it rejects activities without a valid ID" do - user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - |> Map.put("id", "") - - :error = Transmogrifier.handle_incoming(data) - end -end diff --git a/test/web/activity_pub/transmogrifier/undo_handling_test.exs b/test/web/activity_pub/transmogrifier/undo_handling_test.exs @@ -1,185 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.UndoHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "it works for incoming emoji reaction undos" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) - {:ok, reaction_activity} = CommonAPI.react_with_emoji(activity.id, user, "👌") - - data = - File.read!("test/fixtures/mastodon-undo-like.json") - |> Poison.decode!() - |> Map.put("object", reaction_activity.data["id"]) - |> Map.put("actor", user.ap_id) - - {:ok, activity} = Transmogrifier.handle_incoming(data) - - assert activity.actor == user.ap_id - assert activity.data["id"] == data["id"] - assert activity.data["type"] == "Undo" - end - - test "it returns an error for incoming unlikes wihout a like activity" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "leave a like pls"}) - - data = - File.read!("test/fixtures/mastodon-undo-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - assert Transmogrifier.handle_incoming(data) == :error - end - - test "it works for incoming unlikes with an existing like activity" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "leave a like pls"}) - - like_data = - File.read!("test/fixtures/mastodon-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - _liker = insert(:user, ap_id: like_data["actor"], local: false) - - {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data) - - data = - File.read!("test/fixtures/mastodon-undo-like.json") - |> Poison.decode!() - |> Map.put("object", like_data) - |> Map.put("actor", like_data["actor"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "Undo" - assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo" - assert data["object"] == "http://mastodon.example.org/users/admin#likes/2" - - note = Object.get_by_ap_id(like_data["object"]) - assert note.data["like_count"] == 0 - assert note.data["likes"] == [] - end - - test "it works for incoming unlikes with an existing like activity and a compact object" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "leave a like pls"}) - - like_data = - File.read!("test/fixtures/mastodon-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - _liker = insert(:user, ap_id: like_data["actor"], local: false) - - {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data) - - data = - File.read!("test/fixtures/mastodon-undo-like.json") - |> Poison.decode!() - |> Map.put("object", like_data["id"]) - |> Map.put("actor", like_data["actor"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "Undo" - assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo" - assert data["object"] == "http://mastodon.example.org/users/admin#likes/2" - end - - test "it works for incoming unannounces with an existing notice" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - - announce_data = - File.read!("test/fixtures/mastodon-announce.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - _announcer = insert(:user, ap_id: announce_data["actor"], local: false) - - {:ok, %Activity{data: announce_data, local: false}} = - Transmogrifier.handle_incoming(announce_data) - - data = - File.read!("test/fixtures/mastodon-undo-announce.json") - |> Poison.decode!() - |> Map.put("object", announce_data) - |> Map.put("actor", announce_data["actor"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Undo" - - assert data["object"] == - "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" - end - - test "it works for incoming unfollows with an existing follow" do - user = insert(:user) - - follow_data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - _follower = insert(:user, ap_id: follow_data["actor"], local: false) - - {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data) - - data = - File.read!("test/fixtures/mastodon-unfollow-activity.json") - |> Poison.decode!() - |> Map.put("object", follow_data) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Undo" - assert data["object"]["type"] == "Follow" - assert data["object"]["object"] == user.ap_id - assert data["actor"] == "http://mastodon.example.org/users/admin" - - refute User.following?(User.get_cached_by_ap_id(data["actor"]), user) - end - - test "it works for incoming unblocks with an existing block" do - user = insert(:user) - - block_data = - File.read!("test/fixtures/mastodon-block-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - _blocker = insert(:user, ap_id: block_data["actor"], local: false) - - {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data) - - data = - File.read!("test/fixtures/mastodon-unblock-activity.json") - |> Poison.decode!() - |> Map.put("object", block_data) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - assert data["type"] == "Undo" - assert data["object"] == block_data["id"] - - blocker = User.get_cached_by_ap_id(data["actor"]) - - refute User.blocks?(blocker, user) - end -end diff --git a/test/web/activity_pub/transmogrifier/user_update_handling_test.exs b/test/web/activity_pub/transmogrifier/user_update_handling_test.exs @@ -1,159 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.UserUpdateHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Transmogrifier - - import Pleroma.Factory - - test "it works for incoming update activities" do - user = insert(:user, local: false) - - update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() - - object = - update_data["object"] - |> Map.put("actor", user.ap_id) - |> Map.put("id", user.ap_id) - - update_data = - update_data - |> Map.put("actor", user.ap_id) - |> Map.put("object", object) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data) - - assert data["id"] == update_data["id"] - - user = User.get_cached_by_ap_id(data["actor"]) - assert user.name == "gargle" - - assert user.avatar["url"] == [ - %{ - "href" => - "https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg" - } - ] - - assert user.banner["url"] == [ - %{ - "href" => - "https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" - } - ] - - assert user.bio == "<p>Some bio</p>" - end - - test "it works with alsoKnownAs" do - %{ap_id: actor} = insert(:user, local: false) - - assert User.get_cached_by_ap_id(actor).also_known_as == [] - - {:ok, _activity} = - "test/fixtures/mastodon-update.json" - |> File.read!() - |> Poison.decode!() - |> Map.put("actor", actor) - |> Map.update!("object", fn object -> - object - |> Map.put("actor", actor) - |> Map.put("id", actor) - |> Map.put("alsoKnownAs", [ - "http://mastodon.example.org/users/foo", - "http://example.org/users/bar" - ]) - end) - |> Transmogrifier.handle_incoming() - - assert User.get_cached_by_ap_id(actor).also_known_as == [ - "http://mastodon.example.org/users/foo", - "http://example.org/users/bar" - ] - end - - test "it works with custom profile fields" do - user = insert(:user, local: false) - - assert user.fields == [] - - update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() - - object = - update_data["object"] - |> Map.put("actor", user.ap_id) - |> Map.put("id", user.ap_id) - - update_data = - update_data - |> Map.put("actor", user.ap_id) - |> Map.put("object", object) - - {:ok, _update_activity} = Transmogrifier.handle_incoming(update_data) - - user = User.get_cached_by_ap_id(user.ap_id) - - assert user.fields == [ - %{"name" => "foo", "value" => "updated"}, - %{"name" => "foo1", "value" => "updated"} - ] - - Pleroma.Config.put([:instance, :max_remote_account_fields], 2) - - update_data = - update_data - |> put_in(["object", "attachment"], [ - %{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}, - %{"name" => "foo11", "type" => "PropertyValue", "value" => "bar11"}, - %{"name" => "foo22", "type" => "PropertyValue", "value" => "bar22"} - ]) - |> Map.put("id", update_data["id"] <> ".") - - {:ok, _} = Transmogrifier.handle_incoming(update_data) - - user = User.get_cached_by_ap_id(user.ap_id) - - assert user.fields == [ - %{"name" => "foo", "value" => "updated"}, - %{"name" => "foo1", "value" => "updated"} - ] - - update_data = - update_data - |> put_in(["object", "attachment"], []) - |> Map.put("id", update_data["id"] <> ".") - - {:ok, _} = Transmogrifier.handle_incoming(update_data) - - user = User.get_cached_by_ap_id(user.ap_id) - - assert user.fields == [] - end - - test "it works for incoming update activities which lock the account" do - user = insert(:user, local: false) - - update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() - - object = - update_data["object"] - |> Map.put("actor", user.ap_id) - |> Map.put("id", user.ap_id) - |> Map.put("manuallyApprovesFollowers", true) - - update_data = - update_data - |> Map.put("actor", user.ap_id) - |> Map.put("object", object) - - {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(update_data) - - user = User.get_cached_by_ap_id(user.ap_id) - assert user.locked == true - end -end diff --git a/test/web/activity_pub/transmogrifier/video_handling_test.exs b/test/web/activity_pub/transmogrifier/video_handling_test.exs @@ -1,93 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.VideoHandlingTest do - use Oban.Testing, repo: Pleroma.Repo - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Object.Fetcher - alias Pleroma.Web.ActivityPub.Transmogrifier - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "skip converting the content when it is nil" do - data = - File.read!("test/fixtures/tesla_mock/framatube.org-video.json") - |> Jason.decode!() - |> Kernel.put_in(["object", "content"], nil) - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - - assert object = Object.normalize(activity, false) - - assert object.data["content"] == nil - end - - test "it converts content of object to html" do - data = File.read!("test/fixtures/tesla_mock/framatube.org-video.json") |> Jason.decode!() - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - - assert object = Object.normalize(activity, false) - - assert object.data["content"] == - "<p>Après avoir mené avec un certain succès la campagne « Dégooglisons Internet » en 2014, l’association Framasoft annonce fin 2019 arrêter progressivement un certain nombre de ses services alternatifs aux GAFAM. Pourquoi ?</p><p>Transcription par @aprilorg ici : <a href=\"https://www.april.org/deframasoftisons-internet-pierre-yves-gosset-framasoft\">https://www.april.org/deframasoftisons-internet-pierre-yves-gosset-framasoft</a></p>" - end - - test "it remaps video URLs as attachments if necessary" do - {:ok, object} = - Fetcher.fetch_object_from_id( - "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" - ) - - assert object.data["url"] == - "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" - - assert object.data["attachment"] == [ - %{ - "type" => "Link", - "mediaType" => "video/mp4", - "name" => nil, - "url" => [ - %{ - "href" => - "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - } - ] - - data = File.read!("test/fixtures/tesla_mock/framatube.org-video.json") |> Jason.decode!() - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - - assert object = Object.normalize(activity, false) - - assert object.data["attachment"] == [ - %{ - "type" => "Link", - "mediaType" => "video/mp4", - "name" => nil, - "url" => [ - %{ - "href" => - "https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - } - ] - - assert object.data["url"] == - "https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206" - end -end diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs @@ -1,1220 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do - use Oban.Testing, repo: Pleroma.Repo - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.AdminAPI.AccountView - alias Pleroma.Web.CommonAPI - - import Mock - import Pleroma.Factory - import ExUnit.CaptureLog - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup do: clear_config([:instance, :max_remote_account_fields]) - - describe "handle_incoming" do - test "it works for incoming notices with tag not being an array (kroeg)" do - data = File.read!("test/fixtures/kroeg-array-less-emoji.json") |> Poison.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - object = Object.normalize(data["object"]) - - assert object.data["emoji"] == %{ - "icon_e_smile" => "https://puckipedia.com/forum/images/smilies/icon_e_smile.png" - } - - data = File.read!("test/fixtures/kroeg-array-less-hashtag.json") |> Poison.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - object = Object.normalize(data["object"]) - - assert "test" in object.data["tag"] - end - - test "it cleans up incoming notices which are not really DMs" do - user = insert(:user) - other_user = insert(:user) - - to = [user.ap_id, other_user.ap_id] - - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - |> Map.put("to", to) - |> Map.put("cc", []) - - object = - data["object"] - |> Map.put("to", to) - |> Map.put("cc", []) - - data = Map.put(data, "object", object) - - {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) - - assert data["to"] == [] - assert data["cc"] == to - - object_data = Object.normalize(activity).data - - assert object_data["to"] == [] - assert object_data["cc"] == to - end - - test "it ignores an incoming notice if we already have it" do - activity = insert(:note_activity) - - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - |> Map.put("object", Object.normalize(activity).data) - - {:ok, returned_activity} = Transmogrifier.handle_incoming(data) - - assert activity == returned_activity - end - - @tag capture_log: true - test "it fetches reply-to activities if we don't have them" do - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - - object = - data["object"] - |> Map.put("inReplyTo", "https://mstdn.io/users/mayuutann/statuses/99568293732299394") - - data = Map.put(data, "object", object) - {:ok, returned_activity} = Transmogrifier.handle_incoming(data) - returned_object = Object.normalize(returned_activity, false) - - assert activity = - Activity.get_create_by_object_ap_id( - "https://mstdn.io/users/mayuutann/statuses/99568293732299394" - ) - - assert returned_object.data["inReplyTo"] == - "https://mstdn.io/users/mayuutann/statuses/99568293732299394" - end - - test "it does not fetch reply-to activities beyond max replies depth limit" do - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - - object = - data["object"] - |> Map.put("inReplyTo", "https://shitposter.club/notice/2827873") - - data = Map.put(data, "object", object) - - with_mock Pleroma.Web.Federator, - allowed_thread_distance?: fn _ -> false end do - {:ok, returned_activity} = Transmogrifier.handle_incoming(data) - - returned_object = Object.normalize(returned_activity, false) - - refute Activity.get_create_by_object_ap_id( - "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" - ) - - assert returned_object.data["inReplyTo"] == "https://shitposter.club/notice/2827873" - end - end - - test "it does not crash if the object in inReplyTo can't be fetched" do - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - - object = - data["object"] - |> Map.put("inReplyTo", "https://404.site/whatever") - - data = - data - |> Map.put("object", object) - - assert capture_log(fn -> - {:ok, _returned_activity} = Transmogrifier.handle_incoming(data) - end) =~ "[warn] Couldn't fetch \"https://404.site/whatever\", error: nil" - end - - test "it does not work for deactivated users" do - data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() - - insert(:user, ap_id: data["actor"], deactivated: true) - - assert {:error, _} = Transmogrifier.handle_incoming(data) - end - - test "it works for incoming notices" do - data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["id"] == - "http://mastodon.example.org/users/admin/statuses/99512778738411822/activity" - - assert data["context"] == - "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation" - - assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] - - assert data["cc"] == [ - "http://mastodon.example.org/users/admin/followers", - "http://localtesting.pleroma.lol/users/lain" - ] - - assert data["actor"] == "http://mastodon.example.org/users/admin" - - object_data = Object.normalize(data["object"]).data - - assert object_data["id"] == - "http://mastodon.example.org/users/admin/statuses/99512778738411822" - - assert object_data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] - - assert object_data["cc"] == [ - "http://mastodon.example.org/users/admin/followers", - "http://localtesting.pleroma.lol/users/lain" - ] - - assert object_data["actor"] == "http://mastodon.example.org/users/admin" - assert object_data["attributedTo"] == "http://mastodon.example.org/users/admin" - - assert object_data["context"] == - "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation" - - assert object_data["sensitive"] == true - - user = User.get_cached_by_ap_id(object_data["actor"]) - - assert user.note_count == 1 - end - - test "it works for incoming notices with hashtags" do - data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Poison.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - object = Object.normalize(data["object"]) - - assert Enum.at(object.data["tag"], 2) == "moo" - end - - test "it works for incoming notices with contentMap" do - data = - File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - object = Object.normalize(data["object"]) - - 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") |> Poison.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - object = Object.normalize(data["object"]) - - assert object.data["content"] == - "<p>henlo from my Psion netBook</p><p>message sent from my Psion netBook</p>" - end - - test "it ensures that as:Public activities make it to their followers collection" do - user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - |> Map.put("actor", user.ap_id) - |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"]) - |> Map.put("cc", []) - - object = - data["object"] - |> Map.put("attributedTo", user.ap_id) - |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"]) - |> Map.put("cc", []) - |> Map.put("id", user.ap_id <> "/activities/12345678") - - data = Map.put(data, "object", object) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["cc"] == [User.ap_followers(user)] - end - - test "it ensures that address fields become lists" do - user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - |> Map.put("actor", user.ap_id) - |> Map.put("to", nil) - |> Map.put("cc", nil) - - object = - data["object"] - |> Map.put("attributedTo", user.ap_id) - |> Map.put("to", nil) - |> Map.put("cc", nil) - |> Map.put("id", user.ap_id <> "/activities/12345678") - - data = Map.put(data, "object", object) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert !is_nil(data["to"]) - assert !is_nil(data["cc"]) - end - - test "it strips internal likes" do - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - - likes = %{ - "first" => - "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1", - "id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes", - "totalItems" => 3, - "type" => "OrderedCollection" - } - - object = Map.put(data["object"], "likes", likes) - data = Map.put(data, "object", object) - - {:ok, %Activity{object: object}} = Transmogrifier.handle_incoming(data) - - refute Map.has_key?(object.data, "likes") - end - - test "it strips internal reactions" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "📢") - - %{object: object} = Activity.get_by_id_with_object(activity.id) - assert Map.has_key?(object.data, "reactions") - assert Map.has_key?(object.data, "reaction_count") - - object_data = Transmogrifier.strip_internal_fields(object.data) - refute Map.has_key?(object_data, "reactions") - refute Map.has_key?(object_data, "reaction_count") - end - - test "it works for incoming unfollows with an existing follow" do - user = insert(:user) - - follow_data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data) - - data = - File.read!("test/fixtures/mastodon-unfollow-activity.json") - |> Poison.decode!() - |> Map.put("object", follow_data) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Undo" - assert data["object"]["type"] == "Follow" - assert data["object"]["object"] == user.ap_id - assert data["actor"] == "http://mastodon.example.org/users/admin" - - refute User.following?(User.get_cached_by_ap_id(data["actor"]), user) - end - - test "it accepts Flag activities" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - object = Object.normalize(activity) - - note_obj = %{ - "type" => "Note", - "id" => activity.data["id"], - "content" => "test post", - "published" => object.data["published"], - "actor" => AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - } - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "cc" => [user.ap_id], - "object" => [user.ap_id, activity.data["id"]], - "type" => "Flag", - "content" => "blocked AND reported!!!", - "actor" => other_user.ap_id - } - - assert {:ok, activity} = Transmogrifier.handle_incoming(message) - - assert activity.data["object"] == [user.ap_id, note_obj] - assert activity.data["content"] == "blocked AND reported!!!" - assert activity.data["actor"] == other_user.ap_id - assert activity.data["cc"] == [user.ap_id] - end - - test "it correctly processes messages with non-array to field" do - user = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "to" => "https://www.w3.org/ns/activitystreams#Public", - "type" => "Create", - "object" => %{ - "content" => "blah blah blah", - "type" => "Note", - "attributedTo" => user.ap_id, - "inReplyTo" => nil - }, - "actor" => user.ap_id - } - - assert {:ok, activity} = Transmogrifier.handle_incoming(message) - - assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["to"] - end - - test "it correctly processes messages with non-array cc field" do - user = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "to" => user.follower_address, - "cc" => "https://www.w3.org/ns/activitystreams#Public", - "type" => "Create", - "object" => %{ - "content" => "blah blah blah", - "type" => "Note", - "attributedTo" => user.ap_id, - "inReplyTo" => nil - }, - "actor" => user.ap_id - } - - assert {:ok, activity} = Transmogrifier.handle_incoming(message) - - assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"] - assert [user.follower_address] == activity.data["to"] - end - - test "it correctly processes messages with weirdness in address fields" do - user = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "to" => [nil, user.follower_address], - "cc" => ["https://www.w3.org/ns/activitystreams#Public", ["¿"]], - "type" => "Create", - "object" => %{ - "content" => "…", - "type" => "Note", - "attributedTo" => user.ap_id, - "inReplyTo" => nil - }, - "actor" => user.ap_id - } - - assert {:ok, activity} = Transmogrifier.handle_incoming(message) - - assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"] - assert [user.follower_address] == activity.data["to"] - end - - test "it accepts Move activities" do - old_user = insert(:user) - new_user = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Move", - "actor" => old_user.ap_id, - "object" => old_user.ap_id, - "target" => new_user.ap_id - } - - assert :error = Transmogrifier.handle_incoming(message) - - {:ok, _new_user} = User.update_and_set_cache(new_user, %{also_known_as: [old_user.ap_id]}) - - assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(message) - assert activity.actor == old_user.ap_id - assert activity.data["actor"] == old_user.ap_id - assert activity.data["object"] == old_user.ap_id - assert activity.data["target"] == new_user.ap_id - assert activity.data["type"] == "Move" - end - end - - describe "`handle_incoming/2`, Mastodon format `replies` handling" do - setup do: clear_config([:activitypub, :note_replies_output_limit], 5) - setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) - - setup do - data = - "test/fixtures/mastodon-post-activity.json" - |> File.read!() - |> Poison.decode!() - - items = get_in(data, ["object", "replies", "first", "items"]) - assert length(items) > 0 - - %{data: data, items: items} - end - - test "schedules background fetching of `replies` items if max thread depth limit allows", %{ - data: data, - items: items - } do - Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 10) - - {:ok, _activity} = Transmogrifier.handle_incoming(data) - - for id <- items do - job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1} - assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args) - end - end - - test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows", - %{data: data} do - Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0) - - {:ok, _activity} = Transmogrifier.handle_incoming(data) - - assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == [] - end - end - - describe "`handle_incoming/2`, Pleroma format `replies` handling" do - setup do: clear_config([:activitypub, :note_replies_output_limit], 5) - setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) - - setup do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "post1"}) - - {:ok, reply1} = - CommonAPI.post(user, %{status: "reply1", in_reply_to_status_id: activity.id}) - - {:ok, reply2} = - CommonAPI.post(user, %{status: "reply2", in_reply_to_status_id: activity.id}) - - replies_uris = Enum.map([reply1, reply2], fn a -> a.object.data["id"] end) - - {:ok, federation_output} = Transmogrifier.prepare_outgoing(activity.data) - - Repo.delete(activity.object) - Repo.delete(activity) - - %{federation_output: federation_output, replies_uris: replies_uris} - end - - test "schedules background fetching of `replies` items if max thread depth limit allows", %{ - federation_output: federation_output, - replies_uris: replies_uris - } do - Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 1) - - {:ok, _activity} = Transmogrifier.handle_incoming(federation_output) - - for id <- replies_uris do - job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1} - assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args) - end - end - - test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows", - %{federation_output: federation_output} do - Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0) - - {:ok, _activity} = Transmogrifier.handle_incoming(federation_output) - - assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == [] - end - end - - describe "prepare outgoing" do - test "it inlines private announced objects" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey", visibility: "private"}) - - {:ok, announce_activity} = CommonAPI.repeat(activity.id, user) - - {:ok, modified} = Transmogrifier.prepare_outgoing(announce_activity.data) - - assert modified["object"]["content"] == "hey" - assert modified["object"]["actor"] == modified["object"]["attributedTo"] - end - - test "it turns mentions into tags" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{status: "hey, @#{other_user.nickname}, how are ya? #2hu"}) - - with_mock Pleroma.Notification, - get_notified_from_activity: fn _, _ -> [] end do - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - object = modified["object"] - - expected_mention = %{ - "href" => other_user.ap_id, - "name" => "@#{other_user.nickname}", - "type" => "Mention" - } - - expected_tag = %{ - "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu", - "type" => "Hashtag", - "name" => "#2hu" - } - - refute called(Pleroma.Notification.get_notified_from_activity(:_, :_)) - assert Enum.member?(object["tag"], expected_tag) - assert Enum.member?(object["tag"], expected_mention) - end - end - - test "it adds the sensitive property" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#nsfw hey"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert modified["object"]["sensitive"] - end - - test "it adds the json-ld context and the conversation property" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert modified["@context"] == - Pleroma.Web.ActivityPub.Utils.make_json_ld_header()["@context"] - - assert modified["object"]["conversation"] == modified["context"] - end - - test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert modified["object"]["actor"] == modified["object"]["attributedTo"] - end - - test "it strips internal hashtag data" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#2hu"}) - - expected_tag = %{ - "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu", - "type" => "Hashtag", - "name" => "#2hu" - } - - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert modified["object"]["tag"] == [expected_tag] - end - - test "it strips internal fields" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#2hu :firefox:"}) - - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert length(modified["object"]["tag"]) == 2 - - assert is_nil(modified["object"]["emoji"]) - 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"]) - end - - test "it strips internal fields of article" do - activity = insert(:article_activity) - - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert length(modified["object"]["tag"]) == 2 - - assert is_nil(modified["object"]["emoji"]) - 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 - - test "the directMessage flag is present" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "2hu :moominmamma:"}) - - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert modified["directMessage"] == false - - {:ok, activity} = CommonAPI.post(user, %{status: "@#{other_user.nickname} :moominmamma:"}) - - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert modified["directMessage"] == false - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "@#{other_user.nickname} :moominmamma:", - visibility: "direct" - }) - - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert modified["directMessage"] == true - end - - test "it strips BCC field" do - user = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) - - {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) - - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert is_nil(modified["bcc"]) - end - - test "it can handle Listen activities" do - listen_activity = insert(:listen) - - {:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data) - - assert modified["type"] == "Listen" - - user = insert(:user) - - {:ok, activity} = CommonAPI.listen(user, %{"title" => "lain radio episode 1"}) - - {:ok, _modified} = Transmogrifier.prepare_outgoing(activity.data) - 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" - - user = User.get_cached_by_id(user.id) - assert user.note_count == 1 - - activity = Activity.get_by_id(activity.id) - assert user.follower_address in activity.recipients - - assert %{ - "url" => [ - %{ - "href" => - "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg" - } - ] - } = user.avatar - - assert %{ - "url" => [ - %{ - "href" => - "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" - } - ] - } = user.banner - - refute "..." in activity.recipients - - unrelated_activity = Activity.get_by_id(unrelated_activity.id) - refute user.follower_address in unrelated_activity.recipients - - user_two = User.get_cached_by_id(user_two.id) - assert User.following?(user_two, user) - refute "..." in User.following(user_two) - end - end - - describe "actor rewriting" do - test "it fixes the actor URL property to be a proper URI" do - data = %{ - "url" => %{"href" => "http://example.com"} - } - - rewritten = Transmogrifier.maybe_fix_user_object(data) - assert rewritten["url"] == "http://example.com" - end - end - - describe "actor origin containment" do - test "it rejects activities which reference objects with bogus origins" do - data = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "http://mastodon.example.org/users/admin/activities/1234", - "actor" => "http://mastodon.example.org/users/admin", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => "https://info.pleroma.site/activity.json", - "type" => "Announce" - } - - assert capture_log(fn -> - {:error, _} = Transmogrifier.handle_incoming(data) - end) =~ "Object containment failed" - end - - test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do - data = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "http://mastodon.example.org/users/admin/activities/1234", - "actor" => "http://mastodon.example.org/users/admin", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => "https://info.pleroma.site/activity2.json", - "type" => "Announce" - } - - assert capture_log(fn -> - {:error, _} = Transmogrifier.handle_incoming(data) - end) =~ "Object containment failed" - end - - test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do - data = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "http://mastodon.example.org/users/admin/activities/1234", - "actor" => "http://mastodon.example.org/users/admin", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => "https://info.pleroma.site/activity3.json", - "type" => "Announce" - } - - assert capture_log(fn -> - {:error, _} = Transmogrifier.handle_incoming(data) - end) =~ "Object containment failed" - end - end - - describe "reserialization" do - test "successfully reserializes a message with inReplyTo == nil" do - user = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "type" => "Create", - "object" => %{ - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "type" => "Note", - "content" => "Hi", - "inReplyTo" => nil, - "attributedTo" => user.ap_id - }, - "actor" => user.ap_id - } - - {:ok, activity} = Transmogrifier.handle_incoming(message) - - {:ok, _} = Transmogrifier.prepare_outgoing(activity.data) - end - - test "successfully reserializes a message with AS2 objects in IR" do - user = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "type" => "Create", - "object" => %{ - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "type" => "Note", - "content" => "Hi", - "inReplyTo" => nil, - "attributedTo" => user.ap_id, - "tag" => [ - %{"name" => "#2hu", "href" => "http://example.com/2hu", "type" => "Hashtag"}, - %{"name" => "Bob", "href" => "http://example.com/bob", "type" => "Mention"} - ] - }, - "actor" => user.ap_id - } - - {:ok, activity} = Transmogrifier.handle_incoming(message) - - {:ok, _} = Transmogrifier.prepare_outgoing(activity.data) - end - end - - describe "fix_explicit_addressing" do - setup do - user = insert(:user) - [user: user] - end - - test "moves non-explicitly mentioned actors to cc", %{user: user} do - explicitly_mentioned_actors = [ - "https://pleroma.gold/users/user1", - "https://pleroma.gold/user2" - ] - - object = %{ - "actor" => user.ap_id, - "to" => explicitly_mentioned_actors ++ ["https://social.beepboop.ga/users/dirb"], - "cc" => [], - "tag" => - Enum.map(explicitly_mentioned_actors, fn href -> - %{"type" => "Mention", "href" => href} - end) - } - - fixed_object = Transmogrifier.fix_explicit_addressing(object) - assert Enum.all?(explicitly_mentioned_actors, &(&1 in fixed_object["to"])) - refute "https://social.beepboop.ga/users/dirb" in fixed_object["to"] - assert "https://social.beepboop.ga/users/dirb" in fixed_object["cc"] - end - - test "does not move actor's follower collection to cc", %{user: user} do - object = %{ - "actor" => user.ap_id, - "to" => [user.follower_address], - "cc" => [] - } - - fixed_object = Transmogrifier.fix_explicit_addressing(object) - assert user.follower_address in fixed_object["to"] - refute user.follower_address in fixed_object["cc"] - end - - test "removes recipient's follower collection from cc", %{user: user} do - recipient = insert(:user) - - object = %{ - "actor" => user.ap_id, - "to" => [recipient.ap_id, "https://www.w3.org/ns/activitystreams#Public"], - "cc" => [user.follower_address, recipient.follower_address] - } - - fixed_object = Transmogrifier.fix_explicit_addressing(object) - - assert user.follower_address in fixed_object["cc"] - refute recipient.follower_address in fixed_object["cc"] - refute recipient.follower_address in fixed_object["to"] - end - end - - describe "fix_summary/1" do - test "returns fixed object" do - assert Transmogrifier.fix_summary(%{"summary" => nil}) == %{"summary" => ""} - assert Transmogrifier.fix_summary(%{"summary" => "ok"}) == %{"summary" => "ok"} - assert Transmogrifier.fix_summary(%{}) == %{"summary" => ""} - end - end - - describe "fix_in_reply_to/2" do - setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) - - setup do - data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) - [data: data] - end - - test "returns not modified object when hasn't containts inReplyTo field", %{data: data} do - assert Transmogrifier.fix_in_reply_to(data) == data - end - - test "returns object with inReplyTo when denied incoming reply", %{data: data} do - Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0) - - object_with_reply = - Map.put(data["object"], "inReplyTo", "https://shitposter.club/notice/2827873") - - modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) - assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873" - - object_with_reply = - Map.put(data["object"], "inReplyTo", %{"id" => "https://shitposter.club/notice/2827873"}) - - modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) - assert modified_object["inReplyTo"] == %{"id" => "https://shitposter.club/notice/2827873"} - - object_with_reply = - Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"]) - - modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) - assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"] - - object_with_reply = Map.put(data["object"], "inReplyTo", []) - modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) - assert modified_object["inReplyTo"] == [] - end - - @tag capture_log: true - test "returns modified object when allowed incoming reply", %{data: data} do - object_with_reply = - Map.put( - data["object"], - "inReplyTo", - "https://mstdn.io/users/mayuutann/statuses/99568293732299394" - ) - - Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 5) - modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) - - assert modified_object["inReplyTo"] == - "https://mstdn.io/users/mayuutann/statuses/99568293732299394" - - assert modified_object["context"] == - "tag:shitposter.club,2018-02-22:objectType=thread:nonce=e5a7c72d60a9c0e4" - end - end - - describe "fix_url/1" do - test "fixes data for object when url is map" do - object = %{ - "url" => %{ - "type" => "Link", - "mimeType" => "video/mp4", - "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4" - } - } - - assert Transmogrifier.fix_url(object) == %{ - "url" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4" - } - end - - test "returns non-modified object" do - assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"} - end - end - - describe "get_obj_helper/2" do - test "returns nil when cannot normalize object" do - assert capture_log(fn -> - refute Transmogrifier.get_obj_helper("test-obj-id") - end) =~ "Unsupported URI scheme" - end - - @tag capture_log: true - test "returns {:ok, %Object{}} for success case" do - assert {:ok, %Object{}} = - Transmogrifier.get_obj_helper( - "https://mstdn.io/users/mayuutann/statuses/99568293732299394" - ) - end - end - - describe "fix_attachments/1" do - test "returns not modified object" do - data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) - assert Transmogrifier.fix_attachments(data) == data - end - - test "returns modified object when attachment is map" do - assert Transmogrifier.fix_attachments(%{ - "attachment" => %{ - "mediaType" => "video/mp4", - "url" => "https://peertube.moe/stat-480.mp4" - } - }) == %{ - "attachment" => [ - %{ - "mediaType" => "video/mp4", - "type" => "Document", - "url" => [ - %{ - "href" => "https://peertube.moe/stat-480.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - } - ] - } - end - - test "returns modified object when attachment is list" do - assert Transmogrifier.fix_attachments(%{ - "attachment" => [ - %{"mediaType" => "video/mp4", "url" => "https://pe.er/stat-480.mp4"}, - %{"mimeType" => "video/mp4", "href" => "https://pe.er/stat-480.mp4"} - ] - }) == %{ - "attachment" => [ - %{ - "mediaType" => "video/mp4", - "type" => "Document", - "url" => [ - %{ - "href" => "https://pe.er/stat-480.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - }, - %{ - "mediaType" => "video/mp4", - "type" => "Document", - "url" => [ - %{ - "href" => "https://pe.er/stat-480.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - } - ] - } - end - end - - describe "fix_emoji/1" do - test "returns not modified object when object not contains tags" do - data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) - assert Transmogrifier.fix_emoji(data) == data - end - - test "returns object with emoji when object contains list tags" do - assert Transmogrifier.fix_emoji(%{ - "tag" => [ - %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}}, - %{"type" => "Hashtag"} - ] - }) == %{ - "emoji" => %{"bib" => "/test"}, - "tag" => [ - %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"}, - %{"type" => "Hashtag"} - ] - } - end - - test "returns object with emoji when object contains map tag" do - assert Transmogrifier.fix_emoji(%{ - "tag" => %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}} - }) == %{ - "emoji" => %{"bib" => "/test"}, - "tag" => %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"} - } - end - end - - describe "set_replies/1" do - setup do: clear_config([:activitypub, :note_replies_output_limit], 2) - - test "returns unmodified object if activity doesn't have self-replies" do - data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) - assert Transmogrifier.set_replies(data) == data - end - - test "sets `replies` collection with a limited number of self-replies" do - [user, another_user] = insert_list(2, :user) - - {:ok, %{id: id1} = activity} = CommonAPI.post(user, %{status: "1"}) - - {:ok, %{id: id2} = self_reply1} = - CommonAPI.post(user, %{status: "self-reply 1", in_reply_to_status_id: id1}) - - {:ok, self_reply2} = - CommonAPI.post(user, %{status: "self-reply 2", in_reply_to_status_id: id1}) - - # Assuming to _not_ be present in `replies` due to :note_replies_output_limit is set to 2 - {:ok, _} = CommonAPI.post(user, %{status: "self-reply 3", in_reply_to_status_id: id1}) - - {:ok, _} = - CommonAPI.post(user, %{ - status: "self-reply to self-reply", - in_reply_to_status_id: id2 - }) - - {:ok, _} = - CommonAPI.post(another_user, %{ - status: "another user's reply", - in_reply_to_status_id: id1 - }) - - object = Object.normalize(activity) - replies_uris = Enum.map([self_reply1, self_reply2], fn a -> a.object.data["id"] end) - - assert %{"type" => "Collection", "items" => ^replies_uris} = - Transmogrifier.set_replies(object.data)["replies"] - end - end - - test "take_emoji_tags/1" do - user = insert(:user, %{emoji: %{"firefox" => "https://example.org/firefox.png"}}) - - assert Transmogrifier.take_emoji_tags(user) == [ - %{ - "icon" => %{"type" => "Image", "url" => "https://example.org/firefox.png"}, - "id" => "https://example.org/firefox.png", - "name" => ":firefox:", - "type" => "Emoji", - "updated" => "1970-01-01T00:00:00Z" - } - ] - end -end diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs @@ -1,548 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.UtilsTest do - use Pleroma.DataCase - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.AdminAPI.AccountView - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - require Pleroma.Constants - - describe "fetch the latest Follow" do - test "fetches the latest Follow activity" do - %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity) - follower = User.get_cached_by_ap_id(activity.data["actor"]) - followed = User.get_cached_by_ap_id(activity.data["object"]) - - assert activity == Utils.fetch_latest_follow(follower, followed) - end - end - - describe "determine_explicit_mentions()" do - test "works with an object that has mentions" do - object = %{ - "tag" => [ - %{ - "type" => "Mention", - "href" => "https://example.com/~alyssa", - "name" => "Alyssa P. Hacker" - } - ] - } - - assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"] - end - - test "works with an object that does not have mentions" do - object = %{ - "tag" => [ - %{"type" => "Hashtag", "href" => "https://example.com/tag/2hu", "name" => "2hu"} - ] - } - - assert Utils.determine_explicit_mentions(object) == [] - end - - test "works with an object that has mentions and other tags" do - object = %{ - "tag" => [ - %{ - "type" => "Mention", - "href" => "https://example.com/~alyssa", - "name" => "Alyssa P. Hacker" - }, - %{"type" => "Hashtag", "href" => "https://example.com/tag/2hu", "name" => "2hu"} - ] - } - - assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"] - end - - test "works with an object that has no tags" do - object = %{} - - assert Utils.determine_explicit_mentions(object) == [] - end - - test "works with an object that has only IR tags" do - object = %{"tag" => ["2hu"]} - - assert Utils.determine_explicit_mentions(object) == [] - end - - test "works with an object has tags as map" do - object = %{ - "tag" => %{ - "type" => "Mention", - "href" => "https://example.com/~alyssa", - "name" => "Alyssa P. Hacker" - } - } - - assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"] - end - end - - describe "make_like_data" do - setup do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - [user: user, other_user: other_user, third_user: third_user] - end - - test "addresses actor's follower address if the activity is public", %{ - user: user, - other_user: other_user, - third_user: third_user - } do - expected_to = Enum.sort([user.ap_id, other_user.follower_address]) - expected_cc = Enum.sort(["https://www.w3.org/ns/activitystreams#Public", third_user.ap_id]) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: - "hey @#{other_user.nickname}, @#{third_user.nickname} how about beering together this weekend?" - }) - - %{"to" => to, "cc" => cc} = Utils.make_like_data(other_user, activity, nil) - assert Enum.sort(to) == expected_to - assert Enum.sort(cc) == expected_cc - end - - test "does not adress actor's follower address if the activity is not public", %{ - user: user, - other_user: other_user, - third_user: third_user - } do - expected_to = Enum.sort([user.ap_id]) - expected_cc = [third_user.ap_id] - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "@#{other_user.nickname} @#{third_user.nickname} bought a new swimsuit!", - visibility: "private" - }) - - %{"to" => to, "cc" => cc} = Utils.make_like_data(other_user, activity, nil) - assert Enum.sort(to) == expected_to - assert Enum.sort(cc) == expected_cc - end - end - - test "make_json_ld_header/0" do - assert Utils.make_json_ld_header() == %{ - "@context" => [ - "https://www.w3.org/ns/activitystreams", - "http://localhost:4001/schemas/litepub-0.1.jsonld", - %{ - "@language" => "und" - } - ] - } - end - - describe "get_existing_votes" do - test "fetches existing votes" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "How do I pronounce LaTeX?", - poll: %{ - options: ["laytekh", "lahtekh", "latex"], - expires_in: 20, - multiple: true - } - }) - - object = Object.normalize(activity) - {:ok, votes, object} = CommonAPI.vote(other_user, object, [0, 1]) - assert Enum.sort(Utils.get_existing_votes(other_user.ap_id, object)) == Enum.sort(votes) - end - - test "fetches only Create activities" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "Are we living in a society?", - poll: %{ - options: ["yes", "no"], - expires_in: 20 - } - }) - - object = Object.normalize(activity) - {:ok, [vote], object} = CommonAPI.vote(other_user, object, [0]) - {:ok, _activity} = CommonAPI.favorite(user, activity.id) - [fetched_vote] = Utils.get_existing_votes(other_user.ap_id, object) - assert fetched_vote.id == vote.id - end - end - - describe "update_follow_state_for_all/2" do - test "updates the state of all Follow activities with the same actor and object" do - user = insert(:user, locked: true) - follower = insert(:user) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) - {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) - - data = - follow_activity_two.data - |> Map.put("state", "accept") - - cng = Ecto.Changeset.change(follow_activity_two, data: data) - - {:ok, follow_activity_two} = Repo.update(cng) - - {:ok, follow_activity_two} = - Utils.update_follow_state_for_all(follow_activity_two, "accept") - - assert refresh_record(follow_activity).data["state"] == "accept" - assert refresh_record(follow_activity_two).data["state"] == "accept" - end - end - - describe "update_follow_state/2" do - test "updates the state of the given follow activity" do - user = insert(:user, locked: true) - follower = insert(:user) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) - {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) - - data = - follow_activity_two.data - |> Map.put("state", "accept") - - cng = Ecto.Changeset.change(follow_activity_two, data: data) - - {:ok, follow_activity_two} = Repo.update(cng) - - {:ok, follow_activity_two} = Utils.update_follow_state(follow_activity_two, "reject") - - assert refresh_record(follow_activity).data["state"] == "pending" - assert refresh_record(follow_activity_two).data["state"] == "reject" - end - end - - describe "update_element_in_object/3" do - test "updates likes" do - user = insert(:user) - activity = insert(:note_activity) - object = Object.normalize(activity) - - assert {:ok, updated_object} = - Utils.update_element_in_object( - "like", - [user.ap_id], - object - ) - - assert updated_object.data["likes"] == [user.ap_id] - assert updated_object.data["like_count"] == 1 - end - end - - describe "add_like_to_object/2" do - test "add actor to likes" do - user = insert(:user) - user2 = insert(:user) - object = insert(:note) - - assert {:ok, updated_object} = - Utils.add_like_to_object( - %Activity{data: %{"actor" => user.ap_id}}, - object - ) - - assert updated_object.data["likes"] == [user.ap_id] - assert updated_object.data["like_count"] == 1 - - assert {:ok, updated_object2} = - Utils.add_like_to_object( - %Activity{data: %{"actor" => user2.ap_id}}, - updated_object - ) - - assert updated_object2.data["likes"] == [user2.ap_id, user.ap_id] - assert updated_object2.data["like_count"] == 2 - end - end - - describe "remove_like_from_object/2" do - test "removes ap_id from likes" do - user = insert(:user) - user2 = insert(:user) - object = insert(:note, data: %{"likes" => [user.ap_id, user2.ap_id], "like_count" => 2}) - - assert {:ok, updated_object} = - Utils.remove_like_from_object( - %Activity{data: %{"actor" => user.ap_id}}, - object - ) - - assert updated_object.data["likes"] == [user2.ap_id] - assert updated_object.data["like_count"] == 1 - end - end - - describe "get_existing_like/2" do - test "fetches existing like" do - note_activity = insert(:note_activity) - assert object = Object.normalize(note_activity) - - user = insert(:user) - refute Utils.get_existing_like(user.ap_id, object) - {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id) - - assert ^like_activity = Utils.get_existing_like(user.ap_id, object) - end - end - - describe "get_get_existing_announce/2" do - test "returns nil if announce not found" do - actor = insert(:user) - refute Utils.get_existing_announce(actor.ap_id, %{data: %{"id" => "test"}}) - end - - test "fetches existing announce" do - note_activity = insert(:note_activity) - assert object = Object.normalize(note_activity) - actor = insert(:user) - - {:ok, announce} = CommonAPI.repeat(note_activity.id, actor) - assert Utils.get_existing_announce(actor.ap_id, object) == announce - end - end - - describe "fetch_latest_block/2" do - test "fetches last block activities" do - user1 = insert(:user) - user2 = insert(:user) - - assert {:ok, %Activity{} = _} = CommonAPI.block(user1, user2) - assert {:ok, %Activity{} = _} = CommonAPI.block(user1, user2) - assert {:ok, %Activity{} = activity} = CommonAPI.block(user1, user2) - - assert Utils.fetch_latest_block(user1, user2) == activity - end - end - - describe "recipient_in_message/3" do - test "returns true when recipient in `to`" do - recipient = insert(:user) - actor = insert(:user) - assert Utils.recipient_in_message(recipient, actor, %{"to" => recipient.ap_id}) - - assert Utils.recipient_in_message( - recipient, - actor, - %{"to" => [recipient.ap_id], "cc" => ""} - ) - end - - test "returns true when recipient in `cc`" do - recipient = insert(:user) - actor = insert(:user) - assert Utils.recipient_in_message(recipient, actor, %{"cc" => recipient.ap_id}) - - assert Utils.recipient_in_message( - recipient, - actor, - %{"cc" => [recipient.ap_id], "to" => ""} - ) - end - - test "returns true when recipient in `bto`" do - recipient = insert(:user) - actor = insert(:user) - assert Utils.recipient_in_message(recipient, actor, %{"bto" => recipient.ap_id}) - - assert Utils.recipient_in_message( - recipient, - actor, - %{"bcc" => "", "bto" => [recipient.ap_id]} - ) - end - - test "returns true when recipient in `bcc`" do - recipient = insert(:user) - actor = insert(:user) - assert Utils.recipient_in_message(recipient, actor, %{"bcc" => recipient.ap_id}) - - assert Utils.recipient_in_message( - recipient, - actor, - %{"bto" => "", "bcc" => [recipient.ap_id]} - ) - end - - test "returns true when message without addresses fields" do - recipient = insert(:user) - actor = insert(:user) - assert Utils.recipient_in_message(recipient, actor, %{"bccc" => recipient.ap_id}) - - assert Utils.recipient_in_message( - recipient, - actor, - %{"btod" => "", "bccc" => [recipient.ap_id]} - ) - end - - test "returns false" do - recipient = insert(:user) - actor = insert(:user) - refute Utils.recipient_in_message(recipient, actor, %{"to" => "ap_id"}) - end - end - - describe "lazy_put_activity_defaults/2" do - test "returns map with id and published data" do - note_activity = insert(:note_activity) - object = Object.normalize(note_activity) - 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 - - 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) - end - - test "returns activity data with object" do - note_activity = insert(:note_activity) - object = Object.normalize(note_activity) - - res = - Utils.lazy_put_activity_defaults(%{ - "context" => object.data["id"], - "object" => %{} - }) - - 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 - - describe "make_flag_data" do - test "returns empty map when params is invalid" do - assert Utils.make_flag_data(%{}, %{}) == %{} - end - - test "returns map with Flag object" do - reporter = insert(:user) - target_account = insert(:user) - {:ok, activity} = CommonAPI.post(target_account, %{status: "foobar"}) - context = Utils.generate_context_id() - content = "foobar" - - target_ap_id = target_account.ap_id - activity_ap_id = activity.data["id"] - - res = - Utils.make_flag_data( - %{ - actor: reporter, - context: context, - account: target_account, - statuses: [%{"id" => activity.data["id"]}], - content: content - }, - %{} - ) - - note_obj = %{ - "type" => "Note", - "id" => activity_ap_id, - "content" => content, - "published" => activity.object.data["published"], - "actor" => - AccountView.render("show.json", %{user: target_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 - test "adds actor to announcement" do - user = insert(:user) - object = insert(:note) - - activity = - insert(:note_activity, - data: %{ - "actor" => user.ap_id, - "cc" => [Pleroma.Constants.as_public()] - } - ) - - assert {:ok, updated_object} = Utils.add_announce_to_object(activity, object) - assert updated_object.data["announcements"] == [user.ap_id] - assert updated_object.data["announcement_count"] == 1 - end - end - - describe "remove_announce_from_object/2" do - test "removes actor from announcements" do - user = insert(:user) - user2 = insert(:user) - - object = - insert(:note, - data: %{"announcements" => [user.ap_id, user2.ap_id], "announcement_count" => 2} - ) - - activity = insert(:note_activity, data: %{"actor" => user.ap_id}) - - assert {:ok, updated_object} = Utils.remove_announce_from_object(activity, object) - assert updated_object.data["announcements"] == [user2.ap_id] - assert updated_object.data["announcement_count"] == 1 - end - end - - describe "get_cached_emoji_reactions/1" do - test "returns the data or an emtpy 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"]]] - - object = insert(:note, data: %{"reactions" => %{}}) - assert Utils.get_cached_emoji_reactions(object) == [] - end - end -end diff --git a/test/web/activity_pub/views/object_view_test.exs b/test/web/activity_pub/views/object_view_test.exs @@ -1,84 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectViewTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.ObjectView - alias Pleroma.Web.CommonAPI - - test "renders a note object" do - note = insert(:note) - - result = ObjectView.render("object.json", %{object: note}) - - assert result["id"] == note.data["id"] - assert result["to"] == note.data["to"] - assert result["content"] == note.data["content"] - assert result["type"] == "Note" - assert result["@context"] - end - - test "renders a note activity" do - note = insert(:note_activity) - object = Object.normalize(note) - - result = ObjectView.render("object.json", %{object: note}) - - assert result["id"] == note.data["id"] - assert result["to"] == note.data["to"] - assert result["object"]["type"] == "Note" - assert result["object"]["content"] == object.data["content"] - assert result["type"] == "Create" - assert result["@context"] - end - - describe "note activity's `replies` collection rendering" do - setup do: clear_config([:activitypub, :note_replies_output_limit], 5) - - test "renders `replies` collection for a note activity" do - user = insert(:user) - activity = insert(:note_activity, user: user) - - {:ok, self_reply1} = - CommonAPI.post(user, %{status: "self-reply 1", in_reply_to_status_id: activity.id}) - - replies_uris = [self_reply1.object.data["id"]] - result = ObjectView.render("object.json", %{object: refresh_record(activity)}) - - assert %{"type" => "Collection", "items" => ^replies_uris} = - get_in(result, ["object", "replies"]) - end - end - - test "renders a like activity" do - note = insert(:note_activity) - object = Object.normalize(note) - user = insert(:user) - - {:ok, like_activity} = CommonAPI.favorite(user, note.id) - - result = ObjectView.render("object.json", %{object: like_activity}) - - assert result["id"] == like_activity.data["id"] - assert result["object"] == object.data["id"] - assert result["type"] == "Like" - end - - test "renders an announce activity" do - note = insert(:note_activity) - object = Object.normalize(note) - user = insert(:user) - - {:ok, announce_activity} = CommonAPI.repeat(note.id, user) - - result = ObjectView.render("object.json", %{object: announce_activity}) - - assert result["id"] == announce_activity.data["id"] - assert result["object"] == object.data["id"] - assert result["type"] == "Announce" - end -end diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs @@ -1,180 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.UserViewTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.User - alias Pleroma.Web.ActivityPub.UserView - alias Pleroma.Web.CommonAPI - - 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}) - - assert result["id"] == user.ap_id - assert result["preferredUsername"] == user.nickname - - assert String.contains?(result["publicKey"]["publicKeyPem"], "BEGIN PUBLIC KEY") - end - - test "Renders profile fields" do - fields = [ - %{"name" => "foo", "value" => "bar"} - ] - - {:ok, user} = - insert(:user) - |> User.update_changeset(%{fields: fields}) - |> User.update_and_set_cache() - - assert %{ - "attachment" => [%{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}] - } = UserView.render("user.json", %{user: user}) - end - - test "Renders with emoji tags" do - user = insert(:user, emoji: %{"bib" => "/test"}) - - assert %{ - "tag" => [ - %{ - "icon" => %{"type" => "Image", "url" => "/test"}, - "id" => "/test", - "name" => ":bib:", - "type" => "Emoji", - "updated" => "1970-01-01T00:00:00Z" - } - ] - } = UserView.render("user.json", %{user: user}) - end - - 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"] - refute result["image"] - - user = - insert(:user, - avatar: %{"url" => [%{"href" => "https://someurl"}]}, - 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" - end - - test "renders an invisible user with the invisible property set to true" do - user = insert(:user, invisible: true) - - assert %{"invisible" => true} = UserView.render("service.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}) - - assert result["id"] == user.ap_id - - %{ - "sharedInbox" => _, - "oauthAuthorizationEndpoint" => _, - "oauthRegistrationEndpoint" => _, - "oauthTokenEndpoint" => _ - } = result["endpoints"] - end - - 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}) - - assert result["id"] == user.ap_id - assert result["endpoints"] == %{} - end - - 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}) - - refute result["endpoints"]["oauthAuthorizationEndpoint"] - refute result["endpoints"]["oauthRegistrationEndpoint"] - refute result["endpoints"]["oauthTokenEndpoint"] - end - end - - describe "followers" do - test "sets totalItems to zero when followers are hidden" do - user = insert(:user) - other_user = insert(:user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user}) - user = Map.merge(user, %{hide_followers_count: true, hide_followers: true}) - refute UserView.render("followers.json", %{user: user}) |> Map.has_key?("totalItems") - end - - test "sets correct totalItems when followers are hidden but the follower counter is not" do - user = insert(:user) - other_user = insert(:user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user}) - user = Map.merge(user, %{hide_followers_count: false, hide_followers: true}) - assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user}) - end - end - - describe "following" do - test "sets totalItems to zero when follows are hidden" do - user = insert(:user) - other_user = insert(:user) - {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) - assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) - user = Map.merge(user, %{hide_follows_count: true, hide_follows: true}) - assert %{"totalItems" => 0} = UserView.render("following.json", %{user: user}) - end - - test "sets correct totalItems when follows are hidden but the follow counter is not" do - user = insert(:user) - other_user = insert(:user) - {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) - assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) - user = Map.merge(user, %{hide_follows_count: false, hide_follows: true}) - assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) - end - end - - describe "acceptsChatMessages" do - test "it returns this value if it is set" do - true_user = insert(:user, accepts_chat_messages: true) - false_user = insert(:user, accepts_chat_messages: false) - nil_user = insert(:user, accepts_chat_messages: nil) - - assert %{"capabilities" => %{"acceptsChatMessages" => true}} = - UserView.render("user.json", user: true_user) - - assert %{"capabilities" => %{"acceptsChatMessages" => false}} = - UserView.render("user.json", user: false_user) - - refute Map.has_key?( - UserView.render("user.json", user: nil_user)["capabilities"], - "acceptsChatMessages" - ) - end - end -end diff --git a/test/web/activity_pub/visibilty_test.exs b/test/web/activity_pub/visibilty_test.exs @@ -1,230 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.VisibilityTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Web.ActivityPub.Visibility - alias Pleroma.Web.CommonAPI - import Pleroma.Factory - - setup do - user = insert(:user) - mentioned = insert(:user) - following = insert(:user) - unrelated = insert(:user) - {:ok, following} = Pleroma.User.follow(following, user) - {:ok, list} = Pleroma.List.create("foo", user) - - Pleroma.List.follow(list, unrelated) - - {:ok, public} = - CommonAPI.post(user, %{status: "@#{mentioned.nickname}", visibility: "public"}) - - {:ok, private} = - CommonAPI.post(user, %{status: "@#{mentioned.nickname}", visibility: "private"}) - - {:ok, direct} = - CommonAPI.post(user, %{status: "@#{mentioned.nickname}", visibility: "direct"}) - - {:ok, unlisted} = - CommonAPI.post(user, %{status: "@#{mentioned.nickname}", visibility: "unlisted"}) - - {:ok, list} = - CommonAPI.post(user, %{ - status: "@#{mentioned.nickname}", - visibility: "list:#{list.id}" - }) - - %{ - public: public, - private: private, - direct: direct, - unlisted: unlisted, - user: user, - mentioned: mentioned, - following: following, - unrelated: unrelated, - list: list - } - end - - test "is_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) - end - - test "is_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) - end - - test "is_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) - end - - test "is_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) - end - - test "visible_for_user?", %{ - public: public, - private: private, - direct: direct, - unlisted: unlisted, - user: user, - mentioned: mentioned, - following: following, - unrelated: unrelated, - list: list - } do - # All visible to author - - assert Visibility.visible_for_user?(public, user) - assert Visibility.visible_for_user?(private, user) - assert Visibility.visible_for_user?(unlisted, user) - assert Visibility.visible_for_user?(direct, user) - assert Visibility.visible_for_user?(list, user) - - # All visible to a mentioned user - - assert Visibility.visible_for_user?(public, mentioned) - assert Visibility.visible_for_user?(private, mentioned) - assert Visibility.visible_for_user?(unlisted, mentioned) - assert Visibility.visible_for_user?(direct, mentioned) - assert Visibility.visible_for_user?(list, mentioned) - - # DM not visible for just follower - - assert Visibility.visible_for_user?(public, following) - assert Visibility.visible_for_user?(private, following) - assert Visibility.visible_for_user?(unlisted, following) - refute Visibility.visible_for_user?(direct, following) - refute Visibility.visible_for_user?(list, following) - - # Public and unlisted visible for unrelated user - - assert Visibility.visible_for_user?(public, unrelated) - assert Visibility.visible_for_user?(unlisted, unrelated) - refute Visibility.visible_for_user?(private, unrelated) - refute Visibility.visible_for_user?(direct, unrelated) - - # Visible for a list member - assert Visibility.visible_for_user?(list, unrelated) - end - - test "doesn't die when the user doesn't exist", - %{ - direct: direct, - user: user - } do - Repo.delete(user) - Cachex.clear(:user_cache) - refute Visibility.is_private?(direct) - end - - test "get_visibility", %{ - public: public, - private: private, - direct: direct, - unlisted: unlisted, - list: list - } do - assert Visibility.get_visibility(public) == "public" - assert Visibility.get_visibility(private) == "private" - assert Visibility.get_visibility(direct) == "direct" - assert Visibility.get_visibility(unlisted) == "unlisted" - assert Visibility.get_visibility(list) == "list" - end - - test "get_visibility with directMessage flag" do - assert Visibility.get_visibility(%{data: %{"directMessage" => true}}) == "direct" - end - - test "get_visibility with listMessage flag" do - assert Visibility.get_visibility(%{data: %{"listMessage" => ""}}) == "list" - end - - describe "entire_thread_visible_for_user?/2" do - test "returns false if not found activity", %{user: user} do - refute Visibility.entire_thread_visible_for_user?(%Activity{}, user) - end - - test "returns true if activity hasn't 'Create' type", %{user: user} do - activity = insert(:like_activity) - assert Visibility.entire_thread_visible_for_user?(activity, user) - end - - test "returns false when invalid recipients", %{user: user} do - author = insert(:user) - - activity = - insert(:note_activity, - note: - insert(:note, - user: author, - data: %{"to" => ["test-user"]} - ) - ) - - refute Visibility.entire_thread_visible_for_user?(activity, user) - end - - test "returns true if user following to author" do - author = insert(:user) - user = insert(:user) - Pleroma.User.follow(user, author) - - activity = - insert(:note_activity, - note: - insert(:note, - user: author, - data: %{"to" => [user.ap_id]} - ) - ) - - assert Visibility.entire_thread_visible_for_user?(activity, user) - end - end -end diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -1,2034 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do - use Pleroma.Web.ConnCase - use Oban.Testing, repo: Pleroma.Repo - - import ExUnit.CaptureLog - import Mock - import Pleroma.Factory - import Swoosh.TestAssertions - - alias Pleroma.Activity - alias Pleroma.Config - alias Pleroma.HTML - alias Pleroma.MFA - alias Pleroma.ModerationLog - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web - alias Pleroma.Web.ActivityPub.Relay - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MediaProxy - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - - :ok - end - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - {:ok, %{admin: admin, token: token, conn: conn}} - end - - test "with valid `admin_token` query parameter, skips OAuth scopes check" do - clear_config([:admin_token], "password123") - - user = insert(:user) - - conn = get(build_conn(), "/api/pleroma/admin/users/#{user.nickname}?admin_token=password123") - - assert json_response(conn, 200) - end - - describe "with [:auth, :enforce_oauth_admin_scope_usage]," do - setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], true) - - 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(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(conn, :forbidden) - end - end - end - - describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do - setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) - - test "GET /api/pleroma/admin/users/:nickname requires " <> - "read:accounts or 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"]) - good_token4 = insert(:oauth_token, user: admin, scopes: ["read:accounts"]) - good_token5 = insert(:oauth_token, user: admin, scopes: ["read"]) - - good_tokens = [good_token1, good_token2, good_token3, good_token4, good_token5] - - bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts:partial"]) - bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"]) - bad_token3 = nil - - for good_token <- good_tokens do - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, good_token) - |> get(url) - - assert json_response(conn, 200) - end - - for good_token <- good_tokens 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(conn, :forbidden) - end - end - end - - describe "DELETE /api/pleroma/admin/users" do - test "single user", %{admin: admin, conn: conn} do - clear_config([:instance, :federating], true) - - user = - insert(:user, - avatar: %{"url" => [%{"href" => "https://someurl"}]}, - banner: %{"url" => [%{"href" => "https://somebanner"}]}, - bio: "Hello world!", - name: "A guy" - ) - - # Create some activities to check they got deleted later - follower = insert(:user) - {:ok, _} = CommonAPI.post(user, %{status: "test"}) - {:ok, _, _, _} = CommonAPI.follow(user, follower) - {:ok, _, _, _} = CommonAPI.follow(follower, user) - user = Repo.get(User, user.id) - assert user.note_count == 1 - assert user.follower_count == 1 - assert user.following_count == 1 - refute user.deactivated - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end, - perform: fn _, _ -> nil end do - conn = - conn - |> put_req_header("accept", "application/json") - |> delete("/api/pleroma/admin/users?nickname=#{user.nickname}") - - ObanHelpers.perform_all() - - assert User.get_by_nickname(user.nickname).deactivated - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deleted users: @#{user.nickname}" - - assert json_response(conn, 200) == [user.nickname] - - user = Repo.get(User, user.id) - assert user.deactivated - - assert user.avatar == %{} - assert user.banner == %{} - assert user.note_count == 0 - assert user.follower_count == 0 - assert user.following_count == 0 - assert user.bio == "" - assert user.name == nil - - assert called(Pleroma.Web.Federator.publish(:_)) - end - end - - test "multiple users", %{admin: admin, conn: conn} do - user_one = insert(:user) - user_two = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/json") - |> delete("/api/pleroma/admin/users", %{ - nicknames: [user_one.nickname, user_two.nickname] - }) - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deleted users: @#{user_one.nickname}, @#{user_two.nickname}" - - response = json_response(conn, 200) - assert response -- [user_one.nickname, user_two.nickname] == [] - end - end - - describe "/api/pleroma/admin/users" do - test "Create", %{conn: conn} do - conn = - conn - |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/users", %{ - "users" => [ - %{ - "nickname" => "lain", - "email" => "lain@example.org", - "password" => "test" - }, - %{ - "nickname" => "lain2", - "email" => "lain2@example.org", - "password" => "test" - } - ] - }) - - response = json_response(conn, 200) |> Enum.map(&Map.get(&1, "type")) - assert response == ["success", "success"] - - log_entry = Repo.one(ModerationLog) - - assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == [] - end - - test "Cannot create user with existing email", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/users", %{ - "users" => [ - %{ - "nickname" => "lain", - "email" => user.email, - "password" => "test" - } - ] - }) - - assert json_response(conn, 409) == [ - %{ - "code" => 409, - "data" => %{ - "email" => user.email, - "nickname" => "lain" - }, - "error" => "email has already been taken", - "type" => "error" - } - ] - end - - test "Cannot create user with existing nickname", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/users", %{ - "users" => [ - %{ - "nickname" => user.nickname, - "email" => "someuser@plerama.social", - "password" => "test" - } - ] - }) - - assert json_response(conn, 409) == [ - %{ - "code" => 409, - "data" => %{ - "email" => "someuser@plerama.social", - "nickname" => user.nickname - }, - "error" => "nickname has already been taken", - "type" => "error" - } - ] - end - - test "Multiple user creation works in transaction", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/users", %{ - "users" => [ - %{ - "nickname" => "newuser", - "email" => "newuser@pleroma.social", - "password" => "test" - }, - %{ - "nickname" => "lain", - "email" => user.email, - "password" => "test" - } - ] - }) - - assert json_response(conn, 409) == [ - %{ - "code" => 409, - "data" => %{ - "email" => user.email, - "nickname" => "lain" - }, - "error" => "email has already been taken", - "type" => "error" - }, - %{ - "code" => 409, - "data" => %{ - "email" => "newuser@pleroma.social", - "nickname" => "newuser" - }, - "error" => "", - "type" => "error" - } - ] - - assert User.get_by_nickname("newuser") === nil - end - end - - describe "/api/pleroma/admin/users/:nickname" do - test "Show", %{conn: conn} do - user = insert(:user) - - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}") - - expected = %{ - "deactivated" => false, - "id" => to_string(user.id), - "local" => true, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - - assert expected == json_response(conn, 200) - end - - test "when the user doesn't exist", %{conn: conn} do - user = build(:user) - - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}") - - assert %{"error" => "Not found"} == json_response(conn, 404) - end - end - - describe "/api/pleroma/admin/users/follow" do - test "allows to force-follow another user", %{admin: admin, conn: conn} do - user = insert(:user) - follower = insert(:user) - - conn - |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/users/follow", %{ - "follower" => follower.nickname, - "followed" => user.nickname - }) - - user = User.get_cached_by_id(user.id) - follower = User.get_cached_by_id(follower.id) - - assert User.following?(follower, user) - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} made @#{follower.nickname} follow @#{user.nickname}" - end - end - - describe "/api/pleroma/admin/users/unfollow" do - test "allows to force-unfollow another user", %{admin: admin, conn: conn} do - user = insert(:user) - follower = insert(:user) - - User.follow(follower, user) - - conn - |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/users/unfollow", %{ - "follower" => follower.nickname, - "followed" => user.nickname - }) - - user = User.get_cached_by_id(user.id) - follower = User.get_cached_by_id(follower.id) - - refute User.following?(follower, user) - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} made @#{follower.nickname} unfollow @#{user.nickname}" - end - end - - describe "PUT /api/pleroma/admin/users/tag" do - setup %{conn: conn} do - 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 - - test "it appends specified tags to users with specified nicknames", %{ - conn: conn, - admin: admin, - user1: user1, - user2: user2 - } do - 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"] - - log_entry = Repo.one(ModerationLog) - - users = - [user1.nickname, user2.nickname] - |> Enum.map(&"@#{&1}") - |> Enum.join(", ") - - tags = ["foo", "bar"] |> Enum.join(", ") - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} added tags: #{tags} to users: #{users}" - end - - test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do - assert empty_json_response(conn) - assert User.get_cached_by_id(user3.id).tags == ["unchanged"] - end - end - - describe "DELETE /api/pleroma/admin/users/tag" do - setup %{conn: conn} do - 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 - - test "it removes specified tags from users with specified nicknames", %{ - conn: conn, - admin: admin, - user1: user1, - user2: user2 - } do - assert empty_json_response(conn) - assert User.get_cached_by_id(user1.id).tags == [] - assert User.get_cached_by_id(user2.id).tags == ["y"] - - log_entry = Repo.one(ModerationLog) - - users = - [user1.nickname, user2.nickname] - |> Enum.map(&"@#{&1}") - |> Enum.join(", ") - - tags = ["x", "z"] |> Enum.join(", ") - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} removed tags: #{tags} from users: #{users}" - end - - test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do - assert empty_json_response(conn) - assert User.get_cached_by_id(user3.id).tags == ["unchanged"] - end - end - - describe "/api/pleroma/admin/users/:nickname/permission_group" do - test "GET is giving user_info", %{admin: admin, conn: conn} do - conn = - conn - |> put_req_header("accept", "application/json") - |> get("/api/pleroma/admin/users/#{admin.nickname}/permission_group/") - - assert json_response(conn, 200) == %{ - "is_admin" => true, - "is_moderator" => false - } - end - - test "/:right POST, can add to a permission group", %{admin: admin, conn: conn} do - user = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin") - - assert json_response(conn, 200) == %{ - "is_admin" => true - } - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} made @#{user.nickname} admin" - end - - test "/:right POST, can add to a permission group (multiple)", %{admin: admin, conn: conn} do - user_one = insert(:user) - user_two = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/users/permission_group/admin", %{ - nicknames: [user_one.nickname, user_two.nickname] - }) - - assert json_response(conn, 200) == %{"is_admin" => true} - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin" - end - - test "/:right DELETE, can remove from a permission group", %{admin: admin, conn: conn} do - user = insert(:user, is_admin: true) - - conn = - conn - |> put_req_header("accept", "application/json") - |> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin") - - assert json_response(conn, 200) == %{"is_admin" => false} - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} revoked admin role from @#{user.nickname}" - end - - test "/:right DELETE, can remove from a permission group (multiple)", %{ - admin: admin, - conn: conn - } do - user_one = insert(:user, is_admin: true) - user_two = insert(:user, is_admin: true) - - conn = - conn - |> put_req_header("accept", "application/json") - |> delete("/api/pleroma/admin/users/permission_group/admin", %{ - nicknames: [user_one.nickname, user_two.nickname] - }) - - assert json_response(conn, 200) == %{"is_admin" => false} - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} revoked admin role from @#{user_one.nickname}, @#{ - user_two.nickname - }" - end - end - - test "/api/pleroma/admin/users/:nickname/password_reset", %{conn: conn} do - 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) - - assert Regex.match?(~r/(http:\/\/|https:\/\/)/, resp["link"]) - end - - describe "GET /api/pleroma/admin/users" do - test "renders users array for the first page", %{conn: conn, admin: admin} do - user = insert(:user, local: false, tags: ["foo", "bar"]) - user2 = insert(:user, approval_pending: true, registration_reason: "I'm a chill dude") - - conn = get(conn, "/api/pleroma/admin/users?page=1") - - users = - [ - %{ - "deactivated" => admin.deactivated, - "id" => admin.id, - "nickname" => admin.nickname, - "roles" => %{"admin" => true, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(admin) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(admin.name || admin.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => admin.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - }, - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => false, - "tags" => ["foo", "bar"], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - }, - %{ - "deactivated" => user2.deactivated, - "id" => user2.id, - "nickname" => user2.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user2) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user2.name || user2.nickname), - "confirmation_pending" => false, - "approval_pending" => true, - "url" => user2.ap_id, - "registration_reason" => "I'm a chill dude", - "actor_type" => "Person" - } - ] - |> Enum.sort_by(& &1["nickname"]) - - assert json_response(conn, 200) == %{ - "count" => 3, - "page_size" => 50, - "users" => users - } - end - - test "pagination works correctly with service users", %{conn: conn} do - service1 = User.get_or_create_service_actor_by_ap_id(Web.base_url() <> "/meido", "meido") - - insert_list(25, :user) - - assert %{"count" => 26, "page_size" => 10, "users" => users1} = - conn - |> get("/api/pleroma/admin/users?page=1&filters=", %{page_size: "10"}) - |> json_response(200) - - assert Enum.count(users1) == 10 - assert service1 not in users1 - - assert %{"count" => 26, "page_size" => 10, "users" => users2} = - conn - |> get("/api/pleroma/admin/users?page=2&filters=", %{page_size: "10"}) - |> json_response(200) - - assert Enum.count(users2) == 10 - assert service1 not in users2 - - assert %{"count" => 26, "page_size" => 10, "users" => users3} = - conn - |> get("/api/pleroma/admin/users?page=3&filters=", %{page_size: "10"}) - |> json_response(200) - - assert Enum.count(users3) == 6 - assert service1 not in users3 - end - - test "renders empty array for the second page", %{conn: conn} do - insert(:user) - - conn = get(conn, "/api/pleroma/admin/users?page=2") - - assert json_response(conn, 200) == %{ - "count" => 2, - "page_size" => 50, - "users" => [] - } - end - - test "regular search", %{conn: conn} do - user = insert(:user, nickname: "bob") - - conn = get(conn, "/api/pleroma/admin/users?query=bo") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "search by domain", %{conn: conn} do - user = insert(:user, nickname: "nickname@domain.com") - insert(:user) - - conn = get(conn, "/api/pleroma/admin/users?query=domain.com") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "search by full nickname", %{conn: conn} do - user = insert(:user, nickname: "nickname@domain.com") - insert(:user) - - conn = get(conn, "/api/pleroma/admin/users?query=nickname@domain.com") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "search by display name", %{conn: conn} do - user = insert(:user, name: "Display name") - insert(:user) - - conn = get(conn, "/api/pleroma/admin/users?name=display") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "search by email", %{conn: conn} do - user = insert(:user, email: "email@example.com") - insert(:user) - - conn = get(conn, "/api/pleroma/admin/users?email=email@example.com") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "regular search with page size", %{conn: conn} do - user = insert(:user, nickname: "aalice") - user2 = insert(:user, nickname: "alice") - - conn1 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=1") - - assert json_response(conn1, 200) == %{ - "count" => 2, - "page_size" => 1, - "users" => [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - - conn2 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=2") - - assert json_response(conn2, 200) == %{ - "count" => 2, - "page_size" => 1, - "users" => [ - %{ - "deactivated" => user2.deactivated, - "id" => user2.id, - "nickname" => user2.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user2) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user2.name || user2.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user2.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "only local users" do - admin = insert(:user, is_admin: true, nickname: "john") - token = insert(:oauth_admin_token, user: admin) - user = insert(:user, nickname: "bob") - - insert(:user, nickname: "bobb", local: false) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - |> get("/api/pleroma/admin/users?query=bo&filters=local") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "only local users with no query", %{conn: conn, admin: old_admin} do - admin = insert(:user, is_admin: true, nickname: "john") - user = insert(:user, nickname: "bob") - - insert(:user, nickname: "bobb", local: false) - - conn = get(conn, "/api/pleroma/admin/users?filters=local") - - users = - [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - }, - %{ - "deactivated" => admin.deactivated, - "id" => admin.id, - "nickname" => admin.nickname, - "roles" => %{"admin" => true, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(admin) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(admin.name || admin.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => admin.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - }, - %{ - "deactivated" => false, - "id" => old_admin.id, - "local" => true, - "nickname" => old_admin.nickname, - "roles" => %{"admin" => true, "moderator" => false}, - "tags" => [], - "avatar" => User.avatar_url(old_admin) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => old_admin.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - |> Enum.sort_by(& &1["nickname"]) - - assert json_response(conn, 200) == %{ - "count" => 3, - "page_size" => 50, - "users" => users - } - end - - test "only unapproved users", %{conn: conn} do - user = - insert(:user, - nickname: "sadboy", - approval_pending: true, - registration_reason: "Plz let me in!" - ) - - insert(:user, nickname: "happyboy", approval_pending: false) - - conn = get(conn, "/api/pleroma/admin/users?filters=need_approval") - - users = - [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => true, - "url" => user.ap_id, - "registration_reason" => "Plz let me in!", - "actor_type" => "Person" - } - ] - |> Enum.sort_by(& &1["nickname"]) - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => users - } - end - - test "load only admins", %{conn: conn, admin: admin} do - second_admin = insert(:user, is_admin: true) - insert(:user) - insert(:user) - - conn = get(conn, "/api/pleroma/admin/users?filters=is_admin") - - users = - [ - %{ - "deactivated" => false, - "id" => admin.id, - "nickname" => admin.nickname, - "roles" => %{"admin" => true, "moderator" => false}, - "local" => admin.local, - "tags" => [], - "avatar" => User.avatar_url(admin) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(admin.name || admin.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => admin.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - }, - %{ - "deactivated" => false, - "id" => second_admin.id, - "nickname" => second_admin.nickname, - "roles" => %{"admin" => true, "moderator" => false}, - "local" => second_admin.local, - "tags" => [], - "avatar" => User.avatar_url(second_admin) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => second_admin.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - |> Enum.sort_by(& &1["nickname"]) - - assert json_response(conn, 200) == %{ - "count" => 2, - "page_size" => 50, - "users" => users - } - end - - test "load only moderators", %{conn: conn} do - moderator = insert(:user, is_moderator: true) - insert(:user) - insert(:user) - - conn = get(conn, "/api/pleroma/admin/users?filters=is_moderator") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => false, - "id" => moderator.id, - "nickname" => moderator.nickname, - "roles" => %{"admin" => false, "moderator" => true}, - "local" => moderator.local, - "tags" => [], - "avatar" => User.avatar_url(moderator) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(moderator.name || moderator.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => moderator.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "load users with tags list", %{conn: conn} do - user1 = insert(:user, tags: ["first"]) - user2 = insert(:user, tags: ["second"]) - insert(:user) - insert(:user) - - conn = get(conn, "/api/pleroma/admin/users?tags[]=first&tags[]=second") - - users = - [ - %{ - "deactivated" => false, - "id" => user1.id, - "nickname" => user1.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => user1.local, - "tags" => ["first"], - "avatar" => User.avatar_url(user1) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user1.name || user1.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user1.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - }, - %{ - "deactivated" => false, - "id" => user2.id, - "nickname" => user2.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => user2.local, - "tags" => ["second"], - "avatar" => User.avatar_url(user2) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user2.name || user2.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user2.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - |> Enum.sort_by(& &1["nickname"]) - - assert json_response(conn, 200) == %{ - "count" => 2, - "page_size" => 50, - "users" => users - } - end - - test "`active` filters out users pending approval", %{token: token} do - insert(:user, approval_pending: true) - %{id: user_id} = insert(:user, approval_pending: false) - %{id: admin_id} = token.user - - conn = - build_conn() - |> assign(:user, token.user) - |> assign(:token, token) - |> get("/api/pleroma/admin/users?filters=active") - - assert %{ - "count" => 2, - "page_size" => 50, - "users" => [ - %{"id" => ^admin_id}, - %{"id" => ^user_id} - ] - } = json_response(conn, 200) - end - - test "it works with multiple filters" do - admin = insert(:user, nickname: "john", is_admin: true) - token = insert(:oauth_admin_token, user: admin) - user = insert(:user, nickname: "bob", local: false, deactivated: true) - - insert(:user, nickname: "ken", local: true, deactivated: true) - insert(:user, nickname: "bobb", local: false, deactivated: false) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - |> get("/api/pleroma/admin/users?filters=deactivated,external") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => user.local, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "it omits relay user", %{admin: admin, conn: conn} do - assert %User{} = Relay.get_actor() - - conn = get(conn, "/api/pleroma/admin/users") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => admin.deactivated, - "id" => admin.id, - "nickname" => admin.nickname, - "roles" => %{"admin" => true, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(admin) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(admin.name || admin.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => admin.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - end - - test "PATCH /api/pleroma/admin/users/activate", %{admin: admin, conn: conn} do - user_one = insert(:user, deactivated: true) - user_two = insert(:user, deactivated: true) - - conn = - patch( - conn, - "/api/pleroma/admin/users/activate", - %{nicknames: [user_one.nickname, user_two.nickname]} - ) - - response = json_response(conn, 200) - assert Enum.map(response["users"], & &1["deactivated"]) == [false, false] - - 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 - user_one = insert(:user, deactivated: false) - user_two = insert(:user, deactivated: false) - - conn = - patch( - conn, - "/api/pleroma/admin/users/deactivate", - %{nicknames: [user_one.nickname, user_two.nickname]} - ) - - response = json_response(conn, 200) - assert Enum.map(response["users"], & &1["deactivated"]) == [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}" - end - - test "PATCH /api/pleroma/admin/users/approve", %{admin: admin, conn: conn} do - user_one = insert(:user, approval_pending: true) - user_two = insert(:user, approval_pending: true) - - conn = - patch( - conn, - "/api/pleroma/admin/users/approve", - %{nicknames: [user_one.nickname, user_two.nickname]} - ) - - response = json_response(conn, 200) - assert Enum.map(response["users"], & &1["approval_pending"]) == [false, false] - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} approved users: @#{user_one.nickname}, @#{user_two.nickname}" - end - - test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do - user = insert(:user) - - conn = patch(conn, "/api/pleroma/admin/users/#{user.nickname}/toggle_activation") - - assert json_response(conn, 200) == - %{ - "deactivated" => !user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deactivated users: @#{user.nickname}" - end - - describe "PUT disable_mfa" do - test "returns 200 and disable 2fa", %{conn: conn} do - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: "otp_secret", confirmed: true} - } - ) - - response = - conn - |> put("/api/pleroma/admin/users/disable_mfa", %{nickname: user.nickname}) - |> json_response(200) - - assert response == user.nickname - mfa_settings = refresh_record(user).multi_factor_authentication_settings - - refute mfa_settings.enabled - refute mfa_settings.totp.confirmed - end - - test "returns 404 if user not found", %{conn: conn} do - response = - conn - |> put("/api/pleroma/admin/users/disable_mfa", %{nickname: "nickname"}) - |> json_response(404) - - assert response == %{"error" => "Not found"} - end - end - - describe "GET /api/pleroma/admin/restart" do - setup do: clear_config(:configurable_from_database, true) - - test "pleroma restarts", %{conn: conn} do - capture_log(fn -> - assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{} - end) =~ "pleroma restarted" - - refute Restarter.Pleroma.need_reboot?() - end - end - - test "need_reboot flag", %{conn: conn} do - assert conn - |> get("/api/pleroma/admin/need_reboot") - |> json_response(200) == %{"need_reboot" => false} - - Restarter.Pleroma.need_reboot() - - assert conn - |> get("/api/pleroma/admin/need_reboot") - |> json_response(200) == %{"need_reboot" => true} - - on_exit(fn -> Restarter.Pleroma.refresh() end) - end - - describe "GET /api/pleroma/admin/users/:nickname/statuses" do - setup do - user = insert(:user) - - date1 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!() - date2 = (DateTime.to_unix(DateTime.utc_now()) + 1000) |> DateTime.from_unix!() - date3 = (DateTime.to_unix(DateTime.utc_now()) + 3000) |> DateTime.from_unix!() - - insert(:note_activity, user: user, published: date1) - insert(:note_activity, user: user, published: date2) - insert(:note_activity, user: user, published: date3) - - %{user: user} - end - - test "renders user's statuses", %{conn: conn, user: user} do - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses") - - assert json_response(conn, 200) |> length() == 3 - end - - test "renders user's statuses with a limit", %{conn: conn, user: user} do - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?page_size=2") - - assert json_response(conn, 200) |> length() == 2 - end - - test "doesn't return private statuses by default", %{conn: conn, user: user} do - {:ok, _private_status} = CommonAPI.post(user, %{status: "private", visibility: "private"}) - - {:ok, _public_status} = CommonAPI.post(user, %{status: "public", visibility: "public"}) - - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses") - - assert json_response(conn, 200) |> length() == 4 - end - - test "returns private statuses with godmode on", %{conn: conn, user: user} do - {:ok, _private_status} = CommonAPI.post(user, %{status: "private", visibility: "private"}) - - {:ok, _public_status} = CommonAPI.post(user, %{status: "public", visibility: "public"}) - - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?godmode=true") - - assert json_response(conn, 200) |> length() == 5 - end - - test "excludes reblogs by default", %{conn: conn, user: user} do - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "."}) - {:ok, %Activity{}} = CommonAPI.repeat(activity.id, other_user) - - conn_res = get(conn, "/api/pleroma/admin/users/#{other_user.nickname}/statuses") - assert json_response(conn_res, 200) |> length() == 0 - - conn_res = - get(conn, "/api/pleroma/admin/users/#{other_user.nickname}/statuses?with_reblogs=true") - - assert json_response(conn_res, 200) |> length() == 1 - end - end - - describe "GET /api/pleroma/admin/users/:nickname/chats" do - setup do - user = insert(:user) - recipients = insert_list(3, :user) - - Enum.each(recipients, fn recipient -> - CommonAPI.post_chat_message(user, recipient, "yo") - end) - - %{user: user} - end - - test "renders user's chats", %{conn: conn, user: user} do - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/chats") - - assert json_response(conn, 200) |> length() == 3 - end - end - - describe "GET /api/pleroma/admin/users/:nickname/chats unauthorized" do - setup do - user = insert(:user) - recipient = insert(:user) - CommonAPI.post_chat_message(user, recipient, "yo") - %{conn: conn} = oauth_access(["read:chats"]) - %{conn: conn, user: user} - end - - test "returns 403", %{conn: conn, user: user} do - conn - |> get("/api/pleroma/admin/users/#{user.nickname}/chats") - |> json_response(403) - end - end - - describe "GET /api/pleroma/admin/users/:nickname/chats unauthenticated" do - setup do - user = insert(:user) - recipient = insert(:user) - CommonAPI.post_chat_message(user, recipient, "yo") - %{conn: build_conn(), user: user} - end - - test "returns 403", %{conn: conn, user: user} do - conn - |> get("/api/pleroma/admin/users/#{user.nickname}/chats") - |> json_response(403) - end - end - - describe "GET /api/pleroma/admin/moderation_log" do - setup do - moderator = insert(:user, is_moderator: true) - - %{moderator: moderator} - end - - test "returns the log", %{conn: conn, admin: admin} do - Repo.insert(%ModerationLog{ - data: %{ - actor: %{ - "id" => admin.id, - "nickname" => admin.nickname, - "type" => "user" - }, - action: "relay_follow", - target: "https://example.org/relay" - }, - inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second) - }) - - Repo.insert(%ModerationLog{ - data: %{ - actor: %{ - "id" => admin.id, - "nickname" => admin.nickname, - "type" => "user" - }, - action: "relay_unfollow", - target: "https://example.org/relay" - }, - inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second) - }) - - conn = get(conn, "/api/pleroma/admin/moderation_log") - - response = json_response(conn, 200) - [first_entry, second_entry] = response["items"] - - assert response["total"] == 2 - assert first_entry["data"]["action"] == "relay_unfollow" - - assert first_entry["message"] == - "@#{admin.nickname} unfollowed relay: https://example.org/relay" - - assert second_entry["data"]["action"] == "relay_follow" - - assert second_entry["message"] == - "@#{admin.nickname} followed relay: https://example.org/relay" - end - - test "returns the log with pagination", %{conn: conn, admin: admin} do - Repo.insert(%ModerationLog{ - data: %{ - actor: %{ - "id" => admin.id, - "nickname" => admin.nickname, - "type" => "user" - }, - action: "relay_follow", - target: "https://example.org/relay" - }, - inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second) - }) - - Repo.insert(%ModerationLog{ - data: %{ - actor: %{ - "id" => admin.id, - "nickname" => admin.nickname, - "type" => "user" - }, - action: "relay_unfollow", - target: "https://example.org/relay" - }, - inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second) - }) - - conn1 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=1") - - response1 = json_response(conn1, 200) - [first_entry] = response1["items"] - - assert response1["total"] == 2 - assert response1["items"] |> length() == 1 - assert first_entry["data"]["action"] == "relay_unfollow" - - assert first_entry["message"] == - "@#{admin.nickname} unfollowed relay: https://example.org/relay" - - conn2 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=2") - - response2 = json_response(conn2, 200) - [second_entry] = response2["items"] - - assert response2["total"] == 2 - assert response2["items"] |> length() == 1 - assert second_entry["data"]["action"] == "relay_follow" - - assert second_entry["message"] == - "@#{admin.nickname} followed relay: https://example.org/relay" - end - - test "filters log by date", %{conn: conn, admin: admin} do - first_date = "2017-08-15T15:47:06Z" - second_date = "2017-08-20T15:47:06Z" - - Repo.insert(%ModerationLog{ - data: %{ - actor: %{ - "id" => admin.id, - "nickname" => admin.nickname, - "type" => "user" - }, - action: "relay_follow", - target: "https://example.org/relay" - }, - inserted_at: NaiveDateTime.from_iso8601!(first_date) - }) - - Repo.insert(%ModerationLog{ - data: %{ - actor: %{ - "id" => admin.id, - "nickname" => admin.nickname, - "type" => "user" - }, - action: "relay_unfollow", - target: "https://example.org/relay" - }, - inserted_at: NaiveDateTime.from_iso8601!(second_date) - }) - - conn1 = - get( - conn, - "/api/pleroma/admin/moderation_log?start_date=#{second_date}" - ) - - response1 = json_response(conn1, 200) - [first_entry] = response1["items"] - - assert response1["total"] == 1 - assert first_entry["data"]["action"] == "relay_unfollow" - - assert first_entry["message"] == - "@#{admin.nickname} unfollowed relay: https://example.org/relay" - end - - test "returns log filtered by user", %{conn: conn, admin: admin, moderator: moderator} do - Repo.insert(%ModerationLog{ - data: %{ - actor: %{ - "id" => admin.id, - "nickname" => admin.nickname, - "type" => "user" - }, - action: "relay_follow", - target: "https://example.org/relay" - } - }) - - Repo.insert(%ModerationLog{ - data: %{ - actor: %{ - "id" => moderator.id, - "nickname" => moderator.nickname, - "type" => "user" - }, - action: "relay_unfollow", - target: "https://example.org/relay" - } - }) - - conn1 = get(conn, "/api/pleroma/admin/moderation_log?user_id=#{moderator.id}") - - response1 = json_response(conn1, 200) - [first_entry] = response1["items"] - - assert response1["total"] == 1 - assert get_in(first_entry, ["data", "actor", "id"]) == moderator.id - end - - test "returns log filtered by search", %{conn: conn, moderator: moderator} do - ModerationLog.insert_log(%{ - actor: moderator, - action: "relay_follow", - target: "https://example.org/relay" - }) - - ModerationLog.insert_log(%{ - actor: moderator, - action: "relay_unfollow", - target: "https://example.org/relay" - }) - - conn1 = get(conn, "/api/pleroma/admin/moderation_log?search=unfo") - - response1 = json_response(conn1, 200) - [first_entry] = response1["items"] - - assert response1["total"] == 1 - - assert get_in(first_entry, ["data", "message"]) == - "@#{moderator.nickname} unfollowed relay: https://example.org/relay" - end - end - - test "gets a remote users when [:instance, :limit_to_local_content] is set to :unauthenticated", - %{conn: conn} do - clear_config(Pleroma.Config.get([:instance, :limit_to_local_content]), :unauthenticated) - user = insert(:user, %{local: false, nickname: "u@peer1.com"}) - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials") - - assert json_response(conn, 200) - end - - describe "GET /users/:nickname/credentials" do - test "gets the user credentials", %{conn: conn} do - user = insert(:user) - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials") - - response = assert json_response(conn, 200) - assert response["email"] == user.email - end - - test "returns 403 if requested by a non-admin" do - user = insert(:user) - - conn = - build_conn() - |> assign(:user, user) - |> get("/api/pleroma/admin/users/#{user.nickname}/credentials") - - assert json_response(conn, :forbidden) - end - end - - describe "PATCH /users/:nickname/credentials" do - setup do - user = insert(:user) - [user: user] - end - - test "changes password and email", %{conn: conn, admin: admin, user: user} do - assert user.password_reset_pending == false - - 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, 200) == %{"status" => "success"} - - ObanHelpers.perform_all() - - updated_user = User.get_by_id(user.id) - - assert updated_user.email == "new_email@example.com" - assert updated_user.name == "new_name" - assert updated_user.password_hash != user.password_hash - assert updated_user.password_reset_pending == true - - [log_entry2, log_entry1] = ModerationLog |> Repo.all() |> Enum.sort() - - assert ModerationLog.get_log_entry_message(log_entry1) == - "@#{admin.nickname} updated users: @#{user.nickname}" - - assert ModerationLog.get_log_entry_message(log_entry2) == - "@#{admin.nickname} forced password reset for users: @#{user.nickname}" - end - - test "returns 403 if requested by a non-admin", %{user: user} do - conn = - build_conn() - |> assign(:user, user) - |> patch("/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" - - assert patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{ - "actor_type" => "Service" - }) - |> json_response(200) == %{"status" => "success"} - - updated_user = User.get_by_id(user.id) - - assert updated_user.actor_type == "Service" - - assert patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{ - "actor_type" => "Application" - }) - |> json_response(400) == %{"errors" => %{"actor_type" => "is invalid"}} - end - - test "update non existing user", %{conn: conn} do - assert patch(conn, "/api/pleroma/admin/users/non-existing/credentials", %{ - "password" => "new_password" - }) - |> json_response(404) == %{"error" => "Not found"} - end - end - - describe "PATCH /users/:nickname/force_password_reset" do - test "sets password_reset_pending to true", %{conn: conn} do - user = insert(:user) - assert user.password_reset_pending == false - - conn = - patch(conn, "/api/pleroma/admin/users/force_password_reset", %{nicknames: [user.nickname]}) - - assert empty_json_response(conn) == "" - - ObanHelpers.perform_all() - - assert User.get_by_id(user.id).password_reset_pending == true - end - end - - describe "instances" do - test "GET /instances/:instance/statuses", %{conn: conn} do - user = insert(:user, local: false, nickname: "archaeme@archae.me") - user2 = insert(:user, local: false, nickname: "test@test.com") - insert_pair(:note_activity, user: user) - activity = insert(:note_activity, user: user2) - - ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses") - - response = json_response(ret_conn, 200) - - assert length(response) == 2 - - ret_conn = get(conn, "/api/pleroma/admin/instances/test.com/statuses") - - response = json_response(ret_conn, 200) - - assert length(response) == 1 - - ret_conn = get(conn, "/api/pleroma/admin/instances/nonexistent.com/statuses") - - response = json_response(ret_conn, 200) - - assert Enum.empty?(response) - - CommonAPI.repeat(activity.id, user) - - ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses") - response = json_response(ret_conn, 200) - assert length(response) == 2 - - ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses?with_reblogs=true") - response = json_response(ret_conn, 200) - assert length(response) == 3 - end - end - - describe "PATCH /confirm_email" do - test "it confirms emails of two users", %{conn: conn, admin: admin} do - [first_user, second_user] = insert_pair(:user, confirmation_pending: true) - - assert first_user.confirmation_pending == true - assert second_user.confirmation_pending == true - - ret_conn = - patch(conn, "/api/pleroma/admin/users/confirm_email", %{ - nicknames: [ - first_user.nickname, - second_user.nickname - ] - }) - - assert ret_conn.status == 200 - - assert first_user.confirmation_pending == true - assert second_user.confirmation_pending == true - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} confirmed email for users: @#{first_user.nickname}, @#{ - second_user.nickname - }" - end - end - - describe "PATCH /resend_confirmation_email" do - test "it resend emails for two users", %{conn: conn, admin: admin} do - [first_user, second_user] = insert_pair(:user, confirmation_pending: true) - - ret_conn = - patch(conn, "/api/pleroma/admin/users/resend_confirmation_email", %{ - nicknames: [ - first_user.nickname, - second_user.nickname - ] - }) - - assert ret_conn.status == 200 - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} re-sent confirmation email for users: @#{first_user.nickname}, @#{ - second_user.nickname - }" - - ObanHelpers.perform_all() - - Pleroma.Emails.UserEmail.account_confirmation_email(first_user) - # temporary hackney fix until hackney max_connections bug is fixed - # https://git.pleroma.social/pleroma/pleroma/-/issues/2101 - |> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]]) - |> assert_email_sent() - end - end - - describe "/api/pleroma/admin/stats" do - test "status visibility count", %{conn: conn} do - admin = insert(:user, is_admin: true) - user = insert(:user) - CommonAPI.post(user, %{visibility: "public", status: "hey"}) - CommonAPI.post(user, %{visibility: "unlisted", status: "hey"}) - CommonAPI.post(user, %{visibility: "unlisted", status: "hey"}) - - response = - conn - |> assign(:user, admin) - |> get("/api/pleroma/admin/stats") - |> json_response(200) - - assert %{"direct" => 0, "private" => 0, "public" => 1, "unlisted" => 2} = - response["status_visibility"] - end - - test "by instance", %{conn: conn} do - admin = insert(:user, is_admin: true) - user1 = insert(:user) - instance2 = "instance2.tld" - user2 = insert(:user, %{ap_id: "https://#{instance2}/@actor"}) - - CommonAPI.post(user1, %{visibility: "public", status: "hey"}) - CommonAPI.post(user2, %{visibility: "unlisted", status: "hey"}) - CommonAPI.post(user2, %{visibility: "private", status: "hey"}) - - response = - conn - |> assign(:user, admin) - |> get("/api/pleroma/admin/stats", instance: instance2) - |> json_response(200) - - assert %{"direct" => 0, "private" => 1, "public" => 0, "unlisted" => 1} = - response["status_visibility"] - end - end -end - -# Needed for testing -defmodule Pleroma.Web.Endpoint.NotReal do -end - -defmodule Pleroma.Captcha.NotReal do -end diff --git a/test/web/admin_api/controllers/chat_controller_test.exs b/test/web/admin_api/controllers/chat_controller_test.exs @@ -1,219 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.ChatControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - alias Pleroma.Chat - alias Pleroma.Chat.MessageReference - alias Pleroma.Config - alias Pleroma.ModerationLog - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.Web.CommonAPI - - defp admin_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 "DELETE /api/pleroma/admin/chats/:id/messages/:message_id" do - setup do: admin_setup() - - test "it deletes a message from the chat", %{conn: conn, admin: admin} do - user = insert(:user) - recipient = insert(:user) - - {:ok, message} = - CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend") - - object = Object.normalize(message, false) - - chat = Chat.get(user.id, recipient.ap_id) - recipient_chat = Chat.get(recipient.id, user.ap_id) - - cm_ref = MessageReference.for_chat_and_object(chat, object) - recipient_cm_ref = MessageReference.for_chat_and_object(recipient_chat, object) - - result = - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}") - |> json_response_and_validate_schema(200) - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deleted chat message ##{cm_ref.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 - end - - describe "GET /api/pleroma/admin/chats/:id/messages" do - setup do: admin_setup() - - test "it paginates", %{conn: conn} do - user = insert(:user) - recipient = insert(:user) - - Enum.each(1..30, fn _ -> - {:ok, _} = CommonAPI.post_chat_message(user, recipient, "hey") - end) - - chat = Chat.get(user.id, recipient.ap_id) - - result = - conn - |> get("/api/pleroma/admin/chats/#{chat.id}/messages") - |> json_response_and_validate_schema(200) - - assert length(result) == 20 - - result = - conn - |> get("/api/pleroma/admin/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}") - |> json_response_and_validate_schema(200) - - assert length(result) == 10 - end - - test "it returns the messages for a given chat", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - - {:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey") - {:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey") - {:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?") - {:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?") - - chat = Chat.get(user.id, other_user.ap_id) - - result = - conn - |> get("/api/pleroma/admin/chats/#{chat.id}/messages") - |> json_response_and_validate_schema(200) - - result - |> Enum.each(fn message -> - assert message["chat_id"] == chat.id |> to_string() - end) - - assert length(result) == 3 - end - end - - describe "GET /api/pleroma/admin/chats/:id" do - setup do: admin_setup() - - test "it returns a chat", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - - result = - conn - |> get("/api/pleroma/admin/chats/#{chat.id}") - |> json_response_and_validate_schema(200) - - assert result["id"] == to_string(chat.id) - assert %{} = result["sender"] - assert %{} = result["receiver"] - refute result["account"] - end - end - - describe "unauthorized chat moderation" do - setup do - user = insert(:user) - recipient = insert(:user) - - {:ok, message} = CommonAPI.post_chat_message(user, recipient, "Yo") - object = Object.normalize(message, false) - chat = Chat.get(user.id, recipient.ap_id) - cm_ref = MessageReference.for_chat_and_object(chat, object) - - %{conn: conn} = oauth_access(["read:chats", "write:chats"]) - %{conn: conn, chat: chat, cm_ref: cm_ref} - end - - test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{ - conn: conn, - chat: chat, - cm_ref: cm_ref - } do - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}") - |> json_response(403) - - assert MessageReference.get_by_id(cm_ref.id) == cm_ref - end - - test "GET /api/pleroma/admin/chats/:id/messages", %{conn: conn, chat: chat} do - conn - |> get("/api/pleroma/admin/chats/#{chat.id}/messages") - |> json_response(403) - end - - test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do - conn - |> get("/api/pleroma/admin/chats/#{chat.id}") - |> json_response(403) - end - end - - describe "unauthenticated chat moderation" do - setup do - user = insert(:user) - recipient = insert(:user) - - {:ok, message} = CommonAPI.post_chat_message(user, recipient, "Yo") - object = Object.normalize(message, false) - chat = Chat.get(user.id, recipient.ap_id) - cm_ref = MessageReference.for_chat_and_object(chat, object) - - %{conn: build_conn(), chat: chat, cm_ref: cm_ref} - end - - test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{ - conn: conn, - chat: chat, - cm_ref: cm_ref - } do - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}") - |> json_response(403) - - assert MessageReference.get_by_id(cm_ref.id) == cm_ref - end - - test "GET /api/pleroma/admin/chats/:id/messages", %{conn: conn, chat: chat} do - conn - |> get("/api/pleroma/admin/chats/#{chat.id}/messages") - |> json_response(403) - end - - test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do - conn - |> get("/api/pleroma/admin/chats/#{chat.id}") - |> json_response(403) - end - end -end diff --git a/test/web/admin_api/controllers/config_controller_test.exs b/test/web/admin_api/controllers/config_controller_test.exs @@ -1,1465 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do - use Pleroma.Web.ConnCase, async: true - - import ExUnit.CaptureLog - import Pleroma.Factory - - alias Pleroma.Config - alias Pleroma.ConfigDB - - 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/config" do - setup do: clear_config(:configurable_from_database, true) - - test "when configuration from database is off", %{conn: conn} do - Config.put(:configurable_from_database, false) - conn = get(conn, "/api/pleroma/admin/config") - - assert json_response_and_validate_schema(conn, 400) == - %{ - "error" => "To use this endpoint you need to enable configuration from database." - } - end - - test "with settings only in db", %{conn: conn} do - config1 = insert(:config) - config2 = insert(:config) - - conn = get(conn, "/api/pleroma/admin/config?only_db=true") - - %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => key1, - "value" => _ - }, - %{ - "group" => ":pleroma", - "key" => key2, - "value" => _ - } - ] - } = json_response_and_validate_schema(conn, 200) - - assert key1 == inspect(config1.key) - assert key2 == inspect(config2.key) - end - - test "db is added to settings that are in db", %{conn: conn} do - _config = insert(:config, key: ":instance", value: [name: "Some name"]) - - %{"configs" => configs} = - conn - |> get("/api/pleroma/admin/config") - |> json_response_and_validate_schema(200) - - [instance_config] = - Enum.filter(configs, fn %{"group" => group, "key" => key} -> - group == ":pleroma" and key == ":instance" - end) - - assert instance_config["db"] == [":name"] - end - - test "merged default setting with db settings", %{conn: conn} do - config1 = insert(:config) - config2 = insert(:config) - - config3 = - insert(:config, - value: [k1: :v1, k2: :v2] - ) - - %{"configs" => configs} = - conn - |> get("/api/pleroma/admin/config") - |> json_response_and_validate_schema(200) - - assert length(configs) > 3 - - saved_configs = [config1, config2, config3] - keys = Enum.map(saved_configs, &inspect(&1.key)) - - received_configs = - Enum.filter(configs, fn %{"group" => group, "key" => key} -> - group == ":pleroma" and key in keys - end) - - assert length(received_configs) == 3 - - db_keys = - config3.value - |> Keyword.keys() - |> ConfigDB.to_json_types() - - keys = Enum.map(saved_configs -- [config3], &inspect(&1.key)) - - values = Enum.map(saved_configs, &ConfigDB.to_json_types(&1.value)) - - mapset_keys = MapSet.new(keys ++ db_keys) - - Enum.each(received_configs, fn %{"value" => value, "db" => db} -> - db = MapSet.new(db) - assert MapSet.subset?(db, mapset_keys) - - assert value in values - end) - end - - test "subkeys with full update right merge", %{conn: conn} do - insert(:config, - key: ":emoji", - value: [groups: [a: 1, b: 2], key: [a: 1]] - ) - - insert(:config, - key: ":assets", - value: [mascots: [a: 1, b: 2], key: [a: 1]] - ) - - %{"configs" => configs} = - conn - |> get("/api/pleroma/admin/config") - |> json_response_and_validate_schema(200) - - vals = - Enum.filter(configs, fn %{"group" => group, "key" => key} -> - group == ":pleroma" and key in [":emoji", ":assets"] - end) - - emoji = Enum.find(vals, fn %{"key" => key} -> key == ":emoji" end) - assets = Enum.find(vals, fn %{"key" => key} -> key == ":assets" end) - - emoji_val = ConfigDB.to_elixir_types(emoji["value"]) - assets_val = ConfigDB.to_elixir_types(assets["value"]) - - assert emoji_val[:groups] == [a: 1, b: 2] - assert assets_val[:mascots] == [a: 1, b: 2] - end - - test "with valid `admin_token` query parameter, skips OAuth scopes check" do - clear_config([:admin_token], "password123") - - build_conn() - |> get("/api/pleroma/admin/config?admin_token=password123") - |> json_response_and_validate_schema(200) - end - end - - test "POST /api/pleroma/admin/config error", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{"configs" => []}) - - assert json_response_and_validate_schema(conn, 400) == - %{"error" => "To use this endpoint you need to enable configuration from database."} - end - - describe "POST /api/pleroma/admin/config" do - setup do - http = Application.get_env(:pleroma, :http) - - on_exit(fn -> - Application.delete_env(:pleroma, :key1) - Application.delete_env(:pleroma, :key2) - Application.delete_env(:pleroma, :key3) - Application.delete_env(:pleroma, :key4) - Application.delete_env(:pleroma, :keyaa1) - Application.delete_env(:pleroma, :keyaa2) - Application.delete_env(:pleroma, Pleroma.Web.Endpoint.NotReal) - Application.delete_env(:pleroma, Pleroma.Captcha.NotReal) - Application.put_env(:pleroma, :http, http) - Application.put_env(:tesla, :adapter, Tesla.Mock) - Restarter.Pleroma.refresh() - end) - end - - setup do: clear_config(:configurable_from_database, true) - - @tag capture_log: true - test "create new config setting in db", %{conn: conn} do - ueberauth = Application.get_env(:ueberauth, Ueberauth) - on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":key1", value: "value1"}, - %{ - group: ":ueberauth", - key: "Ueberauth", - value: [%{"tuple" => [":consumer_secret", "aaaa"]}] - }, - %{ - group: ":pleroma", - key: ":key2", - value: %{ - ":nested_1" => "nested_value1", - ":nested_2" => [ - %{":nested_22" => "nested_value222"}, - %{":nested_33" => %{":nested_44" => "nested_444"}} - ] - } - }, - %{ - group: ":pleroma", - key: ":key3", - value: [ - %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, - %{"nested_4" => true} - ] - }, - %{ - group: ":pleroma", - key: ":key4", - value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"} - }, - %{ - group: ":idna", - key: ":key5", - value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]} - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => "value1", - "db" => [":key1"] - }, - %{ - "group" => ":ueberauth", - "key" => "Ueberauth", - "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}], - "db" => [":consumer_secret"] - }, - %{ - "group" => ":pleroma", - "key" => ":key2", - "value" => %{ - ":nested_1" => "nested_value1", - ":nested_2" => [ - %{":nested_22" => "nested_value222"}, - %{":nested_33" => %{":nested_44" => "nested_444"}} - ] - }, - "db" => [":key2"] - }, - %{ - "group" => ":pleroma", - "key" => ":key3", - "value" => [ - %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, - %{"nested_4" => true} - ], - "db" => [":key3"] - }, - %{ - "group" => ":pleroma", - "key" => ":key4", - "value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"}, - "db" => [":key4"] - }, - %{ - "group" => ":idna", - "key" => ":key5", - "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}, - "db" => [":key5"] - } - ], - "need_reboot" => false - } - - assert Application.get_env(:pleroma, :key1) == "value1" - - assert Application.get_env(:pleroma, :key2) == %{ - nested_1: "nested_value1", - nested_2: [ - %{nested_22: "nested_value222"}, - %{nested_33: %{nested_44: "nested_444"}} - ] - } - - assert Application.get_env(:pleroma, :key3) == [ - %{"nested_3" => :nested_3, "nested_33" => "nested_33"}, - %{"nested_4" => true} - ] - - assert Application.get_env(:pleroma, :key4) == %{ - "endpoint" => "https://example.com", - nested_5: :upload - } - - assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []} - end - - 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) - - on_exit(fn -> - Application.put_env(:quack, :level, level) - Application.put_env(:quack, :meta, meta) - Application.put_env(:quack, :webhook_url, webhook_url) - end) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":quack", - key: ":level", - value: ":info" - }, - %{ - group: ":quack", - key: ":meta", - value: [":none"] - }, - %{ - group: ":quack", - key: ":webhook_url", - value: "https://hooks.slack.com/services/KEY" - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":quack", - "key" => ":level", - "value" => ":info", - "db" => [":level"] - }, - %{ - "group" => ":quack", - "key" => ":meta", - "value" => [":none"], - "db" => [":meta"] - }, - %{ - "group" => ":quack", - "key" => ":webhook_url", - "value" => "https://hooks.slack.com/services/KEY", - "db" => [":webhook_url"] - } - ], - "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" - end - - test "saving config with partial update", %{conn: conn} do - insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2)) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]} - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key1", 1]}, - %{"tuple" => [":key2", 2]}, - %{"tuple" => [":key3", 3]} - ], - "db" => [":key1", ":key2", ":key3"] - } - ], - "need_reboot" => false - } - end - - test "saving config which need pleroma reboot", %{conn: conn} do - chat = Config.get(:chat) - on_exit(fn -> Config.put(:chat, chat) end) - - assert conn - |> put_req_header("content-type", "application/json") - |> post( - "/api/pleroma/admin/config", - %{ - configs: [ - %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]} - ] - } - ) - |> json_response_and_validate_schema(200) == %{ - "configs" => [ - %{ - "db" => [":enabled"], - "group" => ":pleroma", - "key" => ":chat", - "value" => [%{"tuple" => [":enabled", true]}] - } - ], - "need_reboot" => true - } - - configs = - conn - |> get("/api/pleroma/admin/config") - |> json_response_and_validate_schema(200) - - assert configs["need_reboot"] - - capture_log(fn -> - assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == - %{} - end) =~ "pleroma restarted" - - configs = - conn - |> get("/api/pleroma/admin/config") - |> json_response_and_validate_schema(200) - - assert configs["need_reboot"] == false - end - - test "update setting which need reboot, don't change reboot flag until reboot", %{conn: conn} do - chat = Config.get(:chat) - on_exit(fn -> Config.put(:chat, chat) end) - - assert conn - |> put_req_header("content-type", "application/json") - |> post( - "/api/pleroma/admin/config", - %{ - configs: [ - %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]} - ] - } - ) - |> json_response_and_validate_schema(200) == %{ - "configs" => [ - %{ - "db" => [":enabled"], - "group" => ":pleroma", - "key" => ":chat", - "value" => [%{"tuple" => [":enabled", true]}] - } - ], - "need_reboot" => true - } - - assert conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]} - ] - }) - |> json_response_and_validate_schema(200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key3", 3]} - ], - "db" => [":key3"] - } - ], - "need_reboot" => true - } - - capture_log(fn -> - assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == - %{} - end) =~ "pleroma restarted" - - configs = - conn - |> get("/api/pleroma/admin/config") - |> json_response_and_validate_schema(200) - - assert configs["need_reboot"] == false - end - - test "saving config with nested merge", %{conn: conn} do - insert(:config, key: :key1, value: [key1: 1, key2: [k1: 1, k2: 2]]) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: ":key1", - value: [ - %{"tuple" => [":key3", 3]}, - %{ - "tuple" => [ - ":key2", - [ - %{"tuple" => [":k2", 1]}, - %{"tuple" => [":k3", 3]} - ] - ] - } - ] - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key1", 1]}, - %{"tuple" => [":key3", 3]}, - %{ - "tuple" => [ - ":key2", - [ - %{"tuple" => [":k1", 1]}, - %{"tuple" => [":k2", 1]}, - %{"tuple" => [":k3", 3]} - ] - ] - } - ], - "db" => [":key1", ":key3", ":key2"] - } - ], - "need_reboot" => false - } - end - - test "saving special atoms", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{ - "tuple" => [ - ":ssl_options", - [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}] - ] - } - ] - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{ - "tuple" => [ - ":ssl_options", - [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}] - ] - } - ], - "db" => [":ssl_options"] - } - ], - "need_reboot" => false - } - - assert Application.get_env(:pleroma, :key1) == [ - ssl_options: [versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]] - ] - end - - test "saving full setting if value is in full_key_update list", %{conn: conn} do - backends = Application.get_env(:logger, :backends) - on_exit(fn -> Application.put_env(:logger, :backends, backends) end) - - insert(:config, - group: :logger, - key: :backends, - value: [] - ) - - Pleroma.Config.TransferTask.load_and_update_env([], false) - - assert Application.get_env(:logger, :backends) == [] - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":logger", - key: ":backends", - value: [":console"] - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":logger", - "key" => ":backends", - "value" => [ - ":console" - ], - "db" => [":backends"] - } - ], - "need_reboot" => false - } - - assert Application.get_env(:logger, :backends) == [ - :console - ] - end - - test "saving full setting if value is not keyword", %{conn: conn} do - insert(:config, - group: :tesla, - key: :adapter, - value: Tesla.Adapter.Hackey - ) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{group: ":tesla", key: ":adapter", value: "Tesla.Adapter.Httpc"} - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":tesla", - "key" => ":adapter", - "value" => "Tesla.Adapter.Httpc", - "db" => [":adapter"] - } - ], - "need_reboot" => false - } - end - - test "update config setting & delete with fallback to default value", %{ - conn: conn, - admin: admin, - token: token - } do - ueberauth = Application.get_env(:ueberauth, Ueberauth) - insert(:config, key: :keyaa1) - insert(:config, key: :keyaa2) - - config3 = - insert(:config, - group: :ueberauth, - key: Ueberauth - ) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":keyaa1", value: "another_value"}, - %{group: ":pleroma", key: ":keyaa2", value: "another_value"} - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":keyaa1", - "value" => "another_value", - "db" => [":keyaa1"] - }, - %{ - "group" => ":pleroma", - "key" => ":keyaa2", - "value" => "another_value", - "db" => [":keyaa2"] - } - ], - "need_reboot" => false - } - - assert Application.get_env(:pleroma, :keyaa1) == "another_value" - assert Application.get_env(:pleroma, :keyaa2) == "another_value" - assert Application.get_env(:ueberauth, Ueberauth) == config3.value - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":keyaa2", delete: true}, - %{ - group: ":ueberauth", - key: "Ueberauth", - delete: true - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [], - "need_reboot" => false - } - - assert Application.get_env(:ueberauth, Ueberauth) == ueberauth - refute Keyword.has_key?(Application.get_all_env(:pleroma), :keyaa2) - end - - test "common config example", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Captcha.NotReal", - "value" => [ - %{"tuple" => [":enabled", false]}, - %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]}, - %{"tuple" => [":seconds_valid", 60]}, - %{"tuple" => [":path", ""]}, - %{"tuple" => [":key1", nil]}, - %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}, - %{"tuple" => [":regex1", "~r/https:\/\/example.com/"]}, - %{"tuple" => [":regex2", "~r/https:\/\/example.com/u"]}, - %{"tuple" => [":regex3", "~r/https:\/\/example.com/i"]}, - %{"tuple" => [":regex4", "~r/https:\/\/example.com/s"]}, - %{"tuple" => [":name", "Pleroma"]} - ] - } - ] - }) - - assert Config.get([Pleroma.Captcha.NotReal, :name]) == "Pleroma" - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Captcha.NotReal", - "value" => [ - %{"tuple" => [":enabled", false]}, - %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]}, - %{"tuple" => [":seconds_valid", 60]}, - %{"tuple" => [":path", ""]}, - %{"tuple" => [":key1", nil]}, - %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}, - %{"tuple" => [":regex1", "~r/https:\\/\\/example.com/"]}, - %{"tuple" => [":regex2", "~r/https:\\/\\/example.com/u"]}, - %{"tuple" => [":regex3", "~r/https:\\/\\/example.com/i"]}, - %{"tuple" => [":regex4", "~r/https:\\/\\/example.com/s"]}, - %{"tuple" => [":name", "Pleroma"]} - ], - "db" => [ - ":enabled", - ":method", - ":seconds_valid", - ":path", - ":key1", - ":partial_chain", - ":regex1", - ":regex2", - ":regex3", - ":regex4", - ":name" - ] - } - ], - "need_reboot" => false - } - end - - test "tuples with more than two values", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Web.Endpoint.NotReal", - "value" => [ - %{ - "tuple" => [ - ":http", - [ - %{ - "tuple" => [ - ":key2", - [ - %{ - "tuple" => [ - ":_", - [ - %{ - "tuple" => [ - "/api/v1/streaming", - "Pleroma.Web.MastodonAPI.WebsocketHandler", - [] - ] - }, - %{ - "tuple" => [ - "/websocket", - "Phoenix.Endpoint.CowboyWebSocket", - %{ - "tuple" => [ - "Phoenix.Transports.WebSocket", - %{ - "tuple" => [ - "Pleroma.Web.Endpoint", - "Pleroma.Web.UserSocket", - [] - ] - } - ] - } - ] - }, - %{ - "tuple" => [ - ":_", - "Phoenix.Endpoint.Cowboy2Handler", - %{"tuple" => ["Pleroma.Web.Endpoint", []]} - ] - } - ] - ] - } - ] - ] - } - ] - ] - } - ] - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Web.Endpoint.NotReal", - "value" => [ - %{ - "tuple" => [ - ":http", - [ - %{ - "tuple" => [ - ":key2", - [ - %{ - "tuple" => [ - ":_", - [ - %{ - "tuple" => [ - "/api/v1/streaming", - "Pleroma.Web.MastodonAPI.WebsocketHandler", - [] - ] - }, - %{ - "tuple" => [ - "/websocket", - "Phoenix.Endpoint.CowboyWebSocket", - %{ - "tuple" => [ - "Phoenix.Transports.WebSocket", - %{ - "tuple" => [ - "Pleroma.Web.Endpoint", - "Pleroma.Web.UserSocket", - [] - ] - } - ] - } - ] - }, - %{ - "tuple" => [ - ":_", - "Phoenix.Endpoint.Cowboy2Handler", - %{"tuple" => ["Pleroma.Web.Endpoint", []]} - ] - } - ] - ] - } - ] - ] - } - ] - ] - } - ], - "db" => [":http"] - } - ], - "need_reboot" => false - } - end - - test "settings with nesting map", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key2", "some_val"]}, - %{ - "tuple" => [ - ":key3", - %{ - ":max_options" => 20, - ":max_option_chars" => 200, - ":min_expiration" => 0, - ":max_expiration" => 31_536_000, - "nested" => %{ - ":max_options" => 20, - ":max_option_chars" => 200, - ":min_expiration" => 0, - ":max_expiration" => 31_536_000 - } - } - ] - } - ] - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == - %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key2", "some_val"]}, - %{ - "tuple" => [ - ":key3", - %{ - ":max_expiration" => 31_536_000, - ":max_option_chars" => 200, - ":max_options" => 20, - ":min_expiration" => 0, - "nested" => %{ - ":max_expiration" => 31_536_000, - ":max_option_chars" => 200, - ":max_options" => 20, - ":min_expiration" => 0 - } - } - ] - } - ], - "db" => [":key2", ":key3"] - } - ], - "need_reboot" => false - } - end - - test "value as map", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => %{"key" => "some_val"} - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == - %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => %{"key" => "some_val"}, - "db" => [":key1"] - } - ], - "need_reboot" => false - } - end - - test "queues key as atom", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":oban", - "key" => ":queues", - "value" => [ - %{"tuple" => [":federator_incoming", 50]}, - %{"tuple" => [":federator_outgoing", 50]}, - %{"tuple" => [":web_push", 50]}, - %{"tuple" => [":mailer", 10]}, - %{"tuple" => [":transmogrifier", 20]}, - %{"tuple" => [":scheduled_activities", 10]}, - %{"tuple" => [":background", 5]} - ] - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":oban", - "key" => ":queues", - "value" => [ - %{"tuple" => [":federator_incoming", 50]}, - %{"tuple" => [":federator_outgoing", 50]}, - %{"tuple" => [":web_push", 50]}, - %{"tuple" => [":mailer", 10]}, - %{"tuple" => [":transmogrifier", 20]}, - %{"tuple" => [":scheduled_activities", 10]}, - %{"tuple" => [":background", 5]} - ], - "db" => [ - ":federator_incoming", - ":federator_outgoing", - ":web_push", - ":mailer", - ":transmogrifier", - ":scheduled_activities", - ":background" - ] - } - ], - "need_reboot" => false - } - end - - test "delete part of settings by atom subkeys", %{conn: conn} do - insert(:config, - key: :keyaa1, - value: [subkey1: "val1", subkey2: "val2", subkey3: "val3"] - ) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: ":keyaa1", - subkeys: [":subkey1", ":subkey3"], - delete: true - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":keyaa1", - "value" => [%{"tuple" => [":subkey2", "val2"]}], - "db" => [":subkey2"] - } - ], - "need_reboot" => false - } - end - - test "proxy tuple localhost", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: ":http", - value: [ - %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]} - ] - } - ] - }) - - assert %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":http", - "value" => value, - "db" => db - } - ] - } = json_response_and_validate_schema(conn, 200) - - assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]} in value - assert ":proxy_url" in db - end - - test "proxy tuple domain", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: ":http", - value: [ - %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]} - ] - } - ] - }) - - assert %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":http", - "value" => value, - "db" => db - } - ] - } = json_response_and_validate_schema(conn, 200) - - assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]} in value - assert ":proxy_url" in db - end - - test "proxy tuple ip", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: ":http", - value: [ - %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]} - ] - } - ] - }) - - assert %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":http", - "value" => value, - "db" => db - } - ] - } = json_response_and_validate_schema(conn, 200) - - assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]} in value - assert ":proxy_url" in db - end - - @tag capture_log: true - test "doesn't set keys not in the whitelist", %{conn: conn} do - clear_config(:database_config_whitelist, [ - {:pleroma, :key1}, - {:pleroma, :key2}, - {:pleroma, Pleroma.Captcha.NotReal}, - {:not_real} - ]) - - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":key1", value: "value1"}, - %{group: ":pleroma", key: ":key2", value: "value2"}, - %{group: ":pleroma", key: ":key3", value: "value3"}, - %{group: ":pleroma", key: "Pleroma.Web.Endpoint.NotReal", value: "value4"}, - %{group: ":pleroma", key: "Pleroma.Captcha.NotReal", value: "value5"}, - %{group: ":not_real", key: ":anything", value: "value6"} - ] - }) - - assert Application.get_env(:pleroma, :key1) == "value1" - assert Application.get_env(:pleroma, :key2) == "value2" - assert Application.get_env(:pleroma, :key3) == nil - assert Application.get_env(:pleroma, Pleroma.Web.Endpoint.NotReal) == nil - assert Application.get_env(:pleroma, Pleroma.Captcha.NotReal) == "value5" - assert Application.get_env(:not_real, :anything) == "value6" - end - - test "args for Pleroma.Upload.Filter.Mogrify with custom tuples", %{conn: conn} do - clear_config(Pleroma.Upload.Filter.Mogrify) - - assert conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: "Pleroma.Upload.Filter.Mogrify", - value: [ - %{"tuple" => [":args", ["auto-orient", "strip"]]} - ] - } - ] - }) - |> json_response_and_validate_schema(200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Upload.Filter.Mogrify", - "value" => [ - %{"tuple" => [":args", ["auto-orient", "strip"]]} - ], - "db" => [":args"] - } - ], - "need_reboot" => false - } - - assert Config.get(Pleroma.Upload.Filter.Mogrify) == [args: ["auto-orient", "strip"]] - - assert conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: "Pleroma.Upload.Filter.Mogrify", - value: [ - %{ - "tuple" => [ - ":args", - [ - "auto-orient", - "strip", - "{\"implode\", \"1\"}", - "{\"resize\", \"3840x1080>\"}" - ] - ] - } - ] - } - ] - }) - |> json_response(200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Upload.Filter.Mogrify", - "value" => [ - %{ - "tuple" => [ - ":args", - [ - "auto-orient", - "strip", - "{\"implode\", \"1\"}", - "{\"resize\", \"3840x1080>\"}" - ] - ] - } - ], - "db" => [":args"] - } - ], - "need_reboot" => false - } - - assert Config.get(Pleroma.Upload.Filter.Mogrify) == [ - args: ["auto-orient", "strip", {"implode", "1"}, {"resize", "3840x1080>"}] - ] - end - - test "enables the welcome messages", %{conn: conn} do - clear_config([:welcome]) - - params = %{ - "group" => ":pleroma", - "key" => ":welcome", - "value" => [ - %{ - "tuple" => [ - ":direct_message", - [ - %{"tuple" => [":enabled", true]}, - %{"tuple" => [":message", "Welcome to Pleroma!"]}, - %{"tuple" => [":sender_nickname", "pleroma"]} - ] - ] - }, - %{ - "tuple" => [ - ":chat_message", - [ - %{"tuple" => [":enabled", true]}, - %{"tuple" => [":message", "Welcome to Pleroma!"]}, - %{"tuple" => [":sender_nickname", "pleroma"]} - ] - ] - }, - %{ - "tuple" => [ - ":email", - [ - %{"tuple" => [":enabled", true]}, - %{"tuple" => [":sender", %{"tuple" => ["pleroma@dev.dev", "Pleroma"]}]}, - %{"tuple" => [":subject", "Welcome to <%= instance_name %>!"]}, - %{"tuple" => [":html", "Welcome to <%= instance_name %>!"]}, - %{"tuple" => [":text", "Welcome to <%= instance_name %>!"]} - ] - ] - } - ] - } - - refute Pleroma.User.WelcomeEmail.enabled?() - refute Pleroma.User.WelcomeMessage.enabled?() - refute Pleroma.User.WelcomeChatMessage.enabled?() - - res = - assert conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{"configs" => [params]}) - |> json_response_and_validate_schema(200) - - assert Pleroma.User.WelcomeEmail.enabled?() - assert Pleroma.User.WelcomeMessage.enabled?() - assert Pleroma.User.WelcomeChatMessage.enabled?() - - assert res == %{ - "configs" => [ - %{ - "db" => [":direct_message", ":chat_message", ":email"], - "group" => ":pleroma", - "key" => ":welcome", - "value" => params["value"] - } - ], - "need_reboot" => false - } - end - end - - describe "GET /api/pleroma/admin/config/descriptions" do - test "structure", %{conn: conn} do - admin = insert(:user, is_admin: true) - - conn = - assign(conn, :user, admin) - |> get("/api/pleroma/admin/config/descriptions") - - assert [child | _others] = json_response_and_validate_schema(conn, 200) - - assert child["children"] - assert child["key"] - assert String.starts_with?(child["group"], ":") - assert child["description"] - end - - test "filters by database configuration whitelist", %{conn: conn} do - clear_config(:database_config_whitelist, [ - {:pleroma, :instance}, - {:pleroma, :activitypub}, - {:pleroma, Pleroma.Upload}, - {:esshd} - ]) - - admin = insert(:user, is_admin: true) - - conn = - assign(conn, :user, admin) - |> get("/api/pleroma/admin/config/descriptions") - - children = json_response_and_validate_schema(conn, 200) - - assert length(children) == 4 - - assert Enum.count(children, fn c -> c["group"] == ":pleroma" end) == 3 - - instance = Enum.find(children, fn c -> c["key"] == ":instance" end) - assert instance["children"] - - activitypub = Enum.find(children, fn c -> c["key"] == ":activitypub" end) - assert activitypub["children"] - - 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/web/admin_api/controllers/instance_document_controller_test.exs b/test/web/admin_api/controllers/instance_document_controller_test.exs @@ -1,106 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do - use Pleroma.Web.ConnCase, async: true - import Pleroma.Factory - alias Pleroma.Config - - @dir "test/tmp/instance_static" - @default_instance_panel ~s(<p>Welcome to <a href="https://pleroma.social" target="_blank">Pleroma!</a></p>) - - setup do - File.mkdir_p!(@dir) - on_exit(fn -> File.rm_rf(@dir) end) - end - - setup do: clear_config([:instance, :static_dir], @dir) - - 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/instance_document/:name" do - test "return the instance document url", %{conn: conn} do - conn = get(conn, "/api/pleroma/admin/instance_document/instance-panel") - - assert content = html_response(conn, 200) - assert String.contains?(content, @default_instance_panel) - end - - test "it returns 403 if requested by a non-admin" do - non_admin_user = insert(:user) - token = insert(:oauth_token, user: non_admin_user) - - conn = - build_conn() - |> assign(:user, non_admin_user) - |> assign(:token, token) - |> get("/api/pleroma/admin/instance_document/instance-panel") - - assert json_response(conn, :forbidden) - end - - test "it returns 404 if the instance document with the given name doesn't exist", %{ - conn: conn - } do - conn = get(conn, "/api/pleroma/admin/instance_document/1234") - - assert json_response_and_validate_schema(conn, 404) - end - end - - describe "PATCH /api/pleroma/admin/instance_document/:name" do - test "uploads the instance document", %{conn: conn} do - image = %Plug.Upload{ - content_type: "text/html", - path: Path.absname("test/fixtures/custom_instance_panel.html"), - filename: "custom_instance_panel.html" - } - - conn = - conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/admin/instance_document/instance-panel", %{ - "file" => image - }) - - assert %{"url" => url} = json_response_and_validate_schema(conn, 200) - index = get(build_conn(), url) - assert html_response(index, 200) == "<h2>Custom instance panel</h2>" - end - end - - describe "DELETE /api/pleroma/admin/instance_document/:name" do - test "deletes the instance document", %{conn: conn} do - File.mkdir!(@dir <> "/instance/") - File.write!(@dir <> "/instance/panel.html", "Custom instance panel") - - conn_resp = - conn - |> get("/api/pleroma/admin/instance_document/instance-panel") - - assert html_response(conn_resp, 200) == "Custom instance panel" - - conn - |> delete("/api/pleroma/admin/instance_document/instance-panel") - |> json_response_and_validate_schema(200) - - conn_resp = - conn - |> get("/api/pleroma/admin/instance_document/instance-panel") - - assert content = html_response(conn_resp, 200) - assert String.contains?(content, @default_instance_panel) - end - end -end diff --git a/test/web/admin_api/controllers/invite_controller_test.exs b/test/web/admin_api/controllers/invite_controller_test.exs @@ -1,281 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.InviteControllerTest do - use Pleroma.Web.ConnCase, async: true - - import Pleroma.Factory - - alias Pleroma.Config - alias Pleroma.Repo - alias Pleroma.UserInviteToken - - 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 "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) - - test "sends invitation and returns 204", %{admin: admin, conn: conn} do - recipient_email = "foo@bar.com" - recipient_name = "J. D." - - conn = - conn - |> put_req_header("content-type", "application/json;charset=utf-8") - |> post("/api/pleroma/admin/users/email_invite", %{ - email: recipient_email, - name: recipient_name - }) - - assert json_response_and_validate_schema(conn, :no_content) - - token_record = List.last(Repo.all(Pleroma.UserInviteToken)) - assert token_record - refute token_record.used - - notify_email = Config.get([:instance, :notify_email]) - instance_name = Config.get([:instance, :name]) - - email = - Pleroma.Emails.UserEmail.user_invitation_email( - admin, - token_record, - recipient_email, - recipient_name - ) - - Swoosh.TestAssertions.assert_email_sent( - from: {instance_name, notify_email}, - to: {recipient_name, recipient_email}, - html_body: email.html_body - ) - end - - test "it returns 403 if requested by a non-admin" do - non_admin_user = insert(:user) - token = insert(:oauth_token, user: non_admin_user) - - conn = - build_conn() - |> assign(:user, non_admin_user) - |> assign(:token, token) - |> put_req_header("content-type", "application/json;charset=utf-8") - |> post("/api/pleroma/admin/users/email_invite", %{ - email: "foo@bar.com", - name: "JD" - }) - - assert json_response(conn, :forbidden) - end - - test "email with +", %{conn: conn, admin: admin} do - recipient_email = "foo+bar@baz.com" - - conn - |> put_req_header("content-type", "application/json;charset=utf-8") - |> post("/api/pleroma/admin/users/email_invite", %{email: recipient_email}) - |> json_response_and_validate_schema(:no_content) - - token_record = - Pleroma.UserInviteToken - |> Repo.all() - |> List.last() - - assert token_record - refute token_record.used - - notify_email = Config.get([:instance, :notify_email]) - instance_name = Config.get([:instance, :name]) - - email = - Pleroma.Emails.UserEmail.user_invitation_email( - admin, - token_record, - recipient_email - ) - - Swoosh.TestAssertions.assert_email_sent( - from: {instance_name, notify_email}, - to: recipient_email, - html_body: email.html_body - ) - end - 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]) - - test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn} do - Config.put([:instance, :registrations_open], false) - Config.put([:instance, :invites_enabled], false) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/users/email_invite", %{ - email: "foo@bar.com", - name: "JD" - }) - - assert json_response_and_validate_schema(conn, :bad_request) == - %{ - "error" => - "To send invites you need to set the `invites_enabled` option to true." - } - end - - test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do - Config.put([:instance, :registrations_open], true) - Config.put([:instance, :invites_enabled], true) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/users/email_invite", %{ - email: "foo@bar.com", - name: "JD" - }) - - assert json_response_and_validate_schema(conn, :bad_request) == - %{ - "error" => - "To send invites you need to set the `registrations_open` option to false." - } - end - end - - describe "POST /api/pleroma/admin/users/invite_token" do - test "without options", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/users/invite_token") - - invite_json = json_response_and_validate_schema(conn, 200) - invite = UserInviteToken.find_by_token!(invite_json["token"]) - refute invite.used - refute invite.expires_at - refute invite.max_use - assert invite.invite_type == "one_time" - end - - test "with expires_at", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/users/invite_token", %{ - "expires_at" => Date.to_string(Date.utc_today()) - }) - - invite_json = json_response_and_validate_schema(conn, 200) - invite = UserInviteToken.find_by_token!(invite_json["token"]) - - refute invite.used - assert invite.expires_at == Date.utc_today() - refute invite.max_use - assert invite.invite_type == "date_limited" - end - - test "with max_use", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/users/invite_token", %{"max_use" => 150}) - - invite_json = json_response_and_validate_schema(conn, 200) - invite = UserInviteToken.find_by_token!(invite_json["token"]) - refute invite.used - refute invite.expires_at - assert invite.max_use == 150 - assert invite.invite_type == "reusable" - end - - test "with max use and expires_at", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/users/invite_token", %{ - "max_use" => 150, - "expires_at" => Date.to_string(Date.utc_today()) - }) - - invite_json = json_response_and_validate_schema(conn, 200) - invite = UserInviteToken.find_by_token!(invite_json["token"]) - refute invite.used - assert invite.expires_at == Date.utc_today() - assert invite.max_use == 150 - assert invite.invite_type == "reusable_date_limited" - end - end - - describe "GET /api/pleroma/admin/users/invites" do - test "no invites", %{conn: conn} do - conn = get(conn, "/api/pleroma/admin/users/invites") - - assert json_response_and_validate_schema(conn, 200) == %{"invites" => []} - end - - test "with invite", %{conn: conn} do - {:ok, invite} = UserInviteToken.create_invite() - - conn = get(conn, "/api/pleroma/admin/users/invites") - - assert json_response_and_validate_schema(conn, 200) == %{ - "invites" => [ - %{ - "expires_at" => nil, - "id" => invite.id, - "invite_type" => "one_time", - "max_use" => nil, - "token" => invite.token, - "used" => false, - "uses" => 0 - } - ] - } - end - end - - describe "POST /api/pleroma/admin/users/revoke_invite" do - test "with token", %{conn: conn} do - {:ok, invite} = UserInviteToken.create_invite() - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token}) - - assert json_response_and_validate_schema(conn, 200) == %{ - "expires_at" => nil, - "id" => invite.id, - "invite_type" => "one_time", - "max_use" => nil, - "token" => invite.token, - "used" => true, - "uses" => 0 - } - end - - test "with invalid token", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"}) - - assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} - end - end -end diff --git a/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs b/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs @@ -1,167 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.MediaProxyCacheControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - import Mock - - alias Pleroma.Web.MediaProxy - - setup do: clear_config([:media_proxy]) - - setup do - on_exit(fn -> Cachex.clear(:banned_urls_cache) end) - end - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - Config.put([:media_proxy, :enabled], true) - Config.put([:media_proxy, :invalidation, :enabled], true) - Config.put([:media_proxy, :invalidation, :provider], MediaProxy.Invalidation.Script) - - {:ok, %{admin: admin, token: token, conn: conn}} - end - - describe "GET /api/pleroma/admin/media_proxy_caches" do - test "shows banned MediaProxy URLs", %{conn: conn} do - MediaProxy.put_in_banned_urls([ - "http://localhost:4001/media/a688346.jpg", - "http://localhost:4001/media/fb1f4d.jpg" - ]) - - MediaProxy.put_in_banned_urls("http://localhost:4001/media/gb1f44.jpg") - MediaProxy.put_in_banned_urls("http://localhost:4001/media/tb13f47.jpg") - MediaProxy.put_in_banned_urls("http://localhost:4001/media/wb1f46.jpg") - - response = - conn - |> get("/api/pleroma/admin/media_proxy_caches?page_size=2") - |> json_response_and_validate_schema(200) - - assert response["page_size"] == 2 - assert response["count"] == 5 - - assert response["urls"] == [ - "http://localhost:4001/media/fb1f4d.jpg", - "http://localhost:4001/media/a688346.jpg" - ] - - response = - conn - |> get("/api/pleroma/admin/media_proxy_caches?page_size=2&page=2") - |> json_response_and_validate_schema(200) - - assert response["urls"] == [ - "http://localhost:4001/media/gb1f44.jpg", - "http://localhost:4001/media/tb13f47.jpg" - ] - - assert response["page_size"] == 2 - assert response["count"] == 5 - - response = - conn - |> get("/api/pleroma/admin/media_proxy_caches?page_size=2&page=3") - |> json_response_and_validate_schema(200) - - assert response["urls"] == ["http://localhost:4001/media/wb1f46.jpg"] - end - - test "search banned MediaProxy URLs", %{conn: conn} do - MediaProxy.put_in_banned_urls([ - "http://localhost:4001/media/a688346.jpg", - "http://localhost:4001/media/ff44b1f4d.jpg" - ]) - - MediaProxy.put_in_banned_urls("http://localhost:4001/media/gb1f44.jpg") - MediaProxy.put_in_banned_urls("http://localhost:4001/media/tb13f47.jpg") - MediaProxy.put_in_banned_urls("http://localhost:4001/media/wb1f46.jpg") - - response = - conn - |> get("/api/pleroma/admin/media_proxy_caches?page_size=2&query=F44") - |> json_response_and_validate_schema(200) - - assert response["urls"] == [ - "http://localhost:4001/media/gb1f44.jpg", - "http://localhost:4001/media/ff44b1f4d.jpg" - ] - - assert response["page_size"] == 2 - assert response["count"] == 2 - end - end - - describe "POST /api/pleroma/admin/media_proxy_caches/delete" do - test "deleted MediaProxy URLs from banned", %{conn: conn} do - MediaProxy.put_in_banned_urls([ - "http://localhost:4001/media/a688346.jpg", - "http://localhost:4001/media/fb1f4d.jpg" - ]) - - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/media_proxy_caches/delete", %{ - urls: ["http://localhost:4001/media/a688346.jpg"] - }) - |> json_response_and_validate_schema(200) - - refute MediaProxy.in_banned_urls("http://localhost:4001/media/a688346.jpg") - assert MediaProxy.in_banned_urls("http://localhost:4001/media/fb1f4d.jpg") - end - end - - describe "POST /api/pleroma/admin/media_proxy_caches/purge" do - test "perform invalidates cache of MediaProxy", %{conn: conn} do - urls = [ - "http://example.com/media/a688346.jpg", - "http://example.com/media/fb1f4d.jpg" - ] - - with_mocks [ - {MediaProxy.Invalidation.Script, [], - [ - purge: fn _, _ -> {"ok", 0} end - ]} - ] do - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/media_proxy_caches/purge", %{urls: urls, ban: false}) - |> json_response_and_validate_schema(200) - - refute MediaProxy.in_banned_urls("http://example.com/media/a688346.jpg") - refute MediaProxy.in_banned_urls("http://example.com/media/fb1f4d.jpg") - end - end - - test "perform invalidates cache of MediaProxy and adds url to banned", %{conn: conn} do - urls = [ - "http://example.com/media/a688346.jpg", - "http://example.com/media/fb1f4d.jpg" - ] - - with_mocks [{MediaProxy.Invalidation.Script, [], [purge: fn _, _ -> {"ok", 0} end]}] do - conn - |> put_req_header("content-type", "application/json") - |> post( - "/api/pleroma/admin/media_proxy_caches/purge", - %{urls: urls, ban: true} - ) - |> json_response_and_validate_schema(200) - - assert MediaProxy.in_banned_urls("http://example.com/media/a688346.jpg") - assert MediaProxy.in_banned_urls("http://example.com/media/fb1f4d.jpg") - end - end - end -end diff --git a/test/web/admin_api/controllers/oauth_app_controller_test.exs b/test/web/admin_api/controllers/oauth_app_controller_test.exs @@ -1,220 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.OAuthAppControllerTest do - use Pleroma.Web.ConnCase, async: true - use Oban.Testing, repo: Pleroma.Repo - - import Pleroma.Factory - - alias Pleroma.Config - alias Pleroma.Web - - 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 "POST /api/pleroma/admin/oauth_app" do - test "errors", %{conn: conn} do - response = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/oauth_app", %{}) - |> json_response_and_validate_schema(400) - - assert %{ - "error" => "Missing field: name. Missing field: redirect_uris." - } = response - end - - test "success", %{conn: conn} do - base_url = Web.base_url() - app_name = "Trusted app" - - response = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/oauth_app", %{ - name: app_name, - redirect_uris: base_url - }) - |> json_response_and_validate_schema(200) - - assert %{ - "client_id" => _, - "client_secret" => _, - "name" => ^app_name, - "redirect_uri" => ^base_url, - "trusted" => false - } = response - end - - test "with trusted", %{conn: conn} do - base_url = Web.base_url() - app_name = "Trusted app" - - response = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/oauth_app", %{ - name: app_name, - redirect_uris: base_url, - trusted: true - }) - |> json_response_and_validate_schema(200) - - assert %{ - "client_id" => _, - "client_secret" => _, - "name" => ^app_name, - "redirect_uri" => ^base_url, - "trusted" => true - } = response - end - end - - describe "GET /api/pleroma/admin/oauth_app" do - setup do - app = insert(:oauth_app) - {:ok, app: app} - end - - test "list", %{conn: conn} do - response = - conn - |> get("/api/pleroma/admin/oauth_app") - |> json_response_and_validate_schema(200) - - assert %{"apps" => apps, "count" => count, "page_size" => _} = response - - assert length(apps) == count - end - - test "with page size", %{conn: conn} do - insert(:oauth_app) - page_size = 1 - - response = - conn - |> get("/api/pleroma/admin/oauth_app?page_size=#{page_size}") - |> json_response_and_validate_schema(200) - - assert %{"apps" => apps, "count" => _, "page_size" => ^page_size} = response - - assert length(apps) == page_size - end - - test "search by client name", %{conn: conn, app: app} do - response = - conn - |> get("/api/pleroma/admin/oauth_app?name=#{app.client_name}") - |> json_response_and_validate_schema(200) - - assert %{"apps" => [returned], "count" => _, "page_size" => _} = response - - assert returned["client_id"] == app.client_id - assert returned["name"] == app.client_name - end - - test "search by client id", %{conn: conn, app: app} do - response = - conn - |> get("/api/pleroma/admin/oauth_app?client_id=#{app.client_id}") - |> json_response_and_validate_schema(200) - - assert %{"apps" => [returned], "count" => _, "page_size" => _} = response - - assert returned["client_id"] == app.client_id - assert returned["name"] == app.client_name - end - - test "only trusted", %{conn: conn} do - app = insert(:oauth_app, trusted: true) - - response = - conn - |> get("/api/pleroma/admin/oauth_app?trusted=true") - |> json_response_and_validate_schema(200) - - assert %{"apps" => [returned], "count" => _, "page_size" => _} = response - - assert returned["client_id"] == app.client_id - assert returned["name"] == app.client_name - end - end - - describe "DELETE /api/pleroma/admin/oauth_app/:id" do - test "with id", %{conn: conn} do - app = insert(:oauth_app) - - response = - conn - |> delete("/api/pleroma/admin/oauth_app/" <> to_string(app.id)) - |> json_response_and_validate_schema(:no_content) - - assert response == "" - end - - test "with non existance id", %{conn: conn} do - response = - conn - |> delete("/api/pleroma/admin/oauth_app/0") - |> json_response_and_validate_schema(:bad_request) - - assert response == "" - end - end - - describe "PATCH /api/pleroma/admin/oauth_app/:id" do - test "with id", %{conn: conn} do - app = insert(:oauth_app) - - name = "another name" - url = "https://example.com" - scopes = ["admin"] - id = app.id - website = "http://website.com" - - response = - conn - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/oauth_app/#{id}", %{ - name: name, - trusted: true, - redirect_uris: url, - scopes: scopes, - website: website - }) - |> json_response_and_validate_schema(200) - - assert %{ - "client_id" => _, - "client_secret" => _, - "id" => ^id, - "name" => ^name, - "redirect_uri" => ^url, - "trusted" => true, - "website" => ^website - } = response - end - - test "without id", %{conn: conn} do - response = - conn - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/oauth_app/0") - |> json_response_and_validate_schema(:bad_request) - - assert response == "" - end - end -end diff --git a/test/web/admin_api/controllers/relay_controller_test.exs b/test/web/admin_api/controllers/relay_controller_test.exs @@ -1,99 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.RelayControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - alias Pleroma.Config - alias Pleroma.ModerationLog - alias Pleroma.Repo - alias Pleroma.User - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - - :ok - end - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - {:ok, %{admin: admin, token: token, conn: conn}} - end - - describe "relays" do - test "POST /relay", %{conn: conn, admin: admin} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/relay", %{ - relay_url: "http://mastodon.example.org/users/admin" - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "actor" => "http://mastodon.example.org/users/admin", - "followed_back" => false - } - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" - end - - test "GET /relay", %{conn: conn} do - relay_user = Pleroma.Web.ActivityPub.Relay.get_actor() - - ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"] - |> Enum.each(fn ap_id -> - {:ok, user} = User.get_or_fetch_by_ap_id(ap_id) - User.follow(relay_user, user) - end) - - conn = get(conn, "/api/pleroma/admin/relay") - - assert json_response_and_validate_schema(conn, 200)["relays"] == [ - %{ - "actor" => "http://mastodon.example.org/users/admin", - "followed_back" => true - }, - %{"actor" => "https://mstdn.io/users/mayuutann", "followed_back" => true} - ] - end - - test "DELETE /relay", %{conn: conn, admin: admin} do - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/relay", %{ - relay_url: "http://mastodon.example.org/users/admin" - }) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/pleroma/admin/relay", %{ - relay_url: "http://mastodon.example.org/users/admin" - }) - - assert json_response_and_validate_schema(conn, 200) == - "http://mastodon.example.org/users/admin" - - [log_entry_one, log_entry_two] = Repo.all(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry_one) == - "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" - - assert ModerationLog.get_log_entry_message(log_entry_two) == - "@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin" - end - end -end diff --git a/test/web/admin_api/controllers/report_controller_test.exs b/test/web/admin_api/controllers/report_controller_test.exs @@ -1,372 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.ReportControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - alias Pleroma.Activity - alias Pleroma.Config - alias Pleroma.ModerationLog - alias Pleroma.Repo - alias Pleroma.ReportNote - alias Pleroma.Web.CommonAPI - - 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/reports/:id" do - test "returns report by its id", %{conn: conn} do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %{id: report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - response = - conn - |> get("/api/pleroma/admin/reports/#{report_id}") - |> json_response_and_validate_schema(:ok) - - assert response["id"] == report_id - end - - test "returns 404 when report id is invalid", %{conn: conn} do - conn = get(conn, "/api/pleroma/admin/reports/test") - - assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} - end - end - - describe "PATCH /api/pleroma/admin/reports" do - setup do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %{id: report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - {:ok, %{id: second_report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel very offended", - status_ids: [activity.id] - }) - - %{ - id: report_id, - second_report_id: second_report_id - } - 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"]) - - response = - conn - |> assign(:token, read_token) - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [%{"state" => "resolved", "id" => id}] - }) - |> json_response_and_validate_schema(403) - - assert response == %{ - "error" => "Insufficient permissions: admin:write:reports." - } - - conn - |> assign(:token, write_token) - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [%{"state" => "resolved", "id" => id}] - }) - |> json_response_and_validate_schema(:no_content) - end - - test "mark report as resolved", %{conn: conn, id: id, admin: admin} do - 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(id) - assert activity.data["state"] == "resolved" - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} updated report ##{id} with 'resolved' state" - end - - test "closes report", %{conn: conn, id: id, admin: admin} do - conn - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [ - %{"state" => "closed", "id" => id} - ] - }) - |> json_response_and_validate_schema(:no_content) - - activity = Activity.get_by_id(id) - assert activity.data["state"] == "closed" - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} updated report ##{id} with 'closed' state" - end - - test "returns 400 when state is unknown", %{conn: conn, id: id} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [ - %{"state" => "test", "id" => id} - ] - }) - - assert "Unsupported state" = - hd(json_response_and_validate_schema(conn, :bad_request))["error"] - end - - test "returns 404 when report is not exist", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [ - %{"state" => "closed", "id" => "test"} - ] - }) - - assert hd(json_response_and_validate_schema(conn, :bad_request))["error"] == "not_found" - end - - test "updates state of multiple reports", %{ - conn: conn, - id: id, - admin: admin, - second_report_id: second_report_id - } do - conn - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [ - %{"state" => "resolved", "id" => id}, - %{"state" => "closed", "id" => second_report_id} - ] - }) - |> json_response_and_validate_schema(:no_content) - - activity = Activity.get_by_id(id) - second_activity = Activity.get_by_id(second_report_id) - assert activity.data["state"] == "resolved" - assert second_activity.data["state"] == "closed" - - [first_log_entry, second_log_entry] = Repo.all(ModerationLog) - - assert ModerationLog.get_log_entry_message(first_log_entry) == - "@#{admin.nickname} updated report ##{id} with 'resolved' state" - - assert ModerationLog.get_log_entry_message(second_log_entry) == - "@#{admin.nickname} updated report ##{second_report_id} with 'closed' state" - end - end - - describe "GET /api/pleroma/admin/reports" do - test "returns empty response when no reports created", %{conn: conn} do - response = - conn - |> get(report_path(conn, :index)) - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response["reports"]) - assert response["total"] == 0 - end - - test "returns reports", %{conn: conn} do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %{id: report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - response = - conn - |> get(report_path(conn, :index)) - |> json_response_and_validate_schema(:ok) - - [report] = response["reports"] - - assert length(response["reports"]) == 1 - assert report["id"] == report_id - - assert response["total"] == 1 - end - - test "returns reports with specified state", %{conn: conn} do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %{id: first_report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - {:ok, %{id: second_report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I don't like this user" - }) - - CommonAPI.update_report_state(second_report_id, "closed") - - response = - conn - |> get(report_path(conn, :index, %{state: "open"})) - |> json_response_and_validate_schema(:ok) - - assert [open_report] = response["reports"] - - assert length(response["reports"]) == 1 - assert open_report["id"] == first_report_id - - assert response["total"] == 1 - - response = - conn - |> get(report_path(conn, :index, %{state: "closed"})) - |> json_response_and_validate_schema(:ok) - - assert [closed_report] = response["reports"] - - assert length(response["reports"]) == 1 - assert closed_report["id"] == second_report_id - - assert response["total"] == 1 - - assert %{"total" => 0, "reports" => []} == - conn - |> get(report_path(conn, :index, %{state: "resolved"})) - |> json_response_and_validate_schema(:ok) - end - - test "returns 403 when requested by a non-admin" do - user = insert(:user) - token = insert(:oauth_token, user: user) - - conn = - build_conn() - |> assign(:user, user) - |> assign(:token, token) - |> get("/api/pleroma/admin/reports") - - assert json_response(conn, :forbidden) == - %{"error" => "User is not an admin."} - end - - test "returns 403 when requested by anonymous" do - conn = get(build_conn(), "/api/pleroma/admin/reports") - - assert json_response(conn, :forbidden) == %{ - "error" => "Invalid credentials." - } - end - end - - describe "POST /api/pleroma/admin/reports/:id/notes" do - setup %{conn: conn, admin: admin} do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %{id: report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ - content: "this is disgusting!" - }) - - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ - content: "this is disgusting2!" - }) - - %{ - admin_id: admin.id, - report_id: report_id - } - end - - test "it creates report note", %{admin_id: admin_id, report_id: report_id} do - assert [note, _] = Repo.all(ReportNote) - - assert %{ - activity_id: ^report_id, - content: "this is disgusting!", - user_id: ^admin_id - } = note - end - - test "it returns reports with notes", %{conn: conn, admin: admin} do - conn = get(conn, "/api/pleroma/admin/reports") - - response = json_response_and_validate_schema(conn, 200) - notes = hd(response["reports"])["notes"] - [note, _] = notes - - assert note["user"]["nickname"] == admin.nickname - assert note["content"] == "this is disgusting!" - assert note["created_at"] - assert response["total"] == 1 - end - - test "it deletes the note", %{conn: conn, report_id: report_id} do - assert ReportNote |> Repo.all() |> length() == 2 - assert [note, _] = Repo.all(ReportNote) - - delete(conn, "/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}") - - assert ReportNote |> Repo.all() |> length() == 1 - end - end -end diff --git a/test/web/admin_api/controllers/status_controller_test.exs b/test/web/admin_api/controllers/status_controller_test.exs @@ -1,202 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.StatusControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - alias Pleroma.Activity - alias Pleroma.Config - alias Pleroma.ModerationLog - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - 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/statuses/:id" do - test "not found", %{conn: conn} do - assert conn - |> get("/api/pleroma/admin/statuses/not_found") - |> json_response_and_validate_schema(:not_found) - end - - test "shows activity", %{conn: conn} do - activity = insert(:note_activity) - - response = - conn - |> get("/api/pleroma/admin/statuses/#{activity.id}") - |> json_response_and_validate_schema(200) - - assert response["id"] == activity.id - - account = response["account"] - actor = User.get_by_ap_id(activity.actor) - - assert account["id"] == actor.id - assert account["nickname"] == actor.nickname - assert account["deactivated"] == actor.deactivated - assert account["confirmation_pending"] == actor.confirmation_pending - end - end - - describe "PUT /api/pleroma/admin/statuses/:id" do - setup do - activity = insert(:note_activity) - - %{id: activity.id} - end - - test "toggle sensitive flag", %{conn: conn, id: id, admin: admin} do - response = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"}) - |> json_response_and_validate_schema(:ok) - - assert response["sensitive"] - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} updated status ##{id}, set sensitive: 'true'" - - response = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"}) - |> json_response_and_validate_schema(:ok) - - refute response["sensitive"] - end - - test "change visibility flag", %{conn: conn, id: id, admin: admin} do - response = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "public"}) - |> json_response_and_validate_schema(:ok) - - assert response["visibility"] == "public" - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} updated status ##{id}, set visibility: 'public'" - - response = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "private"}) - |> json_response_and_validate_schema(:ok) - - assert response["visibility"] == "private" - - response = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "unlisted"}) - |> json_response_and_validate_schema(:ok) - - assert response["visibility"] == "unlisted" - end - - test "returns 400 when visibility is unknown", %{conn: conn, id: id} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "test"}) - - assert %{"error" => "test - Invalid value for enum."} = - json_response_and_validate_schema(conn, :bad_request) - end - end - - describe "DELETE /api/pleroma/admin/statuses/:id" do - setup do - activity = insert(:note_activity) - - %{id: activity.id} - end - - test "deletes status", %{conn: conn, id: id, admin: admin} do - conn - |> delete("/api/pleroma/admin/statuses/#{id}") - |> json_response_and_validate_schema(:ok) - - refute Activity.get_by_id(id) - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deleted status ##{id}" - end - - test "returns 404 when the status does not exist", %{conn: conn} do - conn = delete(conn, "/api/pleroma/admin/statuses/test") - - assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} - end - end - - describe "GET /api/pleroma/admin/statuses" do - test "returns all public and unlisted statuses", %{conn: conn, admin: admin} do - blocked = insert(:user) - user = insert(:user) - User.block(admin, blocked) - - {:ok, _} = CommonAPI.post(user, %{status: "@#{admin.nickname}", visibility: "direct"}) - - {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) - {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "private"}) - {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "public"}) - {:ok, _} = CommonAPI.post(blocked, %{status: ".", visibility: "public"}) - - response = - conn - |> get("/api/pleroma/admin/statuses") - |> json_response_and_validate_schema(200) - - refute "private" in Enum.map(response, & &1["visibility"]) - assert length(response) == 3 - end - - test "returns only local statuses with local_only on", %{conn: conn} do - user = insert(:user) - remote_user = insert(:user, local: false, nickname: "archaeme@archae.me") - insert(:note_activity, user: user, local: true) - insert(:note_activity, user: remote_user, local: false) - - response = - conn - |> get("/api/pleroma/admin/statuses?local_only=true") - |> json_response_and_validate_schema(200) - - assert length(response) == 1 - end - - test "returns private and direct statuses with godmode on", %{conn: conn, admin: admin} do - user = insert(:user) - - {:ok, _} = CommonAPI.post(user, %{status: "@#{admin.nickname}", visibility: "direct"}) - - {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "private"}) - {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "public"}) - conn = get(conn, "/api/pleroma/admin/statuses?godmode=true") - assert json_response_and_validate_schema(conn, 200) |> length() == 3 - end - end -end diff --git a/test/web/admin_api/search_test.exs b/test/web/admin_api/search_test.exs @@ -1,190 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.SearchTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Web.AdminAPI.Search - - import Pleroma.Factory - - describe "search for admin" do - test "it ignores case" do - insert(:user, nickname: "papercoach") - insert(:user, nickname: "CanadaPaperCoach") - - {:ok, _results, count} = - Search.user(%{ - query: "paper", - local: false, - page: 1, - page_size: 50 - }) - - assert count == 2 - end - - test "it returns local/external users" do - insert(:user, local: true) - insert(:user, local: false) - insert(:user, local: false) - - {:ok, _results, local_count} = - Search.user(%{ - query: "", - local: true - }) - - {:ok, _results, external_count} = - Search.user(%{ - query: "", - external: true - }) - - assert local_count == 1 - assert external_count == 2 - end - - test "it returns active/deactivated users" do - insert(:user, deactivated: true) - insert(:user, deactivated: true) - insert(:user, deactivated: false) - - {:ok, _results, active_count} = - Search.user(%{ - query: "", - active: true - }) - - {:ok, _results, deactivated_count} = - Search.user(%{ - query: "", - deactivated: true - }) - - assert active_count == 1 - assert deactivated_count == 2 - end - - test "it returns specific user" do - insert(:user) - insert(:user) - user = insert(:user, nickname: "bob", local: true, deactivated: false) - - {:ok, _results, total_count} = Search.user(%{query: ""}) - - {:ok, [^user], count} = - Search.user(%{ - query: "Bo", - active: true, - local: true - }) - - assert total_count == 3 - assert count == 1 - end - - test "it returns user by domain" do - insert(:user) - insert(:user) - user = insert(:user, nickname: "some@domain.com") - - {:ok, _results, total} = Search.user() - {:ok, [^user], count} = Search.user(%{query: "domain.com"}) - assert total == 3 - assert count == 1 - end - - test "it return user by full nickname" do - insert(:user) - insert(:user) - user = insert(:user, nickname: "some@domain.com") - - {:ok, _results, total} = Search.user() - {:ok, [^user], count} = Search.user(%{query: "some@domain.com"}) - assert total == 3 - assert count == 1 - end - - test "it returns admin user" do - admin = insert(:user, is_admin: true) - insert(:user) - insert(:user) - - {:ok, _results, total} = Search.user() - {:ok, [^admin], count} = Search.user(%{is_admin: true}) - assert total == 3 - assert count == 1 - end - - test "it returns moderator user" do - moderator = insert(:user, is_moderator: true) - insert(:user) - insert(:user) - - {:ok, _results, total} = Search.user() - {:ok, [^moderator], count} = Search.user(%{is_moderator: true}) - assert total == 3 - assert count == 1 - end - - test "it returns users with tags" do - user1 = insert(:user, tags: ["first"]) - user2 = insert(:user, tags: ["second"]) - insert(:user) - insert(:user) - - {:ok, _results, total} = Search.user() - {:ok, users, count} = Search.user(%{tags: ["first", "second"]}) - assert total == 4 - assert count == 2 - assert user1 in users - assert user2 in users - end - - test "it returns user by display name" do - user = insert(:user, name: "Display name") - insert(:user) - insert(:user) - - {:ok, _results, total} = Search.user() - {:ok, [^user], count} = Search.user(%{name: "display"}) - - assert total == 3 - assert count == 1 - end - - test "it returns user by email" do - user = insert(:user, email: "some@example.com") - insert(:user) - insert(:user) - - {:ok, _results, total} = Search.user() - {:ok, [^user], count} = Search.user(%{email: "some@example.com"}) - - assert total == 3 - assert count == 1 - end - - test "it returns unapproved user" do - unapproved = insert(:user, approval_pending: true) - insert(:user) - insert(:user) - - {:ok, _results, total} = Search.user() - {:ok, [^unapproved], count} = Search.user(%{need_approval: true}) - assert total == 3 - assert count == 1 - end - - test "it returns non-discoverable users" do - insert(:user) - insert(:user, discoverable: false) - - {:ok, _results, total} = Search.user() - - assert total == 2 - end - end -end diff --git a/test/web/admin_api/views/report_view_test.exs b/test/web/admin_api/views/report_view_test.exs @@ -1,146 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.ReportViewTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Web.AdminAPI - alias Pleroma.Web.AdminAPI.Report - alias Pleroma.Web.AdminAPI.ReportView - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI - alias Pleroma.Web.MastodonAPI.StatusView - - test "renders a report" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.report(user, %{account_id: other_user.id}) - - expected = %{ - content: nil, - actor: - Map.merge( - MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true}), - AdminAPI.AccountView.render("show.json", %{user: user}) - ), - account: - Map.merge( - MastodonAPI.AccountView.render("show.json", %{ - user: other_user, - skip_visibility_check: true - }), - AdminAPI.AccountView.render("show.json", %{user: other_user}) - ), - statuses: [], - notes: [], - state: "open", - id: activity.id - } - - result = - ReportView.render("show.json", Report.extract_report_info(activity)) - |> Map.delete(:created_at) - - assert result == expected - end - - test "includes reported statuses" do - user = insert(:user) - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(other_user, %{status: "toot"}) - - {:ok, report_activity} = - CommonAPI.report(user, %{account_id: other_user.id, status_ids: [activity.id]}) - - other_user = Pleroma.User.get_by_id(other_user.id) - - expected = %{ - content: nil, - actor: - Map.merge( - MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true}), - AdminAPI.AccountView.render("show.json", %{user: user}) - ), - account: - Map.merge( - MastodonAPI.AccountView.render("show.json", %{ - user: other_user, - skip_visibility_check: true - }), - AdminAPI.AccountView.render("show.json", %{user: other_user}) - ), - statuses: [StatusView.render("show.json", %{activity: activity})], - state: "open", - notes: [], - id: report_activity.id - } - - result = - ReportView.render("show.json", Report.extract_report_info(report_activity)) - |> Map.delete(:created_at) - - assert result == expected - end - - test "renders report's state" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.report(user, %{account_id: other_user.id}) - {:ok, activity} = CommonAPI.update_report_state(activity.id, "closed") - - assert %{state: "closed"} = - ReportView.render("show.json", Report.extract_report_info(activity)) - end - - test "renders report description" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.report(user, %{ - account_id: other_user.id, - comment: "posts are too good for this instance" - }) - - assert %{content: "posts are too good for this instance"} = - ReportView.render("show.json", Report.extract_report_info(activity)) - end - - test "sanitizes report description" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.report(user, %{ - account_id: other_user.id, - comment: "" - }) - - data = Map.put(activity.data, "content", "<script> alert('hecked :D:D:D:D:D:D:D') </script>") - activity = Map.put(activity, :data, data) - - refute "<script> alert('hecked :D:D:D:D:D:D:D') </script>" == - ReportView.render("show.json", Report.extract_report_info(activity))[:content] - end - - test "doesn't error out when the user doesn't exists" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.report(user, %{ - account_id: other_user.id, - comment: "" - }) - - Pleroma.User.delete(other_user) - Pleroma.User.invalidate_cache(other_user) - - assert %{} = ReportView.render("show.json", Report.extract_report_info(activity)) - end -end diff --git a/test/web/api_spec/schema_examples_test.exs b/test/web/api_spec/schema_examples_test.exs @@ -1,43 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ApiSpec.SchemaExamplesTest do - use ExUnit.Case, async: true - import Pleroma.Tests.ApiSpecHelpers - - @content_type "application/json" - - for operation <- api_operations() do - describe operation.operationId <> " Request Body" do - if operation.requestBody do - @media_type operation.requestBody.content[@content_type] - @schema resolve_schema(@media_type.schema) - - if @media_type.example do - test "request body media type example matches schema" do - assert_schema(@media_type.example, @schema) - end - end - - if @schema.example do - test "request body schema example matches schema" do - assert_schema(@schema.example, @schema) - end - end - end - end - - for {status, response} <- operation.responses, is_map(response.content[@content_type]) do - describe "#{operation.operationId} - #{status} Response" do - @schema resolve_schema(response.content[@content_type].schema) - - if @schema.example do - test "example matches schema" do - assert_schema(@schema.example, @schema) - end - end - end - end - end -end diff --git a/test/web/auth/auth_test_controller_test.exs b/test/web/auth/auth_test_controller_test.exs @@ -1,242 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Tests.AuthTestControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - describe "do_oauth_check" do - test "serves with proper OAuth token (fulfilling requested scopes)" do - %{conn: good_token_conn, user: user} = oauth_access(["read"]) - - assert %{"user_id" => user.id} == - good_token_conn - |> get("/test/authenticated_api/do_oauth_check") - |> json_response(200) - - # Unintended usage (:api) — use with :authenticated_api instead - assert %{"user_id" => user.id} == - good_token_conn - |> get("/test/api/do_oauth_check") - |> json_response(200) - end - - test "fails on no token / missing scope(s)" do - %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"]) - - bad_token_conn - |> get("/test/authenticated_api/do_oauth_check") - |> json_response(403) - - bad_token_conn - |> assign(:token, nil) - |> get("/test/api/do_oauth_check") - |> json_response(403) - end - end - - describe "fallback_oauth_check" do - test "serves with proper OAuth token (fulfilling requested scopes)" do - %{conn: good_token_conn, user: user} = oauth_access(["read"]) - - assert %{"user_id" => user.id} == - good_token_conn - |> get("/test/api/fallback_oauth_check") - |> json_response(200) - - # Unintended usage (:authenticated_api) — use with :api instead - assert %{"user_id" => user.id} == - good_token_conn - |> get("/test/authenticated_api/fallback_oauth_check") - |> json_response(200) - end - - test "for :api on public instance, drops :user and renders on no token / missing scope(s)" do - clear_config([:instance, :public], true) - - %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"]) - - assert %{"user_id" => nil} == - bad_token_conn - |> get("/test/api/fallback_oauth_check") - |> json_response(200) - - assert %{"user_id" => nil} == - bad_token_conn - |> assign(:token, nil) - |> get("/test/api/fallback_oauth_check") - |> json_response(200) - end - - test "for :api on private instance, fails on no token / missing scope(s)" do - clear_config([:instance, :public], false) - - %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"]) - - bad_token_conn - |> get("/test/api/fallback_oauth_check") - |> json_response(403) - - bad_token_conn - |> assign(:token, nil) - |> get("/test/api/fallback_oauth_check") - |> json_response(403) - end - end - - describe "skip_oauth_check" do - test "for :authenticated_api, serves if :user is set (regardless of token / token scopes)" do - user = insert(:user) - - assert %{"user_id" => user.id} == - build_conn() - |> assign(:user, user) - |> get("/test/authenticated_api/skip_oauth_check") - |> json_response(200) - - %{conn: bad_token_conn, user: user} = oauth_access(["irrelevant_scope"]) - - assert %{"user_id" => user.id} == - bad_token_conn - |> get("/test/authenticated_api/skip_oauth_check") - |> json_response(200) - end - - test "serves via :api on public instance if :user is not set" do - clear_config([:instance, :public], true) - - assert %{"user_id" => nil} == - build_conn() - |> get("/test/api/skip_oauth_check") - |> json_response(200) - - build_conn() - |> get("/test/authenticated_api/skip_oauth_check") - |> json_response(403) - end - - test "fails on private instance if :user is not set" do - clear_config([:instance, :public], false) - - build_conn() - |> get("/test/api/skip_oauth_check") - |> json_response(403) - - build_conn() - |> get("/test/authenticated_api/skip_oauth_check") - |> json_response(403) - end - end - - describe "fallback_oauth_skip_publicity_check" do - test "serves with proper OAuth token (fulfilling requested scopes)" do - %{conn: good_token_conn, user: user} = oauth_access(["read"]) - - assert %{"user_id" => user.id} == - good_token_conn - |> get("/test/api/fallback_oauth_skip_publicity_check") - |> json_response(200) - - # Unintended usage (:authenticated_api) - assert %{"user_id" => user.id} == - good_token_conn - |> get("/test/authenticated_api/fallback_oauth_skip_publicity_check") - |> json_response(200) - end - - test "for :api on private / public instance, drops :user and renders on token issue" do - %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"]) - - for is_public <- [true, false] do - clear_config([:instance, :public], is_public) - - assert %{"user_id" => nil} == - bad_token_conn - |> get("/test/api/fallback_oauth_skip_publicity_check") - |> json_response(200) - - assert %{"user_id" => nil} == - bad_token_conn - |> assign(:token, nil) - |> get("/test/api/fallback_oauth_skip_publicity_check") - |> json_response(200) - end - end - end - - describe "skip_oauth_skip_publicity_check" do - test "for :authenticated_api, serves if :user is set (regardless of token / token scopes)" do - user = insert(:user) - - assert %{"user_id" => user.id} == - build_conn() - |> assign(:user, user) - |> get("/test/authenticated_api/skip_oauth_skip_publicity_check") - |> json_response(200) - - %{conn: bad_token_conn, user: user} = oauth_access(["irrelevant_scope"]) - - assert %{"user_id" => user.id} == - bad_token_conn - |> get("/test/authenticated_api/skip_oauth_skip_publicity_check") - |> json_response(200) - end - - test "for :api, serves on private and public instances regardless of whether :user is set" do - user = insert(:user) - - for is_public <- [true, false] do - clear_config([:instance, :public], is_public) - - assert %{"user_id" => nil} == - build_conn() - |> get("/test/api/skip_oauth_skip_publicity_check") - |> json_response(200) - - assert %{"user_id" => user.id} == - build_conn() - |> assign(:user, user) - |> get("/test/api/skip_oauth_skip_publicity_check") - |> json_response(200) - end - end - end - - describe "missing_oauth_check_definition" do - def test_missing_oauth_check_definition_failure(endpoint, expected_error) do - %{conn: conn} = oauth_access(["read", "write", "follow", "push", "admin"]) - - assert %{"error" => expected_error} == - conn - |> get(endpoint) - |> json_response(403) - end - - test "fails if served via :authenticated_api" do - test_missing_oauth_check_definition_failure( - "/test/authenticated_api/missing_oauth_check_definition", - "Security violation: OAuth scopes check was neither handled nor explicitly skipped." - ) - end - - test "fails if served via :api and the instance is private" do - clear_config([:instance, :public], false) - - test_missing_oauth_check_definition_failure( - "/test/api/missing_oauth_check_definition", - "This resource requires authentication." - ) - end - - test "succeeds with dropped :user if served via :api on public instance" do - %{conn: conn} = oauth_access(["read", "write", "follow", "push", "admin"]) - - assert %{"user_id" => nil} == - conn - |> get("/test/api/missing_oauth_check_definition") - |> json_response(200) - end - end -end diff --git a/test/web/auth/authenticator_test.exs b/test/web/auth/authenticator_test.exs @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Auth.AuthenticatorTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Web.Auth.Authenticator - import Pleroma.Factory - - describe "fetch_user/1" do - test "returns user by name" do - user = insert(:user) - assert Authenticator.fetch_user(user.nickname) == user - end - - test "returns user by email" do - user = insert(:user) - assert Authenticator.fetch_user(user.email) == user - end - - test "returns nil" do - assert Authenticator.fetch_user("email") == nil - end - end - - describe "fetch_credentials/1" do - test "returns name and password from authorization params" do - params = %{"authorization" => %{"name" => "test", "password" => "test-pass"}} - assert Authenticator.fetch_credentials(params) == {:ok, {"test", "test-pass"}} - end - - test "returns name and password with grant_type 'password'" do - params = %{"grant_type" => "password", "username" => "test", "password" => "test-pass"} - assert Authenticator.fetch_credentials(params) == {:ok, {"test", "test-pass"}} - end - - test "returns error" do - assert Authenticator.fetch_credentials(%{}) == {:error, :invalid_credentials} - end - end -end diff --git a/test/web/auth/basic_auth_test.exs b/test/web/auth/basic_auth_test.exs @@ -1,46 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Auth.BasicAuthTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - test "with HTTP Basic Auth used, grants access to OAuth scope-restricted endpoints", %{ - conn: conn - } do - user = insert(:user) - assert Pbkdf2.verify_pass("test", user.password_hash) - - basic_auth_contents = - (URI.encode_www_form(user.nickname) <> ":" <> URI.encode_www_form("test")) - |> Base.encode64() - - # Succeeds with HTTP Basic Auth - response = - conn - |> put_req_header("authorization", "Basic " <> basic_auth_contents) - |> get("/api/v1/accounts/verify_credentials") - |> json_response(200) - - user_nickname = user.nickname - assert %{"username" => ^user_nickname} = response - - # Succeeds with a properly scoped OAuth token - valid_token = insert(:oauth_token, scopes: ["read:accounts"]) - - conn - |> put_req_header("authorization", "Bearer #{valid_token.token}") - |> get("/api/v1/accounts/verify_credentials") - |> json_response(200) - - # Fails with a wrong-scoped OAuth token (proof of restriction) - invalid_token = insert(:oauth_token, scopes: ["read:something"]) - - conn - |> put_req_header("authorization", "Bearer #{invalid_token.token}") - |> get("/api/v1/accounts/verify_credentials") - |> json_response(403) - end -end diff --git a/test/web/auth/pleroma_authenticator_test.exs b/test/web/auth/pleroma_authenticator_test.exs @@ -1,48 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Auth.PleromaAuthenticatorTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Web.Auth.PleromaAuthenticator - import Pleroma.Factory - - setup do - password = "testpassword" - name = "AgentSmith" - user = insert(:user, nickname: name, password_hash: Pbkdf2.hash_pwd_salt(password)) - {:ok, [user: user, name: name, password: password]} - end - - test "get_user/authorization", %{name: name, password: password} do - name = name <> "1" - user = insert(:user, nickname: name, password_hash: Bcrypt.hash_pwd_salt(password)) - - params = %{"authorization" => %{"name" => name, "password" => password}} - res = PleromaAuthenticator.get_user(%Plug.Conn{params: params}) - - assert {:ok, returned_user} = res - assert returned_user.id == user.id - assert "$pbkdf2" <> _ = returned_user.password_hash - end - - test "get_user/authorization with invalid password", %{name: name} do - params = %{"authorization" => %{"name" => name, "password" => "password"}} - res = PleromaAuthenticator.get_user(%Plug.Conn{params: params}) - - assert {:error, {:checkpw, false}} == res - end - - test "get_user/grant_type_password", %{user: user, name: name, password: password} do - params = %{"grant_type" => "password", "username" => name, "password" => password} - res = PleromaAuthenticator.get_user(%Plug.Conn{params: params}) - - assert {:ok, user} == res - end - - test "error credintails" do - res = PleromaAuthenticator.get_user(%Plug.Conn{params: %{}}) - assert {:error, :invalid_credentials} == res - end -end diff --git a/test/web/auth/totp_authenticator_test.exs b/test/web/auth/totp_authenticator_test.exs @@ -1,51 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Auth.TOTPAuthenticatorTest do - use Pleroma.Web.ConnCase - - alias Pleroma.MFA - alias Pleroma.MFA.BackupCodes - alias Pleroma.MFA.TOTP - alias Pleroma.Web.Auth.TOTPAuthenticator - - import Pleroma.Factory - - test "verify token" do - otp_secret = TOTP.generate_secret() - otp_token = TOTP.generate_token(otp_secret) - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - assert TOTPAuthenticator.verify(otp_token, user) == {:ok, :pass} - assert TOTPAuthenticator.verify(nil, user) == {:error, :invalid_token} - assert TOTPAuthenticator.verify("", user) == {:error, :invalid_token} - end - - test "checks backup codes" do - [code | _] = backup_codes = BackupCodes.generate() - - hashed_codes = - backup_codes - |> Enum.map(&Pbkdf2.hash_pwd_salt(&1)) - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - backup_codes: hashed_codes, - totp: %MFA.Settings.TOTP{secret: "otp_secret", confirmed: true} - } - ) - - assert TOTPAuthenticator.verify_recovery_code(user, code) == {:ok, :pass} - refute TOTPAuthenticator.verify_recovery_code(code, refresh_record(user)) == {:ok, :pass} - end -end diff --git a/test/web/chat_channel_test.exs b/test/web/chat_channel_test.exs @@ -1,41 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ChatChannelTest do - use Pleroma.Web.ChannelCase - alias Pleroma.Web.ChatChannel - alias Pleroma.Web.UserSocket - - import Pleroma.Factory - - setup do - user = insert(:user) - - {:ok, _, socket} = - socket(UserSocket, "", %{user_name: user.nickname}) - |> subscribe_and_join(ChatChannel, "chat:public") - - {:ok, socket: socket} - end - - test "it broadcasts a message", %{socket: socket} do - push(socket, "new_msg", %{"text" => "why is tenshi eating a corndog so cute?"}) - assert_broadcast("new_msg", %{text: "why is tenshi eating a corndog so cute?"}) - end - - describe "message lengths" do - setup do: clear_config([:instance, :chat_limit]) - - test "it ignores messages of length zero", %{socket: socket} do - push(socket, "new_msg", %{"text" => ""}) - refute_broadcast("new_msg", %{text: ""}) - end - - test "it ignores messages above a certain length", %{socket: socket} do - Pleroma.Config.put([:instance, :chat_limit], 2) - push(socket, "new_msg", %{"text" => "123"}) - refute_broadcast("new_msg", %{text: "123"}) - end - end -end diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs @@ -1,1244 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.CommonAPITest do - use Pleroma.DataCase - use Oban.Testing, repo: Pleroma.Repo - - alias Pleroma.Activity - alias Pleroma.Chat - alias Pleroma.Conversation.Participation - alias Pleroma.Notification - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.ActivityPub.Visibility - alias Pleroma.Web.AdminAPI.AccountView - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - import Mock - import Ecto.Query, only: [from: 2] - - require Pleroma.Constants - - setup do: clear_config([:instance, :safe_dm_mentions]) - setup do: clear_config([:instance, :limit]) - setup do: clear_config([:instance, :max_pinned_statuses]) - - describe "posting polls" do - test "it posts a poll" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "who is the best", - poll: %{expires_in: 600, options: ["reimu", "marisa"]} - }) - - object = Object.normalize(activity) - - assert object.data["type"] == "Question" - assert object.data["oneOf"] |> length() == 2 - end - end - - describe "blocking" do - setup do - blocker = insert(:user) - blocked = insert(:user) - User.follow(blocker, blocked) - User.follow(blocked, blocker) - %{blocker: blocker, blocked: blocked} - end - - test "it blocks and federates", %{blocker: blocker, blocked: blocked} do - clear_config([:instance, :federating], true) - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - assert {:ok, block} = CommonAPI.block(blocker, blocked) - - assert block.local - assert User.blocks?(blocker, blocked) - refute User.following?(blocker, blocked) - refute User.following?(blocked, blocker) - - assert called(Pleroma.Web.Federator.publish(block)) - end - end - - test "it blocks and does not federate if outgoing blocks are disabled", %{ - blocker: blocker, - blocked: blocked - } do - clear_config([:instance, :federating], true) - clear_config([:activitypub, :outgoing_blocks], false) - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - assert {:ok, block} = CommonAPI.block(blocker, blocked) - - assert block.local - assert User.blocks?(blocker, blocked) - refute User.following?(blocker, blocked) - refute User.following?(blocked, blocker) - - refute called(Pleroma.Web.Federator.publish(block)) - end - end - end - - describe "posting chat messages" do - setup do: clear_config([:instance, :chat_limit]) - - test "it posts a chat message without content but with an attachment" do - author = insert(:user) - recipient = insert(:user) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, upload} = ActivityPub.upload(file, actor: author.ap_id) - - with_mocks([ - { - Pleroma.Web.Streamer, - [], - [ - stream: fn _, _ -> - nil - end - ] - }, - { - Pleroma.Web.Push, - [], - [ - send: fn _ -> nil end - ] - } - ]) do - {:ok, activity} = - CommonAPI.post_chat_message( - author, - recipient, - nil, - media_id: upload.id - ) - - notification = - Notification.for_user_and_activity(recipient, activity) - |> Repo.preload(:activity) - - assert called(Pleroma.Web.Push.send(notification)) - assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) - assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) - - assert activity - end - end - - test "it adds html newlines" do - author = insert(:user) - recipient = insert(:user) - - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post_chat_message( - author, - recipient, - "uguu\nuguuu" - ) - - assert other_user.ap_id not in activity.recipients - - object = Object.normalize(activity, false) - - assert object.data["content"] == "uguu<br/>uguuu" - end - - test "it linkifies" do - author = insert(:user) - recipient = insert(:user) - - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post_chat_message( - author, - recipient, - "https://example.org is the site of @#{other_user.nickname} #2hu" - ) - - assert other_user.ap_id not in activity.recipients - - object = Object.normalize(activity, false) - - assert object.data["content"] == - "<a href=\"https://example.org\" rel=\"ugc\">https://example.org</a> is the site of <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{ - other_user.id - }\" href=\"#{other_user.ap_id}\" rel=\"ugc\">@<span>#{other_user.nickname}</span></a></span> <a class=\"hashtag\" data-tag=\"2hu\" href=\"http://localhost:4001/tag/2hu\">#2hu</a>" - end - - test "it posts a chat message" do - author = insert(:user) - recipient = insert(:user) - - {:ok, activity} = - CommonAPI.post_chat_message( - author, - recipient, - "a test message <script>alert('uuu')</script> :firefox:" - ) - - assert activity.data["type"] == "Create" - assert activity.local - object = Object.normalize(activity) - - assert object.data["type"] == "ChatMessage" - assert object.data["to"] == [recipient.ap_id] - - assert object.data["content"] == - "a test message &lt;script&gt;alert(&#39;uuu&#39;)&lt;/script&gt; :firefox:" - - assert object.data["emoji"] == %{ - "firefox" => "http://localhost:4001/emoji/Firefox.gif" - } - - assert Chat.get(author.id, recipient.ap_id) - assert Chat.get(recipient.id, author.ap_id) - - assert :ok == Pleroma.Web.Federator.perform(:publish, activity) - end - - test "it reject messages over the local limit" do - Pleroma.Config.put([:instance, :chat_limit], 2) - - author = insert(:user) - recipient = insert(:user) - - {:error, message} = - CommonAPI.post_chat_message( - author, - recipient, - "123" - ) - - assert message == :content_too_long - end - - test "it reject messages via MRF" do - clear_config([:mrf_keyword, :reject], ["GNO"]) - clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) - - author = insert(:user) - recipient = insert(:user) - - assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} == - CommonAPI.post_chat_message(author, recipient, "GNO/Linux") - end - end - - describe "unblocking" do - test "it works even without an existing block activity" do - blocked = insert(:user) - blocker = insert(:user) - User.block(blocker, blocked) - - assert User.blocks?(blocker, blocked) - assert {:ok, :no_activity} == CommonAPI.unblock(blocker, blocked) - refute User.blocks?(blocker, blocked) - end - end - - describe "deletion" do - test "it works with pruned objects" do - user = insert(:user) - - {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) - - clear_config([:instance, :federating], true) - - Object.normalize(post, false) - |> Object.prune() - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - assert {:ok, delete} = CommonAPI.delete(post.id, user) - assert delete.local - assert called(Pleroma.Web.Federator.publish(delete)) - end - - refute Activity.get_by_id(post.id) - end - - test "it allows users to delete their posts" do - user = insert(:user) - - {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) - - clear_config([:instance, :federating], true) - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - assert {:ok, delete} = CommonAPI.delete(post.id, user) - assert delete.local - assert called(Pleroma.Web.Federator.publish(delete)) - end - - refute Activity.get_by_id(post.id) - end - - test "it does not allow a user to delete their posts" do - user = insert(:user) - other_user = insert(:user) - - {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) - - assert {:error, "Could not delete"} = CommonAPI.delete(post.id, other_user) - assert Activity.get_by_id(post.id) - end - - test "it allows moderators to delete other user's posts" do - user = insert(:user) - moderator = insert(:user, is_moderator: 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) - end - - test "it allows admins to delete other user's posts" do - user = insert(:user) - moderator = insert(:user, 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) - end - - test "superusers deleting non-local posts won't federate the delete" do - # This is the user of the ingested activity - _user = - insert(:user, - local: false, - ap_id: "http://mastodon.example.org/users/admin", - last_refreshed_at: NaiveDateTime.utc_now() - ) - - moderator = insert(:user, is_admin: true) - - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Jason.decode!() - - {:ok, post} = Transmogrifier.handle_incoming(data) - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - assert {:ok, delete} = CommonAPI.delete(post.id, moderator) - assert delete.local - refute called(Pleroma.Web.Federator.publish(:_)) - end - - refute Activity.get_by_id(post.id) - end - end - - test "favoriting race condition" do - user = insert(:user) - users_serial = insert_list(10, :user) - users = insert_list(10, :user) - - {:ok, activity} = CommonAPI.post(user, %{status: "."}) - - users_serial - |> Enum.map(fn user -> - CommonAPI.favorite(user, activity.id) - end) - - object = Object.get_by_ap_id(activity.data["object"]) - assert object.data["like_count"] == 10 - - users - |> Enum.map(fn user -> - Task.async(fn -> - CommonAPI.favorite(user, activity.id) - end) - end) - |> Enum.map(&Task.await/1) - - object = Object.get_by_ap_id(activity.data["object"]) - assert object.data["like_count"] == 20 - end - - test "repeating race condition" do - user = insert(:user) - users_serial = insert_list(10, :user) - users = insert_list(10, :user) - - {:ok, activity} = CommonAPI.post(user, %{status: "."}) - - users_serial - |> Enum.map(fn user -> - CommonAPI.repeat(activity.id, user) - end) - - object = Object.get_by_ap_id(activity.data["object"]) - assert object.data["announcement_count"] == 10 - - users - |> Enum.map(fn user -> - Task.async(fn -> - CommonAPI.repeat(activity.id, user) - end) - end) - |> Enum.map(&Task.await/1) - - object = Object.get_by_ap_id(activity.data["object"]) - assert object.data["announcement_count"] == 20 - end - - test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) - - [participation] = Participation.for_user(user) - - {:ok, convo_reply} = - CommonAPI.post(user, %{status: ".", in_reply_to_conversation_id: participation.id}) - - assert Visibility.is_direct?(convo_reply) - - assert activity.data["context"] == convo_reply.data["context"] - end - - test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do - har = insert(:user) - jafnhar = insert(:user) - tridi = insert(:user) - - {:ok, activity} = - CommonAPI.post(har, %{ - status: "@#{jafnhar.nickname} hey", - visibility: "direct" - }) - - assert har.ap_id in activity.recipients - assert jafnhar.ap_id in activity.recipients - - [participation] = Participation.for_user(har) - - {:ok, activity} = - CommonAPI.post(har, %{ - status: "I don't really like @#{tridi.nickname}", - visibility: "direct", - in_reply_to_status_id: activity.id, - in_reply_to_conversation_id: participation.id - }) - - assert har.ap_id in activity.recipients - assert jafnhar.ap_id in activity.recipients - refute tridi.ap_id in activity.recipients - end - - test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do - har = insert(:user) - jafnhar = insert(:user) - tridi = insert(:user) - - Pleroma.Config.put([:instance, :safe_dm_mentions], true) - - {:ok, activity} = - CommonAPI.post(har, %{ - status: "@#{jafnhar.nickname} hey, i never want to see @#{tridi.nickname} again", - visibility: "direct" - }) - - refute tridi.ap_id in activity.recipients - assert jafnhar.ap_id in activity.recipients - end - - test "it de-duplicates tags" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU"}) - - object = Object.normalize(activity) - - assert object.data["tag"] == ["2hu"] - end - - test "it adds emoji in the object" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: ":firefox:"}) - - assert Object.normalize(activity).data["emoji"]["firefox"] - end - - describe "posting" do - test "deactivated users can't post" do - user = insert(:user, deactivated: true) - assert {:error, _} = CommonAPI.post(user, %{status: "ye"}) - end - - test "it supports explicit addressing" do - user = insert(:user) - user_two = insert(:user) - user_three = insert(:user) - user_four = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: - "Hey, I think @#{user_three.nickname} is ugly. @#{user_four.nickname} is alright though.", - to: [user_two.nickname, user_four.nickname, "nonexistent"] - }) - - assert user.ap_id in activity.recipients - assert user_two.ap_id in activity.recipients - assert user_four.ap_id in activity.recipients - refute user_three.ap_id in activity.recipients - end - - test "it filters out obviously bad tags when accepting a post as HTML" do - user = insert(:user) - - post = "<p><b>2hu</b></p><script>alert('xss')</script>" - - {:ok, activity} = - CommonAPI.post(user, %{ - status: post, - content_type: "text/html" - }) - - object = Object.normalize(activity) - - assert object.data["content"] == "<p><b>2hu</b></p>alert(&#39;xss&#39;)" - assert object.data["source"] == post - end - - test "it filters out obviously bad tags when accepting a post as Markdown" do - user = insert(:user) - - post = "<p><b>2hu</b></p><script>alert('xss')</script>" - - {:ok, activity} = - CommonAPI.post(user, %{ - status: post, - content_type: "text/markdown" - }) - - object = Object.normalize(activity) - - assert object.data["content"] == "<p><b>2hu</b></p>alert(&#39;xss&#39;)" - assert object.data["source"] == post - end - - test "it does not allow replies to direct messages that are not direct messages themselves" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"}) - - assert {:ok, _} = - CommonAPI.post(user, %{ - status: "suya..", - visibility: "direct", - in_reply_to_status_id: activity.id - }) - - Enum.each(["public", "private", "unlisted"], fn visibility -> - assert {:error, "The message visibility must be direct"} = - CommonAPI.post(user, %{ - status: "suya..", - visibility: visibility, - in_reply_to_status_id: activity.id - }) - end) - end - - test "replying with a direct message will NOT auto-add the author of the reply to the recipient list" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - - {:ok, post} = CommonAPI.post(user, %{status: "I'm stupid"}) - - {:ok, open_answer} = - CommonAPI.post(other_user, %{status: "No ur smart", in_reply_to_status_id: post.id}) - - # The OP is implicitly added - assert user.ap_id in open_answer.recipients - - {:ok, secret_answer} = - CommonAPI.post(other_user, %{ - status: "lol, that guy really is stupid, right, @#{third_user.nickname}?", - in_reply_to_status_id: post.id, - visibility: "direct" - }) - - assert third_user.ap_id in secret_answer.recipients - - # The OP is not added - refute user.ap_id in secret_answer.recipients - end - - test "it allows to address a list" do - user = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) - - {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) - - assert activity.data["bcc"] == [list.ap_id] - assert activity.recipients == [list.ap_id, user.ap_id] - assert activity.data["listMessage"] == list.ap_id - end - - test "it returns error when status is empty and no attachments" do - user = insert(:user) - - assert {:error, "Cannot post an empty status without attachments"} = - CommonAPI.post(user, %{status: ""}) - end - - test "it validates character limits are correctly enforced" do - Pleroma.Config.put([:instance, :limit], 5) - - user = insert(:user) - - assert {:error, "The status is over the character limit"} = - CommonAPI.post(user, %{status: "foobar"}) - - assert {:ok, activity} = CommonAPI.post(user, %{status: "12345"}) - end - - test "it can handle activities that expire" do - user = insert(:user) - - expires_at = DateTime.add(DateTime.utc_now(), 1_000_000) - - assert {:ok, activity} = CommonAPI.post(user, %{status: "chai", expires_in: 1_000_000}) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: activity.id}, - scheduled_at: expires_at - ) - end - end - - describe "reactions" do - test "reacting to a status with an emoji" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) - - {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍") - - assert reaction.data["actor"] == user.ap_id - assert reaction.data["content"] == "👍" - - {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) - - {:error, _} = CommonAPI.react_with_emoji(activity.id, user, ".") - end - - test "unreacting to a status with an emoji" do - user = insert(:user) - other_user = insert(:user) - - clear_config([:instance, :federating], true) - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) - {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍") - - {:ok, unreaction} = CommonAPI.unreact_with_emoji(activity.id, user, "👍") - - assert unreaction.data["type"] == "Undo" - assert unreaction.data["object"] == reaction.data["id"] - assert unreaction.local - - # On federation, it contains the undone (and deleted) object - unreaction_with_object = %{ - unreaction - | data: Map.put(unreaction.data, "object", reaction.data) - } - - assert called(Pleroma.Web.Federator.publish(unreaction_with_object)) - end - end - - test "repeating a status" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) - - {:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, user) - assert Visibility.is_public?(announce_activity) - end - - test "can't repeat a repeat" do - user = insert(:user) - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) - - {:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, other_user) - - refute match?({:ok, %Activity{}}, CommonAPI.repeat(announce.id, user)) - end - - test "repeating a status privately" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) - - {:ok, %Activity{} = announce_activity} = - CommonAPI.repeat(activity.id, user, %{visibility: "private"}) - - assert Visibility.is_private?(announce_activity) - refute Visibility.visible_for_user?(announce_activity, nil) - end - - test "favoriting a status" do - user = insert(:user) - other_user = insert(:user) - - {:ok, post_activity} = CommonAPI.post(other_user, %{status: "cofe"}) - - {:ok, %Activity{data: data}} = CommonAPI.favorite(user, post_activity.id) - assert data["type"] == "Like" - assert data["actor"] == user.ap_id - assert data["object"] == post_activity.data["object"] - end - - test "retweeting a status twice returns the status" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) - {:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, user) - {:ok, ^announce} = CommonAPI.repeat(activity.id, user) - end - - test "favoriting a status twice returns ok, but without the like activity" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) - {:ok, %Activity{}} = CommonAPI.favorite(user, activity.id) - assert {:ok, :already_liked} = CommonAPI.favorite(user, activity.id) - end - end - - describe "pinned statuses" do - setup do - Pleroma.Config.put([:instance, :max_pinned_statuses], 1) - - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"}) - - [user: user, activity: activity] - end - - test "pin status", %{user: user, activity: activity} do - assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) - - id = activity.id - user = refresh_record(user) - - assert %User{pinned_activities: [^id]} = user - end - - test "pin poll", %{user: user} do - {:ok, activity} = - CommonAPI.post(user, %{ - status: "How is fediverse today?", - poll: %{options: ["Absolutely outstanding", "Not good"], expires_in: 20} - }) - - assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) - - id = activity.id - user = refresh_record(user) - - assert %User{pinned_activities: [^id]} = user - end - - test "unlisted statuses can be pinned", %{user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!", visibility: "unlisted"}) - assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) - end - - test "only self-authored can be pinned", %{activity: activity} do - user = insert(:user) - - assert {:error, "Could not pin"} = CommonAPI.pin(activity.id, user) - end - - test "max pinned statuses", %{user: user, activity: activity_one} do - {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"}) - - assert {:ok, ^activity_one} = CommonAPI.pin(activity_one.id, user) - - user = refresh_record(user) - - assert {:error, "You have already pinned the maximum number of statuses"} = - CommonAPI.pin(activity_two.id, user) - end - - test "unpin status", %{user: user, activity: activity} do - {:ok, activity} = CommonAPI.pin(activity.id, user) - - user = refresh_record(user) - - id = activity.id - - assert match?({:ok, %{id: ^id}}, CommonAPI.unpin(activity.id, user)) - - user = refresh_record(user) - - assert %User{pinned_activities: []} = user - end - - test "should unpin when deleting a status", %{user: user, activity: activity} do - {:ok, activity} = CommonAPI.pin(activity.id, user) - - user = refresh_record(user) - - assert {:ok, _} = CommonAPI.delete(activity.id, user) - - user = refresh_record(user) - - assert %User{pinned_activities: []} = user - end - end - - describe "mute tests" do - setup do - user = insert(:user) - - activity = insert(:note_activity) - - [user: user, activity: activity] - end - - test "marks notifications as read after mute" do - author = insert(:user) - activity = insert(:note_activity, user: author) - - friend1 = insert(:user) - friend2 = insert(:user) - - {:ok, reply_activity} = - CommonAPI.post( - friend2, - %{ - status: "@#{author.nickname} @#{friend1.nickname} test reply", - in_reply_to_status_id: activity.id - } - ) - - {:ok, favorite_activity} = CommonAPI.favorite(friend2, activity.id) - {:ok, repeat_activity} = CommonAPI.repeat(activity.id, friend1) - - assert Repo.aggregate( - from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id), - :count - ) == 1 - - unread_notifications = - Repo.all(from(n in Notification, where: n.seen == false, where: n.user_id == ^author.id)) - - assert Enum.any?(unread_notifications, fn n -> - n.type == "favourite" && n.activity_id == favorite_activity.id - end) - - assert Enum.any?(unread_notifications, fn n -> - n.type == "reblog" && n.activity_id == repeat_activity.id - end) - - assert Enum.any?(unread_notifications, fn n -> - n.type == "mention" && n.activity_id == reply_activity.id - end) - - {:ok, _} = CommonAPI.add_mute(author, activity) - assert CommonAPI.thread_muted?(author, activity) - - assert Repo.aggregate( - from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id), - :count - ) == 1 - - read_notifications = - Repo.all(from(n in Notification, where: n.seen == true, where: n.user_id == ^author.id)) - - assert Enum.any?(read_notifications, fn n -> - n.type == "favourite" && n.activity_id == favorite_activity.id - end) - - assert Enum.any?(read_notifications, fn n -> - n.type == "reblog" && n.activity_id == repeat_activity.id - end) - - assert Enum.any?(read_notifications, fn n -> - n.type == "mention" && n.activity_id == reply_activity.id - end) - end - - test "add mute", %{user: user, activity: activity} do - {:ok, _} = CommonAPI.add_mute(user, activity) - assert CommonAPI.thread_muted?(user, activity) - end - - test "remove mute", %{user: user, activity: activity} do - CommonAPI.add_mute(user, activity) - {:ok, _} = CommonAPI.remove_mute(user, activity) - refute CommonAPI.thread_muted?(user, activity) - end - - test "check that mutes can't be duplicate", %{user: user, activity: activity} do - CommonAPI.add_mute(user, activity) - {:error, _} = CommonAPI.add_mute(user, activity) - end - end - - describe "reports" do - test "creates a report" do - reporter = insert(:user) - target_user = insert(:user) - - {:ok, activity} = CommonAPI.post(target_user, %{status: "foobar"}) - - reporter_ap_id = reporter.ap_id - target_ap_id = target_user.ap_id - activity_ap_id = activity.data["id"] - comment = "foobar" - - report_data = %{ - account_id: target_user.id, - comment: comment, - status_ids: [activity.id] - } - - note_obj = %{ - "type" => "Note", - "id" => activity_ap_id, - "content" => "foobar", - "published" => activity.object.data["published"], - "actor" => AccountView.render("show.json", %{user: target_user}) - } - - assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data) - - assert %Activity{ - actor: ^reporter_ap_id, - data: %{ - "type" => "Flag", - "content" => ^comment, - "object" => [^target_ap_id, ^note_obj], - "state" => "open" - } - } = flag_activity - end - - test "updates report state" do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %Activity{id: report_id}} = - 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, activity_id] = report.data["object"] - - assert reported_user == target_user.ap_id - assert activity_id == activity.data["id"] - end - - test "does not update report state when state is unsupported" do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %Activity{id: report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"} - end - - test "updates state of multiple reports" do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %Activity{id: first_report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - {:ok, %Activity{id: second_report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel very offended!", - status_ids: [activity.id] - }) - - {:ok, report_ids} = - CommonAPI.update_report_state([first_report_id, second_report_id], "resolved") - - first_report = Activity.get_by_id(first_report_id) - second_report = Activity.get_by_id(second_report_id) - - assert report_ids -- [first_report_id, second_report_id] == [] - assert first_report.data["state"] == "resolved" - assert second_report.data["state"] == "resolved" - end - end - - describe "reblog muting" do - setup do - muter = insert(:user) - - muted = insert(:user) - - [muter: muter, muted: muted] - end - - test "add a reblog mute", %{muter: muter, muted: muted} do - {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted) - - assert User.showing_reblogs?(muter, muted) == false - end - - test "remove a reblog mute", %{muter: muter, muted: muted} do - {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted) - {:ok, _reblog_mute} = CommonAPI.show_reblogs(muter, muted) - - assert User.showing_reblogs?(muter, muted) == true - end - end - - describe "follow/2" do - test "directly follows a non-locked local user" do - [follower, followed] = insert_pair(:user) - {:ok, follower, followed, _} = CommonAPI.follow(follower, followed) - - assert User.following?(follower, followed) - end - end - - describe "unfollow/2" do - test "also unsubscribes a user" do - [follower, followed] = insert_pair(:user) - {:ok, follower, followed, _} = CommonAPI.follow(follower, followed) - {:ok, _subscription} = User.subscribe(follower, followed) - - assert User.subscribed_to?(follower, followed) - - {:ok, follower} = CommonAPI.unfollow(follower, followed) - - refute User.subscribed_to?(follower, followed) - end - - test "cancels a pending follow for a local user" do - follower = insert(:user) - followed = insert(:user, locked: true) - - assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = - CommonAPI.follow(follower, followed) - - assert User.get_follow_state(follower, followed) == :follow_pending - assert {:ok, follower} = CommonAPI.unfollow(follower, followed) - assert User.get_follow_state(follower, followed) == nil - - assert %{id: ^activity_id, data: %{"state" => "cancelled"}} = - Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed) - - assert %{ - data: %{ - "type" => "Undo", - "object" => %{"type" => "Follow", "state" => "cancelled"} - } - } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower) - end - - test "cancels a pending follow for a remote user" do - follower = insert(:user) - followed = insert(:user, locked: true, local: false, ap_enabled: true) - - assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = - CommonAPI.follow(follower, followed) - - assert User.get_follow_state(follower, followed) == :follow_pending - assert {:ok, follower} = CommonAPI.unfollow(follower, followed) - assert User.get_follow_state(follower, followed) == nil - - assert %{id: ^activity_id, data: %{"state" => "cancelled"}} = - Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed) - - assert %{ - data: %{ - "type" => "Undo", - "object" => %{"type" => "Follow", "state" => "cancelled"} - } - } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower) - end - end - - describe "accept_follow_request/2" do - test "after acceptance, it sets all existing pending follow request states to 'accept'" do - user = insert(:user, locked: true) - follower = insert(:user) - follower_two = insert(:user) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) - {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) - {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user) - - assert follow_activity.data["state"] == "pending" - assert follow_activity_two.data["state"] == "pending" - assert follow_activity_three.data["state"] == "pending" - - {:ok, _follower} = CommonAPI.accept_follow_request(follower, user) - - assert Repo.get(Activity, follow_activity.id).data["state"] == "accept" - assert Repo.get(Activity, follow_activity_two.id).data["state"] == "accept" - assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending" - end - - test "after rejection, it sets all existing pending follow request states to 'reject'" do - user = insert(:user, locked: true) - follower = insert(:user) - follower_two = insert(:user) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) - {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) - {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user) - - assert follow_activity.data["state"] == "pending" - assert follow_activity_two.data["state"] == "pending" - assert follow_activity_three.data["state"] == "pending" - - {:ok, _follower} = CommonAPI.reject_follow_request(follower, user) - - assert Repo.get(Activity, follow_activity.id).data["state"] == "reject" - assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject" - assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending" - end - - test "doesn't create a following relationship if the corresponding follow request doesn't exist" do - user = insert(:user, locked: true) - not_follower = insert(:user) - CommonAPI.accept_follow_request(not_follower, user) - - assert Pleroma.FollowingRelationship.following?(not_follower, user) == false - end - end - - describe "vote/3" do - test "does not allow to vote twice" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "Am I cute?", - poll: %{options: ["Yes", "No"], expires_in: 20} - }) - - object = Object.normalize(activity) - - {:ok, _, object} = CommonAPI.vote(other_user, object, [0]) - - assert {:error, "Already voted"} == CommonAPI.vote(other_user, object, [1]) - end - end - - describe "listen/2" do - test "returns a valid activity" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.listen(user, %{ - title: "lain radio episode 1", - album: "lain radio", - artist: "lain", - length: 180_000 - }) - - object = Object.normalize(activity) - - assert object.data["title"] == "lain radio episode 1" - - assert Visibility.get_visibility(activity) == "public" - end - - test "respects visibility=private" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.listen(user, %{ - title: "lain radio episode 1", - album: "lain radio", - artist: "lain", - length: 180_000, - visibility: "private" - }) - - object = Object.normalize(activity) - - assert object.data["title"] == "lain radio episode 1" - - assert Visibility.get_visibility(activity) == "private" - end - end - - describe "get_user/1" do - test "gets user by ap_id" do - user = insert(:user) - assert CommonAPI.get_user(user.ap_id) == user - end - - test "gets user by guessed nickname" do - user = insert(:user, ap_id: "", nickname: "mario@mushroom.kingdom") - assert CommonAPI.get_user("https://mushroom.kingdom/users/mario") == user - end - - test "fallback" do - assert %User{ - name: "", - ap_id: "", - nickname: "erroruser@example.com" - } = CommonAPI.get_user("") - end - end -end diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs @@ -1,593 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.CommonAPI.UtilsTest do - alias Pleroma.Builders.UserBuilder - alias Pleroma.Object - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.CommonAPI.Utils - use Pleroma.DataCase - - import ExUnit.CaptureLog - import Pleroma.Factory - - @public_address "https://www.w3.org/ns/activitystreams#Public" - - describe "add_attachments/2" do - setup do - name = - "Sakura Mana – Turned on by a Senior OL with a Temptating Tight Skirt-s Full Hipline and Panty Shot- Beautiful Thick Thighs- and Erotic Ass- -2015- -- Oppaitime 8-28-2017 6-50-33 PM.png" - - attachment = %{ - "url" => [%{"href" => URI.encode(name)}] - } - - %{name: name, attachment: attachment} - end - - test "it adds attachment links to a given text and attachment set", %{ - name: name, - attachment: attachment - } do - len = 10 - clear_config([Pleroma.Upload, :filename_display_max_length], len) - - expected = - "<br><a href=\"#{URI.encode(name)}\" class='attachment'>#{String.slice(name, 0..len)}…</a>" - - assert Utils.add_attachments("", [attachment]) == expected - end - - test "doesn't truncate file name if config for truncate is set to 0", %{ - name: name, - attachment: attachment - } do - clear_config([Pleroma.Upload, :filename_display_max_length], 0) - - expected = "<br><a href=\"#{URI.encode(name)}\" class='attachment'>#{name}</a>" - - assert Utils.add_attachments("", [attachment]) == expected - end - end - - describe "it confirms the password given is the current users password" do - test "incorrect password given" do - {:ok, user} = UserBuilder.insert() - - assert Utils.confirm_current_password(user, "") == {:error, "Invalid password."} - end - - test "correct password given" do - {:ok, user} = UserBuilder.insert() - assert Utils.confirm_current_password(user, "test") == {:ok, user} - end - end - - describe "format_input/3" do - test "works for bare text/plain" do - text = "hello world!" - expected = "hello world!" - - {output, [], []} = Utils.format_input(text, "text/plain") - - assert output == expected - - text = "hello world!\n\nsecond paragraph!" - expected = "hello world!<br><br>second paragraph!" - - {output, [], []} = Utils.format_input(text, "text/plain") - - assert output == expected - end - - test "works for bare text/html" do - text = "<p>hello world!</p>" - expected = "<p>hello world!</p>" - - {output, [], []} = Utils.format_input(text, "text/html") - - assert output == expected - - text = "<p>hello world!</p><br/>\n<p>second paragraph</p>" - expected = "<p>hello world!</p><br/>\n<p>second paragraph</p>" - - {output, [], []} = Utils.format_input(text, "text/html") - - assert output == expected - end - - test "works for bare text/markdown" do - text = "**hello world**" - expected = "<p><strong>hello world</strong></p>" - - {output, [], []} = Utils.format_input(text, "text/markdown") - - assert output == expected - - text = "**hello world**\n\n*another paragraph*" - expected = "<p><strong>hello world</strong></p><p><em>another paragraph</em></p>" - - {output, [], []} = Utils.format_input(text, "text/markdown") - - assert output == expected - - text = """ - > cool quote - - by someone - """ - - expected = "<blockquote><p>cool quote</p></blockquote><p>by someone</p>" - - {output, [], []} = Utils.format_input(text, "text/markdown") - - assert output == expected - end - - test "works for bare text/bbcode" do - text = "[b]hello world[/b]" - expected = "<strong>hello world</strong>" - - {output, [], []} = Utils.format_input(text, "text/bbcode") - - assert output == expected - - text = "[b]hello world![/b]\n\nsecond paragraph!" - expected = "<strong>hello world!</strong><br><br>second paragraph!" - - {output, [], []} = Utils.format_input(text, "text/bbcode") - - assert output == expected - - text = "[b]hello world![/b]\n\n<strong>second paragraph!</strong>" - - expected = - "<strong>hello world!</strong><br><br>&lt;strong&gt;second paragraph!&lt;/strong&gt;" - - {output, [], []} = Utils.format_input(text, "text/bbcode") - - assert output == expected - end - - test "works for text/markdown with mentions" do - {:ok, user} = - UserBuilder.insert(%{nickname: "user__test", ap_id: "http://foo.com/user__test"}) - - text = "**hello world**\n\n*another @user__test and @user__test google.com paragraph*" - - {output, _, _} = Utils.format_input(text, "text/markdown") - - assert output == - ~s(<p><strong>hello world</strong></p><p><em>another <span class="h-card"><a class="u-url mention" data-user="#{ - user.id - }" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> and <span class="h-card"><a class="u-url mention" data-user="#{ - user.id - }" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> <a href="http://google.com" rel="ugc">google.com</a> paragraph</em></p>) - 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() - - expected = - date - |> DateTime.from_iso8601() - |> elem(1) - |> Calendar.Strftime.strftime!("%a %b %d %H:%M:%S %z %Y") - - assert Utils.date_to_asctime(date) == expected - end - - test "when date is a binary in wrong format" do - date = DateTime.utc_now() - - expected = "" - - assert capture_log(fn -> - assert Utils.date_to_asctime(date) == expected - end) =~ "[warn] Date #{date} in wrong format, must be ISO 8601" - end - - test "when date is a Unix timestamp" do - date = DateTime.utc_now() |> DateTime.to_unix() - - expected = "" - - assert capture_log(fn -> - assert Utils.date_to_asctime(date) == expected - end) =~ "[warn] Date #{date} in wrong format, must be ISO 8601" - end - - test "when date is nil" do - expected = "" - - assert capture_log(fn -> - assert Utils.date_to_asctime(nil) == expected - end) =~ "[warn] Date in wrong format, must be ISO 8601" - end - - test "when date is a random string" do - assert capture_log(fn -> - assert Utils.date_to_asctime("foo") == "" - end) =~ "[warn] Date foo in wrong format, must be ISO 8601" - end - end - - describe "get_to_and_cc" do - test "for public posts, not a reply" do - user = insert(:user) - mentioned_user = insert(:user) - mentions = [mentioned_user.ap_id] - - {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "public", nil) - - assert length(to) == 2 - assert length(cc) == 1 - - assert @public_address in to - assert mentioned_user.ap_id in to - assert user.follower_address in cc - end - - test "for public posts, a reply" do - user = insert(:user) - mentioned_user = insert(:user) - third_user = insert(:user) - {:ok, activity} = CommonAPI.post(third_user, %{status: "uguu"}) - mentions = [mentioned_user.ap_id] - - {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "public", nil) - - assert length(to) == 3 - assert length(cc) == 1 - - assert @public_address in to - assert mentioned_user.ap_id in to - assert third_user.ap_id in to - assert user.follower_address in cc - end - - test "for unlisted posts, not a reply" do - user = insert(:user) - mentioned_user = insert(:user) - mentions = [mentioned_user.ap_id] - - {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "unlisted", nil) - - assert length(to) == 2 - assert length(cc) == 1 - - assert @public_address in cc - assert mentioned_user.ap_id in to - assert user.follower_address in to - end - - test "for unlisted posts, a reply" do - user = insert(:user) - mentioned_user = insert(:user) - third_user = insert(:user) - {:ok, activity} = CommonAPI.post(third_user, %{status: "uguu"}) - mentions = [mentioned_user.ap_id] - - {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "unlisted", nil) - - assert length(to) == 3 - assert length(cc) == 1 - - assert @public_address in cc - assert mentioned_user.ap_id in to - assert third_user.ap_id in to - assert user.follower_address in to - end - - test "for private posts, not a reply" do - user = insert(:user) - mentioned_user = insert(:user) - mentions = [mentioned_user.ap_id] - - {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private", nil) - assert length(to) == 2 - assert Enum.empty?(cc) - - assert mentioned_user.ap_id in to - assert user.follower_address in to - end - - test "for private posts, a reply" do - user = insert(:user) - mentioned_user = insert(:user) - third_user = insert(:user) - {:ok, activity} = CommonAPI.post(third_user, %{status: "uguu"}) - mentions = [mentioned_user.ap_id] - - {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private", nil) - - assert length(to) == 2 - assert Enum.empty?(cc) - - assert mentioned_user.ap_id in to - assert user.follower_address in to - end - - test "for direct posts, not a reply" do - user = insert(:user) - mentioned_user = insert(:user) - mentions = [mentioned_user.ap_id] - - {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct", nil) - - assert length(to) == 1 - assert Enum.empty?(cc) - - assert mentioned_user.ap_id in to - end - - test "for direct posts, a reply" do - user = insert(:user) - mentioned_user = insert(:user) - third_user = insert(:user) - {:ok, activity} = CommonAPI.post(third_user, %{status: "uguu"}) - mentions = [mentioned_user.ap_id] - - {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct", nil) - - assert length(to) == 1 - assert Enum.empty?(cc) - - assert mentioned_user.ap_id in to - - {:ok, direct_activity} = CommonAPI.post(third_user, %{status: "uguu", visibility: "direct"}) - - {to, cc} = Utils.get_to_and_cc(user, mentions, direct_activity, "direct", nil) - - assert length(to) == 2 - assert Enum.empty?(cc) - - assert mentioned_user.ap_id in to - assert third_user.ap_id in to - end - end - - describe "to_master_date/1" do - test "removes microseconds from date (NaiveDateTime)" do - assert Utils.to_masto_date(~N[2015-01-23 23:50:07.123]) == "2015-01-23T23:50:07.000Z" - end - - test "removes microseconds from date (String)" do - assert Utils.to_masto_date("2015-01-23T23:50:07.123Z") == "2015-01-23T23:50:07.000Z" - end - - test "returns empty string when date invalid" do - assert Utils.to_masto_date("2015-01?23T23:50:07.123Z") == "" - 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) - assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == ["test"] - end - - test "returns recipients from tag" do - user = insert(:user) - - object = - insert(:note, - user: user, - data: %{ - "tag" => [ - %{"type" => "Hashtag"}, - "", - %{"type" => "Mention", "href" => "https://testing.pleroma.lol/users/lain"}, - %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"}, - %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"} - ] - } - ) - - activity = insert(:note_activity, user: user, note: object) - - assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == [ - "test", - "https://testing.pleroma.lol/users/lain", - "https://shitposter.club/user/5381" - ] - end - - test "returns recipients when object is map" do - user = insert(:user) - object = insert(:note, user: user) - - activity = - insert(:note_activity, - user: user, - note: object, - data_attrs: %{ - "object" => %{ - "tag" => [ - %{"type" => "Hashtag"}, - "", - %{"type" => "Mention", "href" => "https://testing.pleroma.lol/users/lain"}, - %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"}, - %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"} - ] - } - } - ) - - Pleroma.Repo.delete(object) - - assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == [ - "test", - "https://testing.pleroma.lol/users/lain", - "https://shitposter.club/user/5381" - ] - end - - test "returns recipients when object not found" do - user = insert(:user) - object = insert(:note, user: user) - - activity = insert(:note_activity, user: user, note: object) - Pleroma.Repo.delete(object) - - obj_url = activity.data["object"] - - Tesla.Mock.mock(fn - %{method: :get, url: ^obj_url} -> - %Tesla.Env{status: 404, body: ""} - end) - - assert Utils.maybe_notify_mentioned_recipients(["test-test"], activity) == [ - "test-test" - ] - end - end - - describe "attachments_from_ids_descs/2" do - test "returns [] when attachment ids is empty" do - assert Utils.attachments_from_ids_descs([], "{}") == [] - end - - test "returns list attachments with desc" do - object = insert(:note) - desc = Jason.encode!(%{object.id => "test-desc"}) - - assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc) == [ - Map.merge(object.data, %{"name" => "test-desc"}) - ] - end - end - - describe "attachments_from_ids/1" do - test "returns attachments with descs" do - object = insert(:note) - desc = Jason.encode!(%{object.id => "test-desc"}) - - assert Utils.attachments_from_ids(%{ - media_ids: ["#{object.id}"], - descriptions: desc - }) == [ - 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] - end - - test "returns [] when not pass media_ids" do - assert Utils.attachments_from_ids(%{}) == [] - end - end - - describe "maybe_add_list_data/3" do - test "adds list params when found user list" do - user = insert(:user) - {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) - - assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) == - %{ - additional: %{"bcc" => [list.ap_id], "listMessage" => list.ap_id}, - object: %{"listMessage" => list.ap_id} - } - end - - test "returns original params when list not found" do - user = insert(:user) - {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", insert(:user)) - - assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) == - %{additional: %{}, object: %{}} - end - end - - describe "make_note_data/11" do - test "returns note data" do - user = insert(:user) - note = insert(:note) - user2 = insert(:user) - user3 = insert(:user) - - assert Utils.make_note_data( - user.ap_id, - [user2.ap_id], - "2hu", - "<h1>This is :moominmamma: note</h1>", - [], - note.id, - [name: "jimm"], - "test summary", - [user3.ap_id], - false, - %{"custom_tag" => "test"} - ) == %{ - "actor" => user.ap_id, - "attachment" => [], - "cc" => [user3.ap_id], - "content" => "<h1>This is :moominmamma: note</h1>", - "context" => "2hu", - "sensitive" => false, - "summary" => "test summary", - "tag" => ["jimm"], - "to" => [user2.ap_id], - "type" => "Note", - "custom_tag" => "test" - } - end - end - - describe "maybe_add_attachments/3" do - test "returns parsed results when attachment_links is false" do - assert Utils.maybe_add_attachments( - {"test", [], ["tags"]}, - [], - false - ) == {"test", [], ["tags"]} - end - - test "adds attachments to parsed results" do - attachment = %{"url" => [%{"href" => "SakuraPM.png"}]} - - assert Utils.maybe_add_attachments( - {"test", [], ["tags"]}, - [attachment], - true - ) == { - "test<br><a href=\"SakuraPM.png\" class='attachment'>SakuraPM.png</a>", - [], - ["tags"] - } - end - end -end diff --git a/test/web/fallback_test.exs b/test/web/fallback_test.exs @@ -1,80 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -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 - - describe "preloaded data and metadata attached to" do - test "GET /:maybe_nickname_or_id", %{conn: conn} do - 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" - end - - test "GET /*path", %{conn: conn} do - assert conn - |> get("/foo") - |> html_response(200) =~ "<!--server-generated-meta-->" - - refute conn - |> get("/foo/bar") - |> html_response(200) =~ "<!--server-generated-meta-->" - end - end - - describe "preloaded data is attached to" do - test "GET /main/public", %{conn: conn} do - public_page = get(conn, "/main/public") - - refute html_response(public_page, 200) =~ "<!--server-generated-meta-->" - assert html_response(public_page, 200) =~ "initial-results" - end - - test "GET /main/all", %{conn: conn} do - public_page = get(conn, "/main/all") - - refute html_response(public_page, 200) =~ "<!--server-generated-meta-->" - assert html_response(public_page, 200) =~ "initial-results" - end - end - - test "GET /api*path", %{conn: conn} do - assert conn - |> get("/api/foo") - |> json_response(404) == %{"error" => "Not implemented"} - end - - test "GET /pleroma/admin -> /pleroma/admin/", %{conn: conn} do - assert redirected_to(get(conn, "/pleroma/admin")) =~ "/pleroma/admin/" - end - - test "OPTIONS /*path", %{conn: conn} do - assert conn - |> options("/foo") - |> response(204) == "" - - assert conn - |> options("/foo/bar") - |> response(204) == "" - end -end diff --git a/test/web/fed_sockets/fed_registry_test.exs b/test/web/fed_sockets/fed_registry_test.exs @@ -1,124 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.FedSockets.FedRegistryTest do - use ExUnit.Case - - alias Pleroma.Web.FedSockets - alias Pleroma.Web.FedSockets.FedRegistry - alias Pleroma.Web.FedSockets.SocketInfo - - @good_domain "http://good.domain" - @good_domain_origin "good.domain:80" - - setup do - start_supervised({Pleroma.Web.FedSockets.Supervisor, []}) - build_test_socket(@good_domain) - Process.sleep(10) - - :ok - end - - describe "add_fed_socket/1 without conflicting sockets" do - test "can be added" do - Process.sleep(10) - assert {:ok, %SocketInfo{origin: origin}} = FedRegistry.get_fed_socket(@good_domain_origin) - assert origin == "good.domain:80" - end - - test "multiple origins can be added" do - build_test_socket("http://anothergood.domain") - Process.sleep(10) - - assert {:ok, %SocketInfo{origin: origin_1}} = - FedRegistry.get_fed_socket(@good_domain_origin) - - assert {:ok, %SocketInfo{origin: origin_2}} = - FedRegistry.get_fed_socket("anothergood.domain:80") - - assert origin_1 == "good.domain:80" - assert origin_2 == "anothergood.domain:80" - assert FedRegistry.list_all() |> Enum.count() == 2 - end - end - - describe "add_fed_socket/1 when duplicate sockets conflict" do - setup do - build_test_socket(@good_domain) - build_test_socket(@good_domain) - Process.sleep(10) - :ok - end - - test "will be ignored" do - assert {:ok, %SocketInfo{origin: origin, pid: pid_one}} = - FedRegistry.get_fed_socket(@good_domain_origin) - - assert origin == "good.domain:80" - - assert FedRegistry.list_all() |> Enum.count() == 1 - end - - test "the newer process will be closed" do - pid_two = build_test_socket(@good_domain) - - assert {:ok, %SocketInfo{origin: origin, pid: pid_one}} = - FedRegistry.get_fed_socket(@good_domain_origin) - - assert origin == "good.domain:80" - Process.sleep(10) - - refute Process.alive?(pid_two) - - assert FedRegistry.list_all() |> Enum.count() == 1 - end - end - - describe "get_fed_socket/1" do - test "returns missing for unknown hosts" do - assert {:error, :missing} = FedRegistry.get_fed_socket("not_a_dmoain") - end - - test "returns rejected for hosts previously rejected" do - "rejected.domain:80" - |> FedSockets.uri_for_origin() - |> FedRegistry.set_host_rejected() - - assert {:error, :rejected} = FedRegistry.get_fed_socket("rejected.domain:80") - end - - test "can retrieve a previously added SocketInfo" do - build_test_socket(@good_domain) - Process.sleep(10) - assert {:ok, %SocketInfo{origin: origin}} = FedRegistry.get_fed_socket(@good_domain_origin) - assert origin == "good.domain:80" - end - - test "removes references to SocketInfos when the process crashes" do - assert {:ok, %SocketInfo{origin: origin, pid: pid}} = - FedRegistry.get_fed_socket(@good_domain_origin) - - assert origin == "good.domain:80" - - Process.exit(pid, :testing) - Process.sleep(100) - assert {:error, :missing} = FedRegistry.get_fed_socket(@good_domain_origin) - end - end - - def build_test_socket(uri) do - Kernel.spawn(fn -> fed_socket_almost(uri) end) - end - - def fed_socket_almost(origin) do - FedRegistry.add_fed_socket(origin) - - receive do - :close -> - :ok - after - 5_000 -> :timeout - end - end -end diff --git a/test/web/fed_sockets/fetch_registry_test.exs b/test/web/fed_sockets/fetch_registry_test.exs @@ -1,67 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.FedSockets.FetchRegistryTest do - use ExUnit.Case - - alias Pleroma.Web.FedSockets.FetchRegistry - alias Pleroma.Web.FedSockets.FetchRegistry.FetchRegistryData - - @json_message "hello" - @json_reply "hello back" - - setup do - start_supervised( - {Pleroma.Web.FedSockets.Supervisor, - [ - ping_interval: 8, - connection_duration: 15, - rejection_duration: 5, - fed_socket_fetches: [default: 10, interval: 10] - ]} - ) - - :ok - end - - test "fetches can be stored" do - uuid = FetchRegistry.register_fetch(@json_message) - - assert {:error, :waiting} = FetchRegistry.check_fetch(uuid) - end - - test "fetches can return" do - uuid = FetchRegistry.register_fetch(@json_message) - task = Task.async(fn -> FetchRegistry.register_fetch_received(uuid, @json_reply) end) - - assert {:error, :waiting} = FetchRegistry.check_fetch(uuid) - Task.await(task) - - assert {:ok, %FetchRegistryData{received_json: received_json}} = - FetchRegistry.check_fetch(uuid) - - assert received_json == @json_reply - end - - test "fetches are deleted once popped from stack" do - uuid = FetchRegistry.register_fetch(@json_message) - task = Task.async(fn -> FetchRegistry.register_fetch_received(uuid, @json_reply) end) - Task.await(task) - - assert {:ok, %FetchRegistryData{received_json: received_json}} = - FetchRegistry.check_fetch(uuid) - - assert received_json == @json_reply - assert {:ok, @json_reply} = FetchRegistry.pop_fetch(uuid) - - assert {:error, :missing} = FetchRegistry.check_fetch(uuid) - end - - test "fetches can time out" do - uuid = FetchRegistry.register_fetch(@json_message) - assert {:error, :waiting} = FetchRegistry.check_fetch(uuid) - Process.sleep(500) - assert {:error, :missing} = FetchRegistry.check_fetch(uuid) - end -end diff --git a/test/web/fed_sockets/socket_info_test.exs b/test/web/fed_sockets/socket_info_test.exs @@ -1,118 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.FedSockets.SocketInfoTest do - use ExUnit.Case - - alias Pleroma.Web.FedSockets - alias Pleroma.Web.FedSockets.SocketInfo - - describe "uri_for_origin" do - test "provides the fed_socket URL given the origin information" do - endpoint = "example.com:4000" - assert FedSockets.uri_for_origin(endpoint) =~ "ws://" - assert FedSockets.uri_for_origin(endpoint) =~ endpoint - end - end - - describe "origin" do - test "will provide the origin field given a url" do - endpoint = "example.com:4000" - assert SocketInfo.origin("ws://#{endpoint}") == endpoint - assert SocketInfo.origin("http://#{endpoint}") == endpoint - assert SocketInfo.origin("https://#{endpoint}") == endpoint - end - - test "will proide the origin field given a uri" do - endpoint = "example.com:4000" - uri = URI.parse("http://#{endpoint}") - - assert SocketInfo.origin(uri) == endpoint - end - end - - describe "touch" do - test "will update the TTL" do - endpoint = "example.com:4000" - socket = SocketInfo.build("ws://#{endpoint}") - Process.sleep(2) - touched_socket = SocketInfo.touch(socket) - - assert socket.connected_until < touched_socket.connected_until - end - end - - describe "expired?" do - setup do - start_supervised( - {Pleroma.Web.FedSockets.Supervisor, - [ - ping_interval: 8, - connection_duration: 5, - rejection_duration: 5, - fed_socket_rejections: [lazy: true] - ]} - ) - - :ok - end - - test "tests if the TTL is exceeded" do - endpoint = "example.com:4000" - socket = SocketInfo.build("ws://#{endpoint}") - refute SocketInfo.expired?(socket) - Process.sleep(10) - - assert SocketInfo.expired?(socket) - end - end - - describe "creating outgoing connection records" do - test "can be passed a string" do - assert %{conn_pid: :pid, origin: _origin} = SocketInfo.build("example.com:4000", :pid) - end - - test "can be passed a URI" do - uri = URI.parse("http://example.com:4000") - assert %{conn_pid: :pid, origin: origin} = SocketInfo.build(uri, :pid) - assert origin =~ "example.com:4000" - end - - test "will include the port number" do - assert %{conn_pid: :pid, origin: origin} = SocketInfo.build("http://example.com:4000", :pid) - - assert origin =~ ":4000" - end - - test "will provide the port if missing" do - assert %{conn_pid: :pid, origin: "example.com:80"} = - SocketInfo.build("http://example.com", :pid) - - assert %{conn_pid: :pid, origin: "example.com:443"} = - SocketInfo.build("https://example.com", :pid) - end - end - - describe "creating incoming connection records" do - test "can be passed a string" do - assert %{pid: _, origin: _origin} = SocketInfo.build("example.com:4000") - end - - test "can be passed a URI" do - uri = URI.parse("example.com:4000") - assert %{pid: _, origin: _origin} = SocketInfo.build(uri) - end - - test "will include the port number" do - assert %{pid: _, origin: origin} = SocketInfo.build("http://example.com:4000") - - assert origin =~ ":4000" - end - - test "will provide the port if missing" do - assert %{pid: _, origin: "example.com:80"} = SocketInfo.build("http://example.com") - assert %{pid: _, origin: "example.com:443"} = SocketInfo.build("https://example.com") - end - end -end diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs @@ -1,173 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.FederatorTest do - alias Pleroma.Instances - alias Pleroma.Tests.ObanHelpers - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Federator - alias Pleroma.Workers.PublisherWorker - - use Pleroma.DataCase - use Oban.Testing, repo: Pleroma.Repo - - import Pleroma.Factory - import Mock - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - - :ok - end - - setup_all do: clear_config([:instance, :federating], true) - setup do: clear_config([:instance, :allow_relay]) - setup do: clear_config([:mrf, :policies]) - setup do: clear_config([:mrf_keyword]) - - describe "Publish an activity" do - setup do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "HI"}) - - relay_mock = { - Pleroma.Web.ActivityPub.Relay, - [], - [publish: fn _activity -> send(self(), :relay_publish) end] - } - - %{activity: activity, relay_mock: relay_mock} - end - - test "with relays active, it publishes to the relay", %{ - activity: activity, - relay_mock: relay_mock - } do - with_mocks([relay_mock]) do - Federator.publish(activity) - ObanHelpers.perform(all_enqueued(worker: PublisherWorker)) - end - - assert_received :relay_publish - end - - test "with relays deactivated, it does not publish to the relay", %{ - activity: activity, - relay_mock: relay_mock - } do - Pleroma.Config.put([:instance, :allow_relay], false) - - with_mocks([relay_mock]) do - Federator.publish(activity) - ObanHelpers.perform(all_enqueued(worker: PublisherWorker)) - end - - refute_received :relay_publish - end - end - - describe "Targets reachability filtering in `publish`" do - test "it federates only to reachable instances via AP" do - user = insert(:user) - - {inbox1, inbox2} = - {"https://domain.com/users/nick1/inbox", "https://domain2.com/users/nick2/inbox"} - - insert(:user, %{ - local: false, - nickname: "nick1@domain.com", - ap_id: "https://domain.com/users/nick1", - inbox: inbox1, - ap_enabled: true - }) - - insert(:user, %{ - local: false, - nickname: "nick2@domain2.com", - ap_id: "https://domain2.com/users/nick2", - inbox: inbox2, - ap_enabled: true - }) - - dt = NaiveDateTime.utc_now() - Instances.set_unreachable(inbox1, dt) - - Instances.set_consistently_unreachable(URI.parse(inbox2).host) - - {:ok, _activity} = - CommonAPI.post(user, %{status: "HI @nick1@domain.com, @nick2@domain2.com!"}) - - expected_dt = NaiveDateTime.to_iso8601(dt) - - ObanHelpers.perform(all_enqueued(worker: PublisherWorker)) - - assert ObanHelpers.member?( - %{ - "op" => "publish_one", - "params" => %{"inbox" => inbox1, "unreachable_since" => expected_dt} - }, - all_enqueued(worker: PublisherWorker) - ) - end - end - - describe "Receive an activity" do - test "successfully processes incoming AP docs with correct origin" do - params = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "actor" => "http://mastodon.example.org/users/admin", - "type" => "Create", - "id" => "http://mastodon.example.org/users/admin/activities/1", - "object" => %{ - "type" => "Note", - "content" => "hi world!", - "id" => "http://mastodon.example.org/users/admin/objects/1", - "attributedTo" => "http://mastodon.example.org/users/admin" - }, - "to" => ["https://www.w3.org/ns/activitystreams#Public"] - } - - assert {:ok, job} = Federator.incoming_ap_doc(params) - assert {:ok, _activity} = ObanHelpers.perform(job) - - assert {:ok, job} = Federator.incoming_ap_doc(params) - assert {:error, :already_present} = ObanHelpers.perform(job) - end - - test "rejects incoming AP docs with incorrect origin" do - params = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "actor" => "https://niu.moe/users/rye", - "type" => "Create", - "id" => "http://mastodon.example.org/users/admin/activities/1", - "object" => %{ - "type" => "Note", - "content" => "hi world!", - "id" => "http://mastodon.example.org/users/admin/objects/1", - "attributedTo" => "http://mastodon.example.org/users/admin" - }, - "to" => ["https://www.w3.org/ns/activitystreams#Public"] - } - - assert {:ok, job} = Federator.incoming_ap_doc(params) - assert {:error, :origin_containment_failed} = ObanHelpers.perform(job) - end - - test "it does not crash if MRF rejects the post" do - Pleroma.Config.put([:mrf_keyword, :reject], ["lain"]) - - Pleroma.Config.put( - [:mrf, :policies], - Pleroma.Web.ActivityPub.MRF.KeywordPolicy - ) - - params = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - - assert {:ok, job} = Federator.incoming_ap_doc(params) - assert {:error, _} = ObanHelpers.perform(job) - end - end -end diff --git a/test/web/feed/tag_controller_test.exs b/test/web/feed/tag_controller_test.exs @@ -1,197 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Feed.TagControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - import SweetXml - - alias Pleroma.Object - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Feed.FeedView - - setup do: clear_config([:feed]) - - test "gets a feed (ATOM)", %{conn: conn} do - Pleroma.Config.put( - [:feed, :post_title], - %{max_length: 25, omission: "..."} - ) - - user = insert(:user) - {:ok, activity1} = CommonAPI.post(user, %{status: "yeah #PleromaArt"}) - - object = Object.normalize(activity1) - - object_data = - Map.put(object.data, "attachment", [ - %{ - "url" => [ - %{ - "href" => - "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - } - ]) - - object - |> Ecto.Changeset.change(data: object_data) - |> Pleroma.Repo.update() - - {:ok, activity2} = CommonAPI.post(user, %{status: "42 This is :moominmamma #PleromaArt"}) - - {:ok, _activity3} = CommonAPI.post(user, %{status: "This is :moominmamma"}) - - response = - conn - |> put_req_header("accept", "application/atom+xml") - |> get(tag_feed_path(conn, :feed, "pleromaart.atom")) - |> response(200) - - xml = parse(response) - - assert xpath(xml, ~x"//feed/title/text()") == '#pleromaart' - - assert xpath(xml, ~x"//feed/entry/title/text()"l) == [ - '42 This is :moominmamm...', - 'yeah #PleromaArt' - ] - - 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 - |> put_req_header("accept", "application/atom+xml") - |> get("/tags/pleromaart.atom", %{"max_id" => activity2.id}) - - assert get_resp_header(conn, "content-type") == ["application/atom+xml; charset=utf-8"] - resp = response(conn, 200) - xml = parse(resp) - - assert xpath(xml, ~x"//feed/title/text()") == '#pleromaart' - - assert xpath(xml, ~x"//feed/entry/title/text()"l) == [ - 'yeah #PleromaArt' - ] - end - - test "gets a feed (RSS)", %{conn: conn} do - Pleroma.Config.put( - [:feed, :post_title], - %{max_length: 25, omission: "..."} - ) - - user = insert(:user) - {:ok, activity1} = CommonAPI.post(user, %{status: "yeah #PleromaArt"}) - - object = Object.normalize(activity1) - - object_data = - Map.put(object.data, "attachment", [ - %{ - "url" => [ - %{ - "href" => - "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - } - ]) - - object - |> Ecto.Changeset.change(data: object_data) - |> Pleroma.Repo.update() - - {:ok, activity2} = CommonAPI.post(user, %{status: "42 This is :moominmamma #PleromaArt"}) - - {:ok, _activity3} = CommonAPI.post(user, %{status: "This is :moominmamma"}) - - response = - conn - |> put_req_header("accept", "application/rss+xml") - |> get(tag_feed_path(conn, :feed, "pleromaart.rss")) - |> response(200) - - xml = parse(response) - assert xpath(xml, ~x"//channel/title/text()") == '#pleromaart' - - assert xpath(xml, ~x"//channel/description/text()"s) == - "These are public toots tagged with #pleromaart. You can interact with them if you have an account anywhere in the fediverse." - - assert xpath(xml, ~x"//channel/link/text()") == - '#{Pleroma.Web.base_url()}/tags/pleromaart.rss' - - assert xpath(xml, ~x"//channel/webfeeds:logo/text()") == - '#{Pleroma.Web.base_url()}/static/logo.png' - - assert xpath(xml, ~x"//channel/item/title/text()"l) == [ - '42 This is :moominmamm...', - 'yeah #PleromaArt' - ] - - assert xpath(xml, ~x"//channel/item/pubDate/text()"sl) == [ - FeedView.pub_date(activity2.data["published"]), - FeedView.pub_date(activity1.data["published"]) - ] - - assert xpath(xml, ~x"//channel/item/enclosure/@url"sl) == [ - "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4" - ] - - obj1 = Object.normalize(activity1) - obj2 = Object.normalize(activity2) - - assert xpath(xml, ~x"//channel/item/description/text()"sl) == [ - HtmlEntities.decode(FeedView.activity_content(obj2.data)), - HtmlEntities.decode(FeedView.activity_content(obj1.data)) - ] - - response = - conn - |> put_req_header("accept", "application/rss+xml") - |> get(tag_feed_path(conn, :feed, "pleromaart")) - |> response(200) - - xml = parse(response) - assert xpath(xml, ~x"//channel/title/text()") == '#pleromaart' - - assert xpath(xml, ~x"//channel/description/text()"s) == - "These are public toots tagged with #pleromaart. You can interact with them if you have an account anywhere in the fediverse." - - conn = - conn - |> put_req_header("accept", "application/rss+xml") - |> get("/tags/pleromaart.rss", %{"max_id" => activity2.id}) - - assert get_resp_header(conn, "content-type") == ["application/rss+xml; charset=utf-8"] - resp = response(conn, 200) - xml = parse(resp) - - assert xpath(xml, ~x"//channel/title/text()") == '#pleromaart' - - assert xpath(xml, ~x"//channel/item/title/text()"l) == [ - 'yeah #PleromaArt' - ] - end - - describe "private instance" do - setup do: clear_config([:instance, :public]) - - test "returns 404 for tags feed", %{conn: conn} do - Config.put([:instance, :public], false) - - conn - |> put_req_header("accept", "application/rss+xml") - |> get(tag_feed_path(conn, :feed, "pleromaart")) - |> response(404) - end - end -end diff --git a/test/web/feed/user_controller_test.exs b/test/web/feed/user_controller_test.exs @@ -1,265 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Feed.UserControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - import SweetXml - - alias Pleroma.Config - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - setup do: clear_config([:instance, :federating], true) - - describe "feed" do - setup do: clear_config([:feed]) - - test "gets an atom feed", %{conn: conn} do - Config.put( - [:feed, :post_title], - %{max_length: 10, omission: "..."} - ) - - activity = insert(:note_activity) - - note = - insert(:note, - data: %{ - "content" => "This is :moominmamma: note ", - "attachment" => [ - %{ - "url" => [ - %{"mediaType" => "image/png", "href" => "https://pleroma.gov/image.png"} - ] - } - ], - "inReplyTo" => activity.data["id"] - } - ) - - note_activity = insert(:note_activity, note: note) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - note2 = - insert(:note, - user: user, - data: %{ - "content" => "42 This is :moominmamma: note ", - "inReplyTo" => activity.data["id"] - } - ) - - note_activity2 = insert(:note_activity, note: note2) - object = Object.normalize(note_activity) - - 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 == ['42 This...', 'This is...'] - assert resp =~ object.data["content"] - - resp = - conn - |> put_req_header("accept", "application/atom+xml") - |> get("/users/#{user.nickname}/feed", %{"max_id" => note_activity2.id}) - |> response(200) - - activity_titles = - resp - |> SweetXml.parse() - |> SweetXml.xpath(~x"//entry/title/text()"l) - - assert activity_titles == ['This is...'] - end - - test "gets a rss feed", %{conn: conn} do - Pleroma.Config.put( - [:feed, :post_title], - %{max_length: 10, omission: "..."} - ) - - activity = insert(:note_activity) - - note = - insert(:note, - data: %{ - "content" => "This is :moominmamma: note ", - "attachment" => [ - %{ - "url" => [ - %{"mediaType" => "image/png", "href" => "https://pleroma.gov/image.png"} - ] - } - ], - "inReplyTo" => activity.data["id"] - } - ) - - note_activity = insert(:note_activity, note: note) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - note2 = - insert(:note, - user: user, - data: %{ - "content" => "42 This is :moominmamma: note ", - "inReplyTo" => activity.data["id"] - } - ) - - note_activity2 = insert(:note_activity, note: note2) - object = Object.normalize(note_activity) - - resp = - conn - |> put_req_header("accept", "application/rss+xml") - |> get("/users/#{user.nickname}/feed.rss") - |> response(200) - - activity_titles = - resp - |> SweetXml.parse() - |> SweetXml.xpath(~x"//item/title/text()"l) - - assert activity_titles == ['42 This...', 'This is...'] - assert resp =~ object.data["content"] - - resp = - conn - |> put_req_header("accept", "application/rss+xml") - |> get("/users/#{user.nickname}/feed.rss", %{"max_id" => note_activity2.id}) - |> response(200) - - activity_titles = - resp - |> SweetXml.parse() - |> SweetXml.xpath(~x"//item/title/text()"l) - - assert activity_titles == ['This is...'] - end - - test "returns 404 for a missing feed", %{conn: conn} do - conn = - conn - |> put_req_header("accept", "application/atom+xml") - |> get(user_feed_path(conn, :feed, "nonexisting")) - - assert response(conn, 404) - end - - test "returns feed with public and unlisted activities", %{conn: conn} do - user = insert(:user) - - {:ok, _} = CommonAPI.post(user, %{status: "public", visibility: "public"}) - {:ok, _} = CommonAPI.post(user, %{status: "direct", visibility: "direct"}) - {:ok, _} = CommonAPI.post(user, %{status: "unlisted", visibility: "unlisted"}) - {:ok, _} = CommonAPI.post(user, %{status: "private", visibility: "private"}) - - 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) - |> Enum.sort() - - assert activity_titles == ['public', 'unlisted'] - end - - test "returns 404 when the user is remote", %{conn: conn} do - user = insert(:user, local: false) - - {:ok, _} = CommonAPI.post(user, %{status: "test"}) - - assert conn - |> put_req_header("accept", "application/atom+xml") - |> get(user_feed_path(conn, :feed, user.nickname)) - |> response(404) - end - end - - # Note: see ActivityPubControllerTest for JSON format tests - describe "feed_redirect" do - test "with html format, it redirects to user feed", %{conn: conn} do - note_activity = insert(:note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - response = - conn - |> get("/users/#{user.nickname}") - |> response(200) - - assert response == - Fallback.RedirectController.redirector_with_meta( - conn, - %{user: user} - ).resp_body - end - - test "with html format, it returns error when user is not found", %{conn: conn} do - response = - conn - |> get("/users/jimm") - |> json_response(404) - - assert response == %{"error" => "Not found"} - end - - test "with non-html / non-json format, it redirects to user feed in atom format", %{ - conn: conn - } do - note_activity = insert(:note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - conn = - conn - |> put_req_header("accept", "application/xml") - |> get("/users/#{user.nickname}") - - assert conn.status == 302 - assert redirected_to(conn) == "#{Pleroma.Web.base_url()}/users/#{user.nickname}/feed.atom" - end - - test "with non-html / non-json format, it returns error when user is not found", %{conn: conn} do - response = - conn - |> put_req_header("accept", "application/xml") - |> get(user_feed_path(conn, :feed, "jimm")) - |> response(404) - - assert response == ~S({"error":"Not found"}) - end - end - - describe "private instance" do - setup do: clear_config([:instance, :public]) - - test "returns 404 for user feed", %{conn: conn} do - Config.put([:instance, :public], false) - user = insert(:user) - - {:ok, _} = CommonAPI.post(user, %{status: "test"}) - - assert conn - |> put_req_header("accept", "application/atom+xml") - |> get(user_feed_path(conn, :feed, user.nickname)) - |> response(404) - end - end -end diff --git a/test/web/instances/instance_test.exs b/test/web/instances/instance_test.exs @@ -1,152 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Instances.InstanceTest do - alias Pleroma.Instances.Instance - alias Pleroma.Repo - - use Pleroma.DataCase - - import ExUnit.CaptureLog - import Pleroma.Factory - - setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1) - - describe "set_reachable/1" do - test "clears `unreachable_since` of existing matching Instance record having non-nil `unreachable_since`" do - unreachable_since = NaiveDateTime.to_iso8601(NaiveDateTime.utc_now()) - instance = insert(:instance, unreachable_since: unreachable_since) - - assert {:ok, instance} = Instance.set_reachable(instance.host) - refute instance.unreachable_since - end - - test "keeps nil `unreachable_since` of existing matching Instance record having nil `unreachable_since`" do - instance = insert(:instance, unreachable_since: nil) - - 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 - test "creates new record having `unreachable_since` to current time if record does not exist" do - assert {:ok, instance} = Instance.set_unreachable("https://domain.com/path") - - instance = Repo.get(Instance, instance.id) - assert instance.unreachable_since - assert "domain.com" == instance.host - end - - test "sets `unreachable_since` of existing record having nil `unreachable_since`" do - instance = insert(:instance, unreachable_since: nil) - refute instance.unreachable_since - - assert {:ok, _} = Instance.set_unreachable(instance.host) - - instance = Repo.get(Instance, instance.id) - assert instance.unreachable_since - end - - test "does NOT modify `unreachable_since` value of existing record in case it's present" do - instance = - insert(:instance, unreachable_since: NaiveDateTime.add(NaiveDateTime.utc_now(), -10)) - - assert instance.unreachable_since - initial_value = instance.unreachable_since - - assert {:ok, _} = Instance.set_unreachable(instance.host) - - instance = Repo.get(Instance, instance.id) - assert initial_value == instance.unreachable_since - end - end - - describe "set_unreachable/2" do - test "sets `unreachable_since` value of existing record in case it's newer than supplied value" do - instance = - insert(:instance, unreachable_since: NaiveDateTime.add(NaiveDateTime.utc_now(), -10)) - - assert instance.unreachable_since - - past_value = NaiveDateTime.add(NaiveDateTime.utc_now(), -100) - assert {:ok, _} = Instance.set_unreachable(instance.host, past_value) - - instance = Repo.get(Instance, instance.id) - assert past_value == instance.unreachable_since - end - - test "does NOT modify `unreachable_since` value of existing record in case it's equal to or older than supplied value" do - instance = - insert(:instance, unreachable_since: NaiveDateTime.add(NaiveDateTime.utc_now(), -10)) - - assert instance.unreachable_since - initial_value = instance.unreachable_since - - assert {:ok, _} = Instance.set_unreachable(instance.host, NaiveDateTime.utc_now()) - - instance = Repo.get(Instance, instance.id) - assert initial_value == instance.unreachable_since - end - end - - describe "get_or_update_favicon/1" do - test "Scrapes favicon URLs" do - Tesla.Mock.mock(fn %{url: "https://favicon.example.org/"} -> - %Tesla.Env{ - status: 200, - body: ~s[<html><head><link rel="icon" href="/favicon.png"></head></html>] - } - end) - - assert "https://favicon.example.org/favicon.png" == - Instance.get_or_update_favicon(URI.parse("https://favicon.example.org/")) - end - - test "Returns nil on too long favicon URLs" do - long_favicon_url = - "https://Lorem.ipsum.dolor.sit.amet/consecteturadipiscingelit/Praesentpharetrapurusutaliquamtempus/Mauriseulaoreetarcu/atfacilisisorci/Nullamporttitor/nequesedfeugiatmollis/dolormagnaefficiturlorem/nonpretiumsapienorcieurisus/Nullamveleratsem/Maecenassedaccumsanexnam/favicon.png" - - Tesla.Mock.mock(fn %{url: "https://long-favicon.example.org/"} -> - %Tesla.Env{ - status: 200, - body: - ~s[<html><head><link rel="icon" href="] <> long_favicon_url <> ~s["></head></html>] - } - end) - - assert capture_log(fn -> - assert nil == - Instance.get_or_update_favicon( - URI.parse("https://long-favicon.example.org/") - ) - end) =~ - "Instance.get_or_update_favicon(\"long-favicon.example.org\") error: %Postgrex.Error{" - end - - test "Handles not getting a favicon URL properly" do - Tesla.Mock.mock(fn %{url: "https://no-favicon.example.org/"} -> - %Tesla.Env{ - status: 200, - body: ~s[<html><head><h1>I wil look down and whisper "GNO.."</h1></head></html>] - } - end) - - refute capture_log(fn -> - assert nil == - Instance.get_or_update_favicon( - URI.parse("https://no-favicon.example.org/") - ) - end) =~ "Instance.scrape_favicon(\"https://no-favicon.example.org/\") error: " - end - end -end diff --git a/test/web/instances/instances_test.exs b/test/web/instances/instances_test.exs @@ -1,124 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.InstancesTest do - alias Pleroma.Instances - - use Pleroma.DataCase - - setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1) - - describe "reachable?/1" do - test "returns `true` for host / url with unknown reachability status" do - assert Instances.reachable?("unknown.site") - assert Instances.reachable?("http://unknown.site") - end - - test "returns `false` for host / url marked unreachable for at least `reachability_datetime_threshold()`" do - host = "consistently-unreachable.name" - Instances.set_consistently_unreachable(host) - - refute Instances.reachable?(host) - refute Instances.reachable?("http://#{host}/path") - end - - test "returns `true` for host / url marked unreachable for less than `reachability_datetime_threshold()`" do - url = "http://eventually-unreachable.name/path" - - Instances.set_unreachable(url) - - assert Instances.reachable?(url) - assert Instances.reachable?(URI.parse(url).host) - end - - test "returns true on non-binary input" do - assert Instances.reachable?(nil) - assert Instances.reachable?(1) - end - end - - describe "filter_reachable/1" do - setup do - host = "consistently-unreachable.name" - url1 = "http://eventually-unreachable.com/path" - url2 = "http://domain.com/path" - - Instances.set_consistently_unreachable(host) - Instances.set_unreachable(url1) - - result = Instances.filter_reachable([host, url1, url2, nil]) - %{result: result, url1: url1, url2: url2} - end - - test "returns a map with keys containing 'not marked consistently unreachable' elements of supplied list", - %{result: result, url1: url1, url2: url2} do - assert is_map(result) - assert Enum.sort([url1, url2]) == result |> Map.keys() |> Enum.sort() - end - - test "returns a map with `unreachable_since` values for keys", - %{result: result, url1: url1, url2: url2} do - assert is_map(result) - assert %NaiveDateTime{} = result[url1] - assert is_nil(result[url2]) - end - - test "returns an empty map for empty list or list containing no hosts / url" do - assert %{} == Instances.filter_reachable([]) - assert %{} == Instances.filter_reachable([nil]) - end - end - - describe "set_reachable/1" do - test "sets unreachable url or host reachable" do - host = "domain.com" - Instances.set_consistently_unreachable(host) - refute Instances.reachable?(host) - - Instances.set_reachable(host) - assert Instances.reachable?(host) - end - - test "keeps reachable url or host reachable" do - url = "https://site.name?q=" - assert Instances.reachable?(url) - - Instances.set_reachable(url) - assert Instances.reachable?(url) - end - - test "returns error status on non-binary input" do - assert {:error, _} = Instances.set_reachable(nil) - assert {:error, _} = Instances.set_reachable(1) - end - end - - # Note: implementation-specific (e.g. Instance) details of set_unreachable/1 - # should be tested in implementation-specific tests - describe "set_unreachable/1" do - test "returns error status on non-binary input" do - assert {:error, _} = Instances.set_unreachable(nil) - assert {:error, _} = Instances.set_unreachable(1) - end - end - - describe "set_consistently_unreachable/1" do - test "sets reachable url or host unreachable" do - url = "http://domain.com?q=" - assert Instances.reachable?(url) - - Instances.set_consistently_unreachable(url) - refute Instances.reachable?(url) - end - - test "keeps unreachable url or host unreachable" do - host = "site.name" - Instances.set_consistently_unreachable(host) - refute Instances.reachable?(host) - - Instances.set_consistently_unreachable(host) - refute Instances.reachable?(host) - end - end -end diff --git a/test/web/masto_fe_controller_test.exs b/test/web/masto_fe_controller_test.exs @@ -1,85 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.MastoFEController do - use Pleroma.Web.ConnCase - - alias Pleroma.Config - alias Pleroma.User - - import Pleroma.Factory - - setup do: clear_config([:instance, :public]) - - test "put settings", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:accounts"])) - |> put("/api/web/settings", %{"data" => %{"programming" => "socks"}}) - - assert _result = json_response(conn, 200) - - user = User.get_cached_by_ap_id(user.ap_id) - assert user.mastofe_settings == %{"programming" => "socks"} - end - - describe "index/2 redirections" do - setup %{conn: conn} do - session_opts = [ - store: :cookie, - key: "_test", - signing_salt: "cooldude" - ] - - conn = - conn - |> Plug.Session.call(Plug.Session.init(session_opts)) - |> fetch_session() - - test_path = "/web/statuses/test" - %{conn: conn, path: test_path} - end - - test "redirects not logged-in users to the login page", %{conn: conn, path: path} do - conn = get(conn, path) - - assert conn.status == 302 - assert redirected_to(conn) == "/web/login" - end - - test "redirects not logged-in users to the login page on private instances", %{ - conn: conn, - path: path - } do - Config.put([:instance, :public], false) - - conn = get(conn, path) - - assert conn.status == 302 - assert redirected_to(conn) == "/web/login" - end - - test "does not redirect logged in users to the login page", %{conn: conn, path: path} do - token = insert(:oauth_token, scopes: ["read"]) - - conn = - conn - |> assign(:user, token.user) - |> assign(:token, token) - |> get(path) - - assert conn.status == 200 - end - - test "saves referer path to session", %{conn: conn, path: path} do - conn = get(conn, path) - return_to = Plug.Conn.get_session(conn, :return_to) - - assert return_to == path - end - end -end diff --git a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs @@ -1,529 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do - alias Pleroma.Repo - alias Pleroma.User - - use Pleroma.Web.ConnCase - - import Mock - import Pleroma.Factory - - setup do: clear_config([:instance, :max_account_fields]) - - describe "updating credentials" do - setup do: oauth_access(["write:accounts"]) - setup :request_content_type - - test "sets user settings in a generic way", %{conn: conn} do - res_conn = - patch(conn, "/api/v1/accounts/update_credentials", %{ - "pleroma_settings_store" => %{ - pleroma_fe: %{ - theme: "bla" - } - } - }) - - assert user_data = json_response_and_validate_schema(res_conn, 200) - assert user_data["pleroma"]["settings_store"] == %{"pleroma_fe" => %{"theme" => "bla"}} - - user = Repo.get(User, user_data["id"]) - - res_conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{ - "pleroma_settings_store" => %{ - masto_fe: %{ - theme: "bla" - } - } - }) - - assert user_data = json_response_and_validate_schema(res_conn, 200) - - assert user_data["pleroma"]["settings_store"] == - %{ - "pleroma_fe" => %{"theme" => "bla"}, - "masto_fe" => %{"theme" => "bla"} - } - - user = Repo.get(User, user_data["id"]) - - clear_config([:instance, :federating], true) - - with_mock Pleroma.Web.Federator, - publish: fn _activity -> :ok end do - res_conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{ - "pleroma_settings_store" => %{ - masto_fe: %{ - theme: "blub" - } - } - }) - - assert user_data = json_response_and_validate_schema(res_conn, 200) - - assert user_data["pleroma"]["settings_store"] == - %{ - "pleroma_fe" => %{"theme" => "bla"}, - "masto_fe" => %{"theme" => "blub"} - } - - assert_called(Pleroma.Web.Federator.publish(:_)) - end - end - - test "updates the user's bio", %{conn: conn} do - user2 = insert(:user) - - raw_bio = "I drink #cofe with @#{user2.nickname}\n\nsuya.." - - conn = patch(conn, "/api/v1/accounts/update_credentials", %{"note" => raw_bio}) - - assert user_data = json_response_and_validate_schema(conn, 200) - - assert user_data["note"] == - ~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe">#cofe</a> with <span class="h-card"><a class="u-url mention" data-user="#{ - user2.id - }" href="#{user2.ap_id}" rel="ugc">@<span>#{user2.nickname}</span></a></span><br/><br/>suya..) - - assert user_data["source"]["note"] == raw_bio - - user = Repo.get(User, user_data["id"]) - - assert user.raw_bio == raw_bio - end - - test "updates the user's locking status", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{locked: "true"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["locked"] == true - end - - test "updates the user's chat acceptance status", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{accepts_chat_messages: "false"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["pleroma"]["accepts_chat_messages"] == false - end - - test "updates the user's allow_following_move", %{user: user, conn: conn} do - assert user.allow_following_move == true - - conn = patch(conn, "/api/v1/accounts/update_credentials", %{allow_following_move: "false"}) - - assert refresh_record(user).allow_following_move == false - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["pleroma"]["allow_following_move"] == false - end - - test "updates the user's default scope", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{default_scope: "unlisted"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["source"]["privacy"] == "unlisted" - end - - test "updates the user's privacy", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{source: %{privacy: "unlisted"}}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["source"]["privacy"] == "unlisted" - end - - test "updates the user's hide_followers status", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_followers: "true"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["pleroma"]["hide_followers"] == true - end - - test "updates the user's discoverable status", %{conn: conn} do - assert %{"source" => %{"pleroma" => %{"discoverable" => true}}} = - conn - |> patch("/api/v1/accounts/update_credentials", %{discoverable: "true"}) - |> json_response_and_validate_schema(:ok) - - assert %{"source" => %{"pleroma" => %{"discoverable" => false}}} = - conn - |> patch("/api/v1/accounts/update_credentials", %{discoverable: "false"}) - |> json_response_and_validate_schema(:ok) - end - - test "updates the user's hide_followers_count and hide_follows_count", %{conn: conn} do - conn = - patch(conn, "/api/v1/accounts/update_credentials", %{ - hide_followers_count: "true", - hide_follows_count: "true" - }) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["pleroma"]["hide_followers_count"] == true - assert user_data["pleroma"]["hide_follows_count"] == true - end - - test "updates the user's skip_thread_containment option", %{user: user, conn: conn} do - response = - conn - |> patch("/api/v1/accounts/update_credentials", %{skip_thread_containment: "true"}) - |> json_response_and_validate_schema(200) - - assert response["pleroma"]["skip_thread_containment"] == true - assert refresh_record(user).skip_thread_containment - end - - test "updates the user's hide_follows status", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_follows: "true"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["pleroma"]["hide_follows"] == true - end - - test "updates the user's hide_favorites status", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_favorites: "true"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["pleroma"]["hide_favorites"] == true - end - - test "updates the user's show_role status", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{show_role: "false"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["source"]["pleroma"]["show_role"] == false - end - - test "updates the user's no_rich_text status", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{no_rich_text: "true"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["source"]["pleroma"]["no_rich_text"] == true - end - - test "updates the user's name", %{conn: conn} do - conn = - patch(conn, "/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["display_name"] == "markorepairs" - - update_activity = Repo.one(Pleroma.Activity) - assert update_activity.data["type"] == "Update" - assert update_activity.data["object"]["name"] == "markorepairs" - end - - test "updates the user's avatar", %{user: user, conn: conn} do - new_avatar = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - assert user.avatar == %{} - - res = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar}) - - assert user_response = json_response_and_validate_schema(res, 200) - assert user_response["avatar"] != User.avatar_url(user) - - user = User.get_by_id(user.id) - refute user.avatar == %{} - - # Also resets it - _res = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => ""}) - - user = User.get_by_id(user.id) - assert user.avatar == nil - end - - test "updates the user's banner", %{user: user, conn: conn} do - new_header = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - res = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header}) - - assert user_response = json_response_and_validate_schema(res, 200) - assert user_response["header"] != User.banner_url(user) - - # Also resets it - _res = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => ""}) - - user = User.get_by_id(user.id) - assert user.banner == nil - end - - test "updates the user's background", %{conn: conn, user: user} do - new_header = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - res = - patch(conn, "/api/v1/accounts/update_credentials", %{ - "pleroma_background_image" => new_header - }) - - assert user_response = json_response_and_validate_schema(res, 200) - assert user_response["pleroma"]["background_image"] - # - # Also resets it - _res = - patch(conn, "/api/v1/accounts/update_credentials", %{"pleroma_background_image" => ""}) - - user = User.get_by_id(user.id) - assert user.background == nil - end - - test "requires 'write:accounts' permission" do - token1 = insert(:oauth_token, scopes: ["read"]) - token2 = insert(:oauth_token, scopes: ["write", "follow"]) - - for token <- [token1, token2] do - conn = - build_conn() - |> put_req_header("content-type", "multipart/form-data") - |> put_req_header("authorization", "Bearer #{token.token}") - |> patch("/api/v1/accounts/update_credentials", %{}) - - if token == token1 do - assert %{"error" => "Insufficient permissions: write:accounts."} == - json_response_and_validate_schema(conn, 403) - else - assert json_response_and_validate_schema(conn, 200) - end - end - end - - test "updates profile emojos", %{user: user, conn: conn} do - note = "*sips :blank:*" - name = "I am :firefox:" - - ret_conn = - patch(conn, "/api/v1/accounts/update_credentials", %{ - "note" => note, - "display_name" => name - }) - - assert json_response_and_validate_schema(ret_conn, 200) - - conn = get(conn, "/api/v1/accounts/#{user.id}") - - assert user_data = json_response_and_validate_schema(conn, 200) - - assert user_data["note"] == note - assert user_data["display_name"] == name - assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = user_data["emojis"] - end - - test "update fields", %{conn: conn} do - fields = [ - %{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "<script>bar</script>"}, - %{"name" => "link.io", "value" => "cofe.io"} - ] - - account_data = - conn - |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(200) - - assert account_data["fields"] == [ - %{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "bar"}, - %{ - "name" => "link.io", - "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>) - } - ] - - assert account_data["source"]["fields"] == [ - %{ - "name" => "<a href=\"http://google.com\">foo</a>", - "value" => "<script>bar</script>" - }, - %{"name" => "link.io", "value" => "cofe.io"} - ] - end - - test "emojis in fields labels", %{conn: conn} do - fields = [ - %{"name" => ":firefox:", "value" => "is best 2hu"}, - %{"name" => "they wins", "value" => ":blank:"} - ] - - account_data = - conn - |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(200) - - assert account_data["fields"] == [ - %{"name" => ":firefox:", "value" => "is best 2hu"}, - %{"name" => "they wins", "value" => ":blank:"} - ] - - assert account_data["source"]["fields"] == [ - %{"name" => ":firefox:", "value" => "is best 2hu"}, - %{"name" => "they wins", "value" => ":blank:"} - ] - - assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = account_data["emojis"] - end - - test "update fields via x-www-form-urlencoded", %{conn: conn} do - fields = - [ - "fields_attributes[1][name]=link", - "fields_attributes[1][value]=http://cofe.io", - "fields_attributes[0][name]=foo", - "fields_attributes[0][value]=bar" - ] - |> Enum.join("&") - - account = - conn - |> put_req_header("content-type", "application/x-www-form-urlencoded") - |> patch("/api/v1/accounts/update_credentials", fields) - |> json_response_and_validate_schema(200) - - assert account["fields"] == [ - %{"name" => "foo", "value" => "bar"}, - %{ - "name" => "link", - "value" => ~S(<a href="http://cofe.io" rel="ugc">http://cofe.io</a>) - } - ] - - assert account["source"]["fields"] == [ - %{"name" => "foo", "value" => "bar"}, - %{"name" => "link", "value" => "http://cofe.io"} - ] - end - - test "update fields with empty name", %{conn: conn} do - fields = [ - %{"name" => "foo", "value" => ""}, - %{"name" => "", "value" => "bar"} - ] - - account = - conn - |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(200) - - assert account["fields"] == [ - %{"name" => "foo", "value" => ""} - ] - end - - test "update fields when invalid request", %{conn: conn} do - name_limit = Pleroma.Config.get([:instance, :account_field_name_length]) - value_limit = Pleroma.Config.get([:instance, :account_field_value_length]) - - long_name = Enum.map(0..name_limit, fn _ -> "x" end) |> Enum.join() - long_value = Enum.map(0..value_limit, fn _ -> "x" end) |> Enum.join() - - fields = [%{"name" => "foo", "value" => long_value}] - - assert %{"error" => "Invalid request"} == - conn - |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(403) - - fields = [%{"name" => long_name, "value" => "bar"}] - - assert %{"error" => "Invalid request"} == - conn - |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(403) - - Pleroma.Config.put([:instance, :max_account_fields], 1) - - fields = [ - %{"name" => "foo", "value" => "bar"}, - %{"name" => "link", "value" => "cofe.io"} - ] - - assert %{"error" => "Invalid request"} == - conn - |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(403) - end - end - - describe "Mark account as bot" do - setup do: oauth_access(["write:accounts"]) - setup :request_content_type - - test "changing actor_type to Service makes account a bot", %{conn: conn} do - account = - conn - |> patch("/api/v1/accounts/update_credentials", %{actor_type: "Service"}) - |> json_response_and_validate_schema(200) - - assert account["bot"] - assert account["source"]["pleroma"]["actor_type"] == "Service" - end - - test "changing actor_type to Person makes account a human", %{conn: conn} do - account = - conn - |> patch("/api/v1/accounts/update_credentials", %{actor_type: "Person"}) - |> json_response_and_validate_schema(200) - - refute account["bot"] - assert account["source"]["pleroma"]["actor_type"] == "Person" - end - - test "changing actor_type to Application causes error", %{conn: conn} do - response = - conn - |> patch("/api/v1/accounts/update_credentials", %{actor_type: "Application"}) - |> json_response_and_validate_schema(403) - - assert %{"error" => "Invalid request"} == response - end - - test "changing bot field to true changes actor_type to Service", %{conn: conn} do - account = - conn - |> patch("/api/v1/accounts/update_credentials", %{bot: "true"}) - |> json_response_and_validate_schema(200) - - assert account["bot"] - assert account["source"]["pleroma"]["actor_type"] == "Service" - end - - test "changing bot field to false changes actor_type to Person", %{conn: conn} do - account = - conn - |> patch("/api/v1/accounts/update_credentials", %{bot: "false"}) - |> json_response_and_validate_schema(200) - - refute account["bot"] - assert account["source"]["pleroma"]["actor_type"] == "Person" - end - - test "actor_type field has a higher priority than bot", %{conn: conn} do - account = - conn - |> patch("/api/v1/accounts/update_credentials", %{ - actor_type: "Person", - bot: "true" - }) - |> json_response_and_validate_schema(200) - - refute account["bot"] - assert account["source"]["pleroma"]["actor_type"] == "Person" - end - end -end diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -1,1536 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.InternalFetchActor - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.OAuth.Token - - import Pleroma.Factory - - describe "account fetching" do - test "works by id" do - %User{id: user_id} = insert(:user) - - assert %{"id" => ^user_id} = - build_conn() - |> get("/api/v1/accounts/#{user_id}") - |> json_response_and_validate_schema(200) - - assert %{"error" => "Can't find user"} = - build_conn() - |> get("/api/v1/accounts/-1") - |> json_response_and_validate_schema(404) - end - - test "works by nickname" do - user = insert(:user) - - assert %{"id" => user_id} = - build_conn() - |> get("/api/v1/accounts/#{user.nickname}") - |> json_response_and_validate_schema(200) - end - - test "works by nickname for remote users" do - clear_config([:instance, :limit_to_local_content], false) - - user = insert(:user, nickname: "user@example.com", local: false) - - assert %{"id" => user_id} = - build_conn() - |> get("/api/v1/accounts/#{user.nickname}") - |> json_response_and_validate_schema(200) - end - - test "respects limit_to_local_content == :all for remote user nicknames" do - clear_config([:instance, :limit_to_local_content], :all) - - user = insert(:user, nickname: "user@example.com", local: false) - - assert build_conn() - |> get("/api/v1/accounts/#{user.nickname}") - |> json_response_and_validate_schema(404) - end - - test "respects limit_to_local_content == :unauthenticated for remote user nicknames" do - clear_config([:instance, :limit_to_local_content], :unauthenticated) - - user = insert(:user, nickname: "user@example.com", local: false) - reading_user = insert(:user) - - conn = - build_conn() - |> get("/api/v1/accounts/#{user.nickname}") - - assert json_response_and_validate_schema(conn, 404) - - conn = - build_conn() - |> assign(:user, reading_user) - |> assign(:token, insert(:oauth_token, user: reading_user, scopes: ["read:accounts"])) - |> get("/api/v1/accounts/#{user.nickname}") - - assert %{"id" => id} = json_response_and_validate_schema(conn, 200) - assert id == user.id - end - - test "accounts fetches correct account for nicknames beginning with numbers", %{conn: conn} do - # Need to set an old-style integer ID to reproduce the problem - # (these are no longer assigned to new accounts but were preserved - # for existing accounts during the migration to flakeIDs) - user_one = insert(:user, %{id: 1212}) - user_two = insert(:user, %{nickname: "#{user_one.id}garbage"}) - - acc_one = - conn - |> get("/api/v1/accounts/#{user_one.id}") - |> json_response_and_validate_schema(:ok) - - acc_two = - conn - |> get("/api/v1/accounts/#{user_two.nickname}") - |> json_response_and_validate_schema(:ok) - - acc_three = - conn - |> get("/api/v1/accounts/#{user_two.id}") - |> json_response_and_validate_schema(:ok) - - refute acc_one == acc_two - assert acc_two == acc_three - end - - test "returns 404 when user is invisible", %{conn: conn} do - user = insert(:user, %{invisible: true}) - - assert %{"error" => "Can't find user"} = - conn - |> get("/api/v1/accounts/#{user.nickname}") - |> json_response_and_validate_schema(404) - end - - test "returns 404 for internal.fetch actor", %{conn: conn} do - %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor() - - assert %{"error" => "Can't find user"} = - conn - |> get("/api/v1/accounts/internal.fetch") - |> json_response_and_validate_schema(404) - end - - test "returns 404 for deactivated user", %{conn: conn} do - user = insert(:user, deactivated: true) - - assert %{"error" => "Can't find user"} = - conn - |> get("/api/v1/accounts/#{user.id}") - |> json_response_and_validate_schema(:not_found) - end - end - - defp local_and_remote_users do - local = insert(:user) - remote = insert(:user, local: false) - {:ok, local: local, remote: remote} - end - - describe "user fetching with restrict unauthenticated profiles for local and remote" do - setup do: local_and_remote_users() - - setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) - - setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - assert %{"error" => "This API requires an authenticated user"} == - conn - |> get("/api/v1/accounts/#{local.id}") - |> json_response_and_validate_schema(:unauthorized) - - assert %{"error" => "This API requires an authenticated user"} == - conn - |> get("/api/v1/accounts/#{remote.id}") - |> json_response_and_validate_schema(:unauthorized) - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/accounts/#{local.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - end - end - - describe "user fetching with restrict unauthenticated profiles for local" do - setup do: local_and_remote_users() - - setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/accounts/#{local.id}") - - assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ - "error" => "This API requires an authenticated user" - } - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/accounts/#{local.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - end - end - - describe "user fetching with restrict unauthenticated profiles for remote" do - setup do: local_and_remote_users() - - setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/accounts/#{local.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}") - - assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ - "error" => "This API requires an authenticated user" - } - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/accounts/#{local.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - end - end - - describe "user timelines" do - setup do: oauth_access(["read:statuses"]) - - test "works with announces that are just addressed to public", %{conn: conn} do - user = insert(:user, ap_id: "https://honktest/u/test", local: false) - other_user = insert(:user) - - {:ok, post} = CommonAPI.post(other_user, %{status: "bonkeronk"}) - - {:ok, announce, _} = - %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "actor" => "https://honktest/u/test", - "id" => "https://honktest/u/test/bonk/1793M7B9MQ48847vdx", - "object" => post.data["object"], - "published" => "2019-06-25T19:33:58Z", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "type" => "Announce" - } - |> ActivityPub.persist(local: false) - - assert resp = - conn - |> get("/api/v1/accounts/#{user.id}/statuses") - |> json_response_and_validate_schema(200) - - assert [%{"id" => id}] = resp - assert id == announce.id - end - - test "deactivated user", %{conn: conn} do - user = insert(:user, deactivated: true) - - assert %{"error" => "Can't find user"} == - conn - |> get("/api/v1/accounts/#{user.id}/statuses") - |> json_response_and_validate_schema(:not_found) - end - - test "returns 404 when user is invisible", %{conn: conn} do - user = insert(:user, %{invisible: true}) - - assert %{"error" => "Can't find user"} = - conn - |> get("/api/v1/accounts/#{user.id}") - |> json_response_and_validate_schema(404) - end - - test "respects blocks", %{user: user_one, conn: conn} do - user_two = insert(:user) - user_three = insert(:user) - - User.block(user_one, user_two) - - {:ok, activity} = CommonAPI.post(user_two, %{status: "User one sux0rz"}) - {:ok, repeat} = CommonAPI.repeat(activity.id, user_three) - - assert resp = - conn - |> get("/api/v1/accounts/#{user_two.id}/statuses") - |> json_response_and_validate_schema(200) - - assert [%{"id" => id}] = resp - assert id == activity.id - - # Even a blocked user will deliver the full user timeline, there would be - # no point in looking at a blocked users timeline otherwise - assert resp = - conn - |> get("/api/v1/accounts/#{user_two.id}/statuses") - |> json_response_and_validate_schema(200) - - assert [%{"id" => id}] = resp - assert id == activity.id - - # Third user's timeline includes the repeat when viewed by unauthenticated user - resp = - build_conn() - |> get("/api/v1/accounts/#{user_three.id}/statuses") - |> json_response_and_validate_schema(200) - - assert [%{"id" => id}] = resp - assert id == repeat.id - - # When viewing a third user's timeline, the blocked users' statuses will NOT be shown - resp = get(conn, "/api/v1/accounts/#{user_three.id}/statuses") - - assert [] == json_response_and_validate_schema(resp, 200) - end - - test "gets users statuses", %{conn: conn} do - user_one = insert(:user) - user_two = insert(:user) - user_three = insert(:user) - - {:ok, _user_three} = User.follow(user_three, user_one) - - {:ok, activity} = CommonAPI.post(user_one, %{status: "HI!!!"}) - - {:ok, direct_activity} = - CommonAPI.post(user_one, %{ - status: "Hi, @#{user_two.nickname}.", - visibility: "direct" - }) - - {:ok, private_activity} = - CommonAPI.post(user_one, %{status: "private", visibility: "private"}) - - # TODO!!! - 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) - - resp = - conn - |> assign(:user, user_two) - |> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"])) - |> get("/api/v1/accounts/#{user_one.id}/statuses") - |> json_response_and_validate_schema(200) - - assert [%{"id" => id_one}, %{"id" => id_two}] = resp - assert id_one == to_string(direct_activity.id) - assert id_two == to_string(activity.id) - - resp = - conn - |> assign(:user, user_three) - |> assign(:token, insert(:oauth_token, user: user_three, scopes: ["read:statuses"])) - |> get("/api/v1/accounts/#{user_one.id}/statuses") - |> json_response_and_validate_schema(200) - - assert [%{"id" => id_one}, %{"id" => id_two}] = resp - assert id_one == to_string(private_activity.id) - assert id_two == to_string(activity.id) - end - - test "unimplemented pinned statuses feature", %{conn: conn} do - note = insert(:note_activity) - user = User.get_cached_by_ap_id(note.data["actor"]) - - conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?pinned=true") - - assert json_response_and_validate_schema(conn, 200) == [] - 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"]) - other_user = insert(:user) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, %{id: media_id}} = ActivityPub.upload(file, actor: user.ap_id) - - {:ok, %{id: image_post_id}} = CommonAPI.post(user, %{status: "cofe", media_ids: [media_id]}) - - {:ok, %{id: media_id}} = ActivityPub.upload(file, actor: other_user.ap_id) - - {:ok, %{id: other_image_post_id}} = - CommonAPI.post(other_user, %{status: "cofe2", media_ids: [media_id]}) - - {:ok, _announce} = CommonAPI.repeat(other_image_post_id, user) - - conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?only_media=true") - - assert [%{"id" => ^image_post_id}] = json_response_and_validate_schema(conn, 200) - - conn = get(build_conn(), "/api/v1/accounts/#{user.id}/statuses?only_media=1") - - assert [%{"id" => ^image_post_id}] = json_response_and_validate_schema(conn, 200) - end - - test "gets a user's statuses without reblogs", %{user: user, conn: conn} do - {:ok, %{id: post_id}} = CommonAPI.post(user, %{status: "HI!!!"}) - {:ok, _} = CommonAPI.repeat(post_id, user) - - conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_reblogs=true") - assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200) - - conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_reblogs=1") - assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200) - end - - test "filters user's statuses by a hashtag", %{user: user, conn: conn} do - {:ok, %{id: post_id}} = CommonAPI.post(user, %{status: "#hashtag"}) - {:ok, _post} = CommonAPI.post(user, %{status: "hashtag"}) - - conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?tagged=hashtag") - assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200) - end - - test "the user views their own timelines and excludes direct messages", %{ - user: user, - conn: conn - } do - {:ok, %{id: public_activity_id}} = - CommonAPI.post(user, %{status: ".", visibility: "public"}) - - {:ok, _direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) - - conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_visibilities[]=direct") - assert [%{"id" => ^public_activity_id}] = json_response_and_validate_schema(conn, 200) - end - end - - defp local_and_remote_activities(%{local: local, remote: remote}) do - insert(:note_activity, user: local) - insert(:note_activity, user: remote, local: false) - - :ok - end - - describe "statuses with restrict unauthenticated profiles for local and remote" do - setup do: local_and_remote_users() - setup :local_and_remote_activities - - setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) - - setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - assert %{"error" => "This API requires an authenticated user"} == - conn - |> get("/api/v1/accounts/#{local.id}/statuses") - |> json_response_and_validate_schema(:unauthorized) - - assert %{"error" => "This API requires an authenticated user"} == - conn - |> get("/api/v1/accounts/#{remote.id}/statuses") - |> json_response_and_validate_schema(:unauthorized) - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - end - end - - describe "statuses with restrict unauthenticated profiles for local" do - setup do: local_and_remote_users() - setup :local_and_remote_activities - - setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - assert %{"error" => "This API requires an authenticated user"} == - conn - |> get("/api/v1/accounts/#{local.id}/statuses") - |> json_response_and_validate_schema(:unauthorized) - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - end - end - - describe "statuses with restrict unauthenticated profiles for remote" do - setup do: local_and_remote_users() - setup :local_and_remote_activities - - setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - - assert %{"error" => "This API requires an authenticated user"} == - conn - |> get("/api/v1/accounts/#{remote.id}/statuses") - |> json_response_and_validate_schema(:unauthorized) - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - end - end - - describe "followers" do - setup do: oauth_access(["read:accounts"]) - - test "getting followers", %{user: user, conn: conn} do - other_user = insert(:user) - {:ok, %{id: user_id}} = User.follow(user, other_user) - - conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers") - - assert [%{"id" => ^user_id}] = json_response_and_validate_schema(conn, 200) - end - - test "getting followers, hide_followers", %{user: user, conn: conn} do - other_user = insert(:user, hide_followers: true) - {:ok, _user} = User.follow(user, other_user) - - conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers") - - assert [] == json_response_and_validate_schema(conn, 200) - end - - test "getting followers, hide_followers, same user requesting" do - user = insert(:user) - other_user = insert(:user, hide_followers: true) - {:ok, _user} = User.follow(user, other_user) - - conn = - build_conn() - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) - |> get("/api/v1/accounts/#{other_user.id}/followers") - - refute [] == json_response_and_validate_schema(conn, 200) - end - - test "getting followers, pagination", %{user: user, conn: conn} do - {:ok, %User{id: follower1_id}} = :user |> insert() |> User.follow(user) - {:ok, %User{id: follower2_id}} = :user |> insert() |> User.follow(user) - {:ok, %User{id: follower3_id}} = :user |> insert() |> User.follow(user) - - assert [%{"id" => ^follower3_id}, %{"id" => ^follower2_id}] = - conn - |> get("/api/v1/accounts/#{user.id}/followers?since_id=#{follower1_id}") - |> json_response_and_validate_schema(200) - - assert [%{"id" => ^follower2_id}, %{"id" => ^follower1_id}] = - conn - |> get("/api/v1/accounts/#{user.id}/followers?max_id=#{follower3_id}") - |> json_response_and_validate_schema(200) - - assert [%{"id" => ^follower2_id}, %{"id" => ^follower1_id}] = - conn - |> get( - "/api/v1/accounts/#{user.id}/followers?id=#{user.id}&limit=20&max_id=#{ - follower3_id - }" - ) - |> json_response_and_validate_schema(200) - - res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?limit=1&max_id=#{follower3_id}") - - assert [%{"id" => ^follower2_id}] = json_response_and_validate_schema(res_conn, 200) - - assert [link_header] = get_resp_header(res_conn, "link") - assert link_header =~ ~r/min_id=#{follower2_id}/ - assert link_header =~ ~r/max_id=#{follower2_id}/ - end - end - - describe "following" do - setup do: oauth_access(["read:accounts"]) - - test "getting following", %{user: user, conn: conn} do - other_user = insert(:user) - {:ok, user} = User.follow(user, other_user) - - conn = get(conn, "/api/v1/accounts/#{user.id}/following") - - assert [%{"id" => id}] = json_response_and_validate_schema(conn, 200) - assert id == to_string(other_user.id) - end - - test "getting following, hide_follows, other user requesting" do - user = insert(:user, hide_follows: true) - other_user = insert(:user) - {:ok, user} = User.follow(user, other_user) - - conn = - build_conn() - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) - |> get("/api/v1/accounts/#{user.id}/following") - - assert [] == json_response_and_validate_schema(conn, 200) - end - - test "getting following, hide_follows, same user requesting" do - user = insert(:user, hide_follows: true) - other_user = insert(:user) - {:ok, user} = User.follow(user, other_user) - - conn = - build_conn() - |> assign(:user, user) - |> assign(:token, insert(:oauth_token, user: user, scopes: ["read:accounts"])) - |> get("/api/v1/accounts/#{user.id}/following") - - refute [] == json_response_and_validate_schema(conn, 200) - end - - test "getting following, pagination", %{user: user, conn: conn} do - following1 = insert(:user) - following2 = insert(:user) - following3 = insert(:user) - {:ok, _} = User.follow(user, following1) - {:ok, _} = User.follow(user, following2) - {:ok, _} = User.follow(user, following3) - - res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?since_id=#{following1.id}") - - assert [%{"id" => id3}, %{"id" => id2}] = json_response_and_validate_schema(res_conn, 200) - assert id3 == following3.id - assert id2 == following2.id - - res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?max_id=#{following3.id}") - - assert [%{"id" => id2}, %{"id" => id1}] = json_response_and_validate_schema(res_conn, 200) - assert id2 == following2.id - assert id1 == following1.id - - res_conn = - get( - conn, - "/api/v1/accounts/#{user.id}/following?id=#{user.id}&limit=20&max_id=#{following3.id}" - ) - - assert [%{"id" => id2}, %{"id" => id1}] = json_response_and_validate_schema(res_conn, 200) - assert id2 == following2.id - assert id1 == following1.id - - res_conn = - get(conn, "/api/v1/accounts/#{user.id}/following?limit=1&max_id=#{following3.id}") - - assert [%{"id" => id2}] = json_response_and_validate_schema(res_conn, 200) - assert id2 == following2.id - - assert [link_header] = get_resp_header(res_conn, "link") - assert link_header =~ ~r/min_id=#{following2.id}/ - assert link_header =~ ~r/max_id=#{following2.id}/ - end - end - - describe "follow/unfollow" do - setup do: oauth_access(["follow"]) - - test "following / unfollowing a user", %{conn: conn} do - %{id: other_user_id, nickname: other_user_nickname} = insert(:user) - - assert %{"id" => _id, "following" => true} = - conn - |> post("/api/v1/accounts/#{other_user_id}/follow") - |> json_response_and_validate_schema(200) - - assert %{"id" => _id, "following" => false} = - conn - |> post("/api/v1/accounts/#{other_user_id}/unfollow") - |> json_response_and_validate_schema(200) - - assert %{"id" => ^other_user_id} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/follows", %{"uri" => other_user_nickname}) - |> json_response_and_validate_schema(200) - end - - test "cancelling follow request", %{conn: conn} do - %{id: other_user_id} = insert(:user, %{locked: true}) - - assert %{"id" => ^other_user_id, "following" => false, "requested" => true} = - conn - |> post("/api/v1/accounts/#{other_user_id}/follow") - |> json_response_and_validate_schema(:ok) - - assert %{"id" => ^other_user_id, "following" => false, "requested" => false} = - conn - |> post("/api/v1/accounts/#{other_user_id}/unfollow") - |> json_response_and_validate_schema(:ok) - end - - test "following without reblogs" do - %{conn: conn} = oauth_access(["follow", "read:statuses"]) - followed = insert(:user) - other_user = insert(:user) - - ret_conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: false}) - - assert %{"showing_reblogs" => false} = json_response_and_validate_schema(ret_conn, 200) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) - {:ok, %{id: reblog_id}} = CommonAPI.repeat(activity.id, followed) - - assert [] == - conn - |> get("/api/v1/timelines/home") - |> json_response(200) - - assert %{"showing_reblogs" => true} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: true}) - |> json_response_and_validate_schema(200) - - assert [%{"id" => ^reblog_id}] = - conn - |> get("/api/v1/timelines/home") - |> json_response(200) - end - - test "following with reblogs" do - %{conn: conn} = oauth_access(["follow", "read:statuses"]) - followed = insert(:user) - other_user = insert(:user) - - ret_conn = post(conn, "/api/v1/accounts/#{followed.id}/follow") - - assert %{"showing_reblogs" => true} = json_response_and_validate_schema(ret_conn, 200) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) - {:ok, %{id: reblog_id}} = CommonAPI.repeat(activity.id, followed) - - assert [%{"id" => ^reblog_id}] = - conn - |> get("/api/v1/timelines/home") - |> json_response(200) - - assert %{"showing_reblogs" => false} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: false}) - |> json_response_and_validate_schema(200) - - assert [] == - conn - |> get("/api/v1/timelines/home") - |> json_response(200) - end - - test "following / unfollowing errors", %{user: user, conn: conn} do - # self follow - conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow") - - assert %{"error" => "Can not follow yourself"} = - json_response_and_validate_schema(conn_res, 400) - - # self unfollow - user = User.get_cached_by_id(user.id) - conn_res = post(conn, "/api/v1/accounts/#{user.id}/unfollow") - - assert %{"error" => "Can not unfollow yourself"} = - json_response_and_validate_schema(conn_res, 400) - - # self follow via uri - user = User.get_cached_by_id(user.id) - - assert %{"error" => "Can not follow yourself"} = - conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/v1/follows", %{"uri" => user.nickname}) - |> json_response_and_validate_schema(400) - - # follow non existing user - conn_res = post(conn, "/api/v1/accounts/doesntexist/follow") - assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404) - - # follow non existing user via uri - conn_res = - conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/v1/follows", %{"uri" => "doesntexist"}) - - assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404) - - # unfollow non existing user - conn_res = post(conn, "/api/v1/accounts/doesntexist/unfollow") - assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404) - end - end - - describe "mute/unmute" do - setup do: oauth_access(["write:mutes"]) - - test "with notifications", %{conn: conn} do - other_user = insert(:user) - - assert %{"id" => _id, "muting" => true, "muting_notifications" => true} = - conn - |> post("/api/v1/accounts/#{other_user.id}/mute") - |> json_response_and_validate_schema(200) - - conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute") - - assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = - json_response_and_validate_schema(conn, 200) - end - - test "without notifications", %{conn: conn} do - other_user = insert(:user) - - ret_conn = - conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"}) - - assert %{"id" => _id, "muting" => true, "muting_notifications" => false} = - json_response_and_validate_schema(ret_conn, 200) - - conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute") - - assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = - json_response_and_validate_schema(conn, 200) - end - end - - describe "pinned statuses" do - setup do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"}) - %{conn: conn} = oauth_access(["read:statuses"], user: user) - - [conn: conn, user: user, activity: activity] - end - - test "returns pinned statuses", %{conn: conn, user: user, activity: %{id: activity_id}} do - {:ok, _} = CommonAPI.pin(activity_id, user) - - assert [%{"id" => ^activity_id, "pinned" => true}] = - conn - |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") - |> json_response_and_validate_schema(200) - end - end - - test "blocking / unblocking a user" do - %{conn: conn} = oauth_access(["follow"]) - other_user = insert(:user) - - ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/block") - - assert %{"id" => _id, "blocking" => true} = json_response_and_validate_schema(ret_conn, 200) - - conn = post(conn, "/api/v1/accounts/#{other_user.id}/unblock") - - assert %{"id" => _id, "blocking" => false} = json_response_and_validate_schema(conn, 200) - end - - describe "create account by app" do - setup do - valid_params = %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - agreement: true - } - - [valid_params: valid_params] - end - - test "registers and logs in without :account_activation_required / :account_approval_required", - %{conn: conn} do - clear_config([:instance, :account_activation_required], false) - clear_config([:instance, :account_approval_required], false) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/apps", %{ - client_name: "client_name", - redirect_uris: "urn:ietf:wg:oauth:2.0:oob", - scopes: "read, write, follow" - }) - - assert %{ - "client_id" => client_id, - "client_secret" => client_secret, - "id" => _, - "name" => "client_name", - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "vapid_key" => _, - "website" => nil - } = json_response_and_validate_schema(conn, 200) - - conn = - post(conn, "/oauth/token", %{ - grant_type: "client_credentials", - client_id: client_id, - client_secret: client_secret - }) - - assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = - json_response(conn, 200) - - assert token - token_from_db = Repo.get_by(Token, token: token) - assert token_from_db - assert refresh - assert scope == "read write follow" - - clear_config([User, :email_blacklist], ["example.org"]) - - params = %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - bio: "Test Bio", - agreement: true - } - - conn = - build_conn() - |> put_req_header("content-type", "multipart/form-data") - |> put_req_header("authorization", "Bearer " <> token) - |> post("/api/v1/accounts", params) - - assert %{"error" => "{\"email\":[\"Invalid email\"]}"} = - json_response_and_validate_schema(conn, 400) - - Pleroma.Config.put([User, :email_blacklist], []) - - conn = - build_conn() - |> put_req_header("content-type", "multipart/form-data") - |> put_req_header("authorization", "Bearer " <> token) - |> post("/api/v1/accounts", params) - - %{ - "access_token" => token, - "created_at" => _created_at, - "scope" => ^scope, - "token_type" => "Bearer" - } = json_response_and_validate_schema(conn, 200) - - token_from_db = Repo.get_by(Token, token: token) - assert token_from_db - user = Repo.preload(token_from_db, :user).user - - assert user - refute user.confirmation_pending - refute user.approval_pending - end - - test "registers but does not log in with :account_activation_required", %{conn: conn} do - clear_config([:instance, :account_activation_required], true) - clear_config([:instance, :account_approval_required], false) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/apps", %{ - client_name: "client_name", - redirect_uris: "urn:ietf:wg:oauth:2.0:oob", - scopes: "read, write, follow" - }) - - assert %{ - "client_id" => client_id, - "client_secret" => client_secret, - "id" => _, - "name" => "client_name", - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "vapid_key" => _, - "website" => nil - } = json_response_and_validate_schema(conn, 200) - - conn = - post(conn, "/oauth/token", %{ - grant_type: "client_credentials", - client_id: client_id, - client_secret: client_secret - }) - - assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = - json_response(conn, 200) - - assert token - token_from_db = Repo.get_by(Token, token: token) - assert token_from_db - assert refresh - assert scope == "read write follow" - - conn = - build_conn() - |> put_req_header("content-type", "multipart/form-data") - |> put_req_header("authorization", "Bearer " <> token) - |> post("/api/v1/accounts", %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - bio: "Test Bio", - agreement: true - }) - - response = json_response_and_validate_schema(conn, 200) - assert %{"identifier" => "missing_confirmed_email"} = response - refute response["access_token"] - refute response["token_type"] - - user = Repo.get_by(User, email: "lain@example.org") - assert user.confirmation_pending - end - - test "registers but does not log in with :account_approval_required", %{conn: conn} do - clear_config([:instance, :account_approval_required], true) - clear_config([:instance, :account_activation_required], false) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/apps", %{ - client_name: "client_name", - redirect_uris: "urn:ietf:wg:oauth:2.0:oob", - scopes: "read, write, follow" - }) - - assert %{ - "client_id" => client_id, - "client_secret" => client_secret, - "id" => _, - "name" => "client_name", - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "vapid_key" => _, - "website" => nil - } = json_response_and_validate_schema(conn, 200) - - conn = - post(conn, "/oauth/token", %{ - grant_type: "client_credentials", - client_id: client_id, - client_secret: client_secret - }) - - assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = - json_response(conn, 200) - - assert token - token_from_db = Repo.get_by(Token, token: token) - assert token_from_db - assert refresh - assert scope == "read write follow" - - conn = - build_conn() - |> put_req_header("content-type", "multipart/form-data") - |> put_req_header("authorization", "Bearer " <> token) - |> post("/api/v1/accounts", %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - bio: "Test Bio", - agreement: true, - reason: "I'm a cool dude, bro" - }) - - response = json_response_and_validate_schema(conn, 200) - assert %{"identifier" => "awaiting_approval"} = response - refute response["access_token"] - refute response["token_type"] - - user = Repo.get_by(User, email: "lain@example.org") - - assert user.approval_pending - 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 - _user = insert(:user, email: "lain@example.org") - app_token = insert(:oauth_token, user: nil) - - res = - conn - |> put_req_header("authorization", "Bearer " <> app_token.token) - |> put_req_header("content-type", "application/json") - |> post("/api/v1/accounts", valid_params) - - assert json_response_and_validate_schema(res, 400) == %{ - "error" => "{\"email\":[\"has already been taken\"]}" - } - end - - test "returns bad_request if missing required params", %{ - conn: conn, - valid_params: valid_params - } do - app_token = insert(:oauth_token, user: nil) - - conn = - conn - |> put_req_header("authorization", "Bearer " <> app_token.token) - |> put_req_header("content-type", "application/json") - - res = post(conn, "/api/v1/accounts", valid_params) - assert json_response_and_validate_schema(res, 200) - - [{127, 0, 0, 1}, {127, 0, 0, 2}, {127, 0, 0, 3}, {127, 0, 0, 4}] - |> Stream.zip(Map.delete(valid_params, :email)) - |> Enum.each(fn {ip, {attr, _}} -> - res = - conn - |> Map.put(:remote_ip, ip) - |> post("/api/v1/accounts", Map.delete(valid_params, attr)) - |> json_response_and_validate_schema(400) - - assert res == %{ - "error" => "Missing field: #{attr}.", - "errors" => [ - %{ - "message" => "Missing field: #{attr}", - "source" => %{"pointer" => "/#{attr}"}, - "title" => "Invalid value" - } - ] - } - end) - end - - test "returns bad_request if missing email params when :account_activation_required is enabled", - %{conn: conn, valid_params: valid_params} do - clear_config([:instance, :account_activation_required], true) - - app_token = insert(:oauth_token, user: nil) - - conn = - conn - |> put_req_header("authorization", "Bearer " <> app_token.token) - |> put_req_header("content-type", "application/json") - - res = - conn - |> Map.put(:remote_ip, {127, 0, 0, 5}) - |> post("/api/v1/accounts", Map.delete(valid_params, :email)) - - assert json_response_and_validate_schema(res, 400) == - %{"error" => "Missing parameter: email"} - - res = - conn - |> Map.put(:remote_ip, {127, 0, 0, 6}) - |> post("/api/v1/accounts", Map.put(valid_params, :email, "")) - - assert json_response_and_validate_schema(res, 400) == %{ - "error" => "{\"email\":[\"can't be blank\"]}" - } - end - - test "allow registration without an email", %{conn: conn, valid_params: valid_params} do - app_token = insert(:oauth_token, user: nil) - conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token) - - res = - conn - |> put_req_header("content-type", "application/json") - |> Map.put(:remote_ip, {127, 0, 0, 7}) - |> post("/api/v1/accounts", Map.delete(valid_params, :email)) - - assert json_response_and_validate_schema(res, 200) - end - - test "allow registration with an empty email", %{conn: conn, valid_params: valid_params} do - app_token = insert(:oauth_token, user: nil) - conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token) - - res = - conn - |> put_req_header("content-type", "application/json") - |> Map.put(:remote_ip, {127, 0, 0, 8}) - |> post("/api/v1/accounts", Map.put(valid_params, :email, "")) - - assert json_response_and_validate_schema(res, 200) - end - - test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_params} do - res = - conn - |> put_req_header("authorization", "Bearer " <> "invalid-token") - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/v1/accounts", valid_params) - - assert json_response_and_validate_schema(res, 403) == %{"error" => "Invalid credentials"} - end - - test "registration from trusted app" do - clear_config([Pleroma.Captcha, :enabled], true) - app = insert(:oauth_app, trusted: true, scopes: ["read", "write", "follow", "push"]) - - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "client_credentials", - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert %{"access_token" => token, "token_type" => "Bearer"} = json_response(conn, 200) - - response = - build_conn() - |> Plug.Conn.put_req_header("authorization", "Bearer " <> token) - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/v1/accounts", %{ - nickname: "nickanme", - agreement: true, - email: "email@example.com", - fullname: "Lain", - username: "Lain", - password: "some_password", - confirm: "some_password" - }) - |> json_response_and_validate_schema(200) - - assert %{ - "access_token" => access_token, - "created_at" => _, - "scope" => "read write follow push", - "token_type" => "Bearer" - } = response - - response = - build_conn() - |> Plug.Conn.put_req_header("authorization", "Bearer " <> access_token) - |> get("/api/v1/accounts/verify_credentials") - |> json_response_and_validate_schema(200) - - assert %{ - "acct" => "Lain", - "bot" => false, - "display_name" => "Lain", - "follow_requests_count" => 0, - "followers_count" => 0, - "following_count" => 0, - "locked" => false, - "note" => "", - "source" => %{ - "fields" => [], - "note" => "", - "pleroma" => %{ - "actor_type" => "Person", - "discoverable" => false, - "no_rich_text" => false, - "show_role" => true - }, - "privacy" => "public", - "sensitive" => false - }, - "statuses_count" => 0, - "username" => "Lain" - } = response - end - end - - describe "create account by app / rate limit" do - setup do: clear_config([:rate_limit, :app_account_creation], {10_000, 2}) - - test "respects rate limit setting", %{conn: conn} do - app_token = insert(:oauth_token, user: nil) - - conn = - conn - |> put_req_header("authorization", "Bearer " <> app_token.token) - |> Map.put(:remote_ip, {15, 15, 15, 15}) - |> put_req_header("content-type", "multipart/form-data") - - for i <- 1..2 do - conn = - conn - |> post("/api/v1/accounts", %{ - username: "#{i}lain", - email: "#{i}lain@example.org", - password: "PlzDontHackLain", - agreement: true - }) - - %{ - "access_token" => token, - "created_at" => _created_at, - "scope" => _scope, - "token_type" => "Bearer" - } = json_response_and_validate_schema(conn, 200) - - token_from_db = Repo.get_by(Token, token: token) - assert token_from_db - token_from_db = Repo.preload(token_from_db, :user) - assert token_from_db.user - end - - conn = - post(conn, "/api/v1/accounts", %{ - username: "6lain", - email: "6lain@example.org", - password: "PlzDontHackLain", - agreement: true - }) - - assert json_response_and_validate_schema(conn, :too_many_requests) == %{ - "error" => "Throttled" - } - end - end - - describe "create account with enabled captcha" do - setup %{conn: conn} do - app_token = insert(:oauth_token, user: nil) - - conn = - conn - |> put_req_header("authorization", "Bearer " <> app_token.token) - |> put_req_header("content-type", "multipart/form-data") - - [conn: conn] - end - - setup do: clear_config([Pleroma.Captcha, :enabled], true) - - test "creates an account and returns 200 if captcha is valid", %{conn: conn} do - %{token: token, answer_data: answer_data} = Pleroma.Captcha.new() - - params = %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - agreement: true, - captcha_solution: Pleroma.Captcha.Mock.solution(), - captcha_token: token, - captcha_answer_data: answer_data - } - - assert %{ - "access_token" => access_token, - "created_at" => _, - "scope" => "read", - "token_type" => "Bearer" - } = - conn - |> post("/api/v1/accounts", params) - |> json_response_and_validate_schema(:ok) - - assert Token |> Repo.get_by(token: access_token) |> Repo.preload(:user) |> Map.get(:user) - - Cachex.del(:used_captcha_cache, token) - end - - test "returns 400 if any captcha field is not provided", %{conn: conn} do - captcha_fields = [:captcha_solution, :captcha_token, :captcha_answer_data] - - valid_params = %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - agreement: true, - captcha_solution: "xx", - captcha_token: "xx", - captcha_answer_data: "xx" - } - - for field <- captcha_fields do - expected = %{ - "error" => "{\"captcha\":[\"Invalid CAPTCHA (Missing parameter: #{field})\"]}" - } - - assert expected == - conn - |> post("/api/v1/accounts", Map.delete(valid_params, field)) - |> json_response_and_validate_schema(:bad_request) - end - end - - test "returns an error if captcha is invalid", %{conn: conn} do - params = %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - agreement: true, - captcha_solution: "cofe", - captcha_token: "cofe", - captcha_answer_data: "cofe" - } - - assert %{"error" => "{\"captcha\":[\"Invalid answer data\"]}"} == - conn - |> post("/api/v1/accounts", params) - |> json_response_and_validate_schema(:bad_request) - end - end - - describe "GET /api/v1/accounts/:id/lists - account_lists" do - test "returns lists to which the account belongs" do - %{user: user, conn: conn} = oauth_access(["read:lists"]) - other_user = insert(:user) - assert {:ok, %Pleroma.List{id: list_id} = list} = Pleroma.List.create("Test List", user) - {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user) - - assert [%{"id" => list_id, "title" => "Test List"}] = - conn - |> get("/api/v1/accounts/#{other_user.id}/lists") - |> json_response_and_validate_schema(200) - end - end - - describe "verify_credentials" do - test "verify_credentials" do - %{user: user, conn: conn} = oauth_access(["read:accounts"]) - - [notification | _] = - insert_list(7, :notification, user: user, activity: insert(:note_activity)) - - Pleroma.Notification.set_read_up_to(user, notification.id) - conn = get(conn, "/api/v1/accounts/verify_credentials") - - response = json_response_and_validate_schema(conn, 200) - - assert %{"id" => id, "source" => %{"privacy" => "public"}} = response - assert response["pleroma"]["chat_token"] - assert response["pleroma"]["unread_notifications_count"] == 6 - assert id == to_string(user.id) - end - - test "verify_credentials default scope unlisted" do - user = insert(:user, default_scope: "unlisted") - %{conn: conn} = oauth_access(["read:accounts"], user: user) - - conn = get(conn, "/api/v1/accounts/verify_credentials") - - assert %{"id" => id, "source" => %{"privacy" => "unlisted"}} = - json_response_and_validate_schema(conn, 200) - - assert id == to_string(user.id) - end - - test "locked accounts" do - user = insert(:user, default_scope: "private") - %{conn: conn} = oauth_access(["read:accounts"], user: user) - - conn = get(conn, "/api/v1/accounts/verify_credentials") - - assert %{"id" => id, "source" => %{"privacy" => "private"}} = - json_response_and_validate_schema(conn, 200) - - assert id == to_string(user.id) - end - end - - describe "user relationships" do - setup do: oauth_access(["read:follows"]) - - test "returns the relationships for the current user", %{user: user, conn: conn} do - %{id: other_user_id} = other_user = insert(:user) - {:ok, _user} = User.follow(user, other_user) - - assert [%{"id" => ^other_user_id}] = - conn - |> get("/api/v1/accounts/relationships?id=#{other_user.id}") - |> json_response_and_validate_schema(200) - - assert [%{"id" => ^other_user_id}] = - conn - |> get("/api/v1/accounts/relationships?id[]=#{other_user.id}") - |> json_response_and_validate_schema(200) - end - - test "returns an empty list on a bad request", %{conn: conn} do - conn = get(conn, "/api/v1/accounts/relationships", %{}) - - assert [] = json_response_and_validate_schema(conn, 200) - end - end - - test "getting a list of mutes" do - %{user: user, conn: conn} = oauth_access(["read:mutes"]) - other_user = insert(:user) - - {:ok, _user_relationships} = User.mute(user, other_user) - - conn = get(conn, "/api/v1/mutes") - - other_user_id = to_string(other_user.id) - assert [%{"id" => ^other_user_id}] = json_response_and_validate_schema(conn, 200) - end - - test "getting a list of blocks" do - %{user: user, conn: conn} = oauth_access(["read:blocks"]) - other_user = insert(:user) - - {:ok, _user_relationship} = User.block(user, other_user) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/blocks") - - other_user_id = to_string(other_user.id) - assert [%{"id" => ^other_user_id}] = json_response_and_validate_schema(conn, 200) - end -end diff --git a/test/web/mastodon_api/controllers/app_controller_test.exs b/test/web/mastodon_api/controllers/app_controller_test.exs @@ -1,60 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.AppControllerTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Repo - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.Push - - import Pleroma.Factory - - test "apps/verify_credentials", %{conn: conn} do - token = insert(:oauth_token) - - conn = - conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> get("/api/v1/apps/verify_credentials") - - app = Repo.preload(token, :app).app - - expected = %{ - "name" => app.client_name, - "website" => app.website, - "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key) - } - - assert expected == json_response_and_validate_schema(conn, 200) - end - - test "creates an oauth app", %{conn: conn} do - user = insert(:user) - app_attrs = build(:oauth_app) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> assign(:user, user) - |> post("/api/v1/apps", %{ - client_name: app_attrs.client_name, - redirect_uris: app_attrs.redirect_uris - }) - - [app] = Repo.all(App) - - expected = %{ - "name" => app.client_name, - "website" => app.website, - "client_id" => app.client_id, - "client_secret" => app.client_secret, - "id" => app.id |> to_string(), - "redirect_uri" => app.redirect_uris, - "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key) - } - - assert expected == json_response_and_validate_schema(conn, 200) - end -end diff --git a/test/web/mastodon_api/controllers/auth_controller_test.exs b/test/web/mastodon_api/controllers/auth_controller_test.exs @@ -1,159 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.AuthControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Config - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - - import Pleroma.Factory - import Swoosh.TestAssertions - - describe "GET /web/login" do - setup %{conn: conn} do - session_opts = [ - store: :cookie, - key: "_test", - signing_salt: "cooldude" - ] - - conn = - conn - |> Plug.Session.call(Plug.Session.init(session_opts)) - |> fetch_session() - - test_path = "/web/statuses/test" - %{conn: conn, path: test_path} - end - - test "redirects to the saved path after log in", %{conn: conn, path: path} do - app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".") - auth = insert(:oauth_authorization, app: app) - - conn = - conn - |> put_session(:return_to, path) - |> get("/web/login", %{code: auth.token}) - - assert conn.status == 302 - assert redirected_to(conn) == path - end - - test "redirects to the getting-started page when referer is not present", %{conn: conn} do - app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".") - auth = insert(:oauth_authorization, app: app) - - conn = get(conn, "/web/login", %{code: auth.token}) - - assert conn.status == 302 - assert redirected_to(conn) == "/web/getting-started" - end - end - - describe "POST /auth/password, with valid parameters" do - setup %{conn: conn} do - user = insert(:user) - conn = post(conn, "/auth/password?email=#{user.email}") - %{conn: conn, user: user} - end - - test "it returns 204", %{conn: conn} do - assert empty_json_response(conn) - end - - test "it creates a PasswordResetToken record for user", %{user: user} do - token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id) - assert token_record - end - - test "it sends an email to user", %{user: user} do - ObanHelpers.perform_all() - token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id) - - email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token) - notify_email = Config.get([:instance, :notify_email]) - instance_name = Config.get([:instance, :name]) - - assert_email_sent( - from: {instance_name, notify_email}, - to: {user.name, user.email}, - html_body: email.html_body - ) - end - end - - describe "POST /auth/password, with nickname" do - test "it returns 204", %{conn: conn} do - user = insert(:user) - - assert conn - |> post("/auth/password?nickname=#{user.nickname}") - |> empty_json_response() - - ObanHelpers.perform_all() - token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id) - - email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token) - notify_email = Config.get([:instance, :notify_email]) - instance_name = Config.get([:instance, :name]) - - assert_email_sent( - from: {instance_name, notify_email}, - to: {user.name, user.email}, - html_body: email.html_body - ) - end - - test "it doesn't fail when a user has no email", %{conn: conn} do - user = insert(:user, %{email: nil}) - - assert conn - |> post("/auth/password?nickname=#{user.nickname}") - |> empty_json_response() - end - end - - describe "POST /auth/password, with invalid parameters" do - setup do - user = insert(:user) - {:ok, user: user} - end - - test "it returns 204 when user is not found", %{conn: conn, user: user} do - conn = post(conn, "/auth/password?email=nonexisting_#{user.email}") - - assert empty_json_response(conn) - end - - test "it returns 204 when user is not local", %{conn: conn, user: user} do - {:ok, user} = Repo.update(Ecto.Changeset.change(user, local: false)) - conn = post(conn, "/auth/password?email=#{user.email}") - - assert empty_json_response(conn) - end - - test "it returns 204 when user is deactivated", %{conn: conn, user: user} do - {:ok, user} = Repo.update(Ecto.Changeset.change(user, deactivated: true, local: true)) - conn = post(conn, "/auth/password?email=#{user.email}") - - assert empty_json_response(conn) - end - end - - describe "DELETE /auth/sign_out" do - test "redirect to root page", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> delete("/auth/sign_out") - - assert conn.status == 302 - assert redirected_to(conn) == "/" - end - end -end diff --git a/test/web/mastodon_api/controllers/conversation_controller_test.exs b/test/web/mastodon_api/controllers/conversation_controller_test.exs @@ -1,209 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - setup do: oauth_access(["read:statuses"]) - - describe "returns a list of conversations" do - setup(%{user: user_one, conn: conn}) do - user_two = insert(:user) - user_three = insert(:user) - - {:ok, user_two} = User.follow(user_two, user_one) - - {:ok, %{user: user_one, user_two: user_two, user_three: user_three, conn: conn}} - end - - test "returns correct conversations", %{ - user: user_one, - user_two: user_two, - user_three: user_three, - conn: conn - } do - assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 - {:ok, direct} = create_direct_message(user_one, [user_two, user_three]) - - assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1 - - {:ok, _follower_only} = - CommonAPI.post(user_one, %{ - status: "Hi @#{user_two.nickname}!", - visibility: "private" - }) - - res_conn = get(conn, "/api/v1/conversations") - - assert response = json_response_and_validate_schema(res_conn, 200) - - assert [ - %{ - "id" => res_id, - "accounts" => res_accounts, - "last_status" => res_last_status, - "unread" => unread - } - ] = response - - account_ids = Enum.map(res_accounts, & &1["id"]) - assert length(res_accounts) == 2 - assert user_two.id in account_ids - assert user_three.id in account_ids - assert is_binary(res_id) - assert unread == false - assert res_last_status["id"] == direct.id - assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 - end - - test "observes limit params", %{ - user: user_one, - user_two: user_two, - user_three: user_three, - conn: conn - } do - {:ok, _} = create_direct_message(user_one, [user_two, user_three]) - {:ok, _} = create_direct_message(user_two, [user_one, user_three]) - {:ok, _} = create_direct_message(user_three, [user_two, user_one]) - - res_conn = get(conn, "/api/v1/conversations?limit=1") - - assert response = json_response_and_validate_schema(res_conn, 200) - - assert Enum.count(response) == 1 - - res_conn = get(conn, "/api/v1/conversations?limit=2") - - assert response = json_response_and_validate_schema(res_conn, 200) - - assert Enum.count(response) == 2 - end - end - - test "filters conversations by recipients", %{user: user_one, conn: conn} do - user_two = insert(:user) - user_three = insert(:user) - {:ok, direct1} = create_direct_message(user_one, [user_two]) - {:ok, _direct2} = create_direct_message(user_one, [user_three]) - {:ok, direct3} = create_direct_message(user_one, [user_two, user_three]) - {:ok, _direct4} = create_direct_message(user_two, [user_three]) - {:ok, direct5} = create_direct_message(user_two, [user_one]) - - assert [conversation1, conversation2] = - conn - |> get("/api/v1/conversations?recipients[]=#{user_two.id}") - |> json_response_and_validate_schema(200) - - assert conversation1["last_status"]["id"] == direct5.id - assert conversation2["last_status"]["id"] == direct1.id - - [conversation1] = - conn - |> get("/api/v1/conversations?recipients[]=#{user_two.id}&recipients[]=#{user_three.id}") - |> json_response_and_validate_schema(200) - - assert conversation1["last_status"]["id"] == direct3.id - end - - test "updates the last_status on reply", %{user: user_one, conn: conn} do - user_two = insert(:user) - {:ok, direct} = create_direct_message(user_one, [user_two]) - - {:ok, direct_reply} = - CommonAPI.post(user_two, %{ - status: "reply", - visibility: "direct", - in_reply_to_status_id: direct.id - }) - - [%{"last_status" => res_last_status}] = - conn - |> get("/api/v1/conversations") - |> json_response_and_validate_schema(200) - - assert res_last_status["id"] == direct_reply.id - end - - test "the user marks a conversation as read", %{user: user_one, conn: conn} do - user_two = insert(:user) - {:ok, direct} = create_direct_message(user_one, [user_two]) - - assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 - assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1 - - user_two_conn = - build_conn() - |> assign(:user, user_two) - |> assign( - :token, - insert(:oauth_token, user: user_two, scopes: ["read:statuses", "write:conversations"]) - ) - - [%{"id" => direct_conversation_id, "unread" => true}] = - user_two_conn - |> get("/api/v1/conversations") - |> json_response_and_validate_schema(200) - - %{"unread" => false} = - user_two_conn - |> post("/api/v1/conversations/#{direct_conversation_id}/read") - |> json_response_and_validate_schema(200) - - assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 - assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 - - # The conversation is marked as unread on reply - {:ok, _} = - CommonAPI.post(user_two, %{ - status: "reply", - visibility: "direct", - in_reply_to_status_id: direct.id - }) - - [%{"unread" => true}] = - conn - |> get("/api/v1/conversations") - |> json_response_and_validate_schema(200) - - assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1 - assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 - - # A reply doesn't increment the user's unread_conversation_count if the conversation is unread - {:ok, _} = - CommonAPI.post(user_two, %{ - status: "reply", - visibility: "direct", - in_reply_to_status_id: direct.id - }) - - assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1 - assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 - end - - test "(vanilla) Mastodon frontend behaviour", %{user: user_one, conn: conn} do - user_two = insert(:user) - {:ok, direct} = create_direct_message(user_one, [user_two]) - - res_conn = get(conn, "/api/v1/statuses/#{direct.id}/context") - - assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200) - end - - defp create_direct_message(sender, recips) do - hellos = - recips - |> Enum.map(fn s -> "@#{s.nickname}" end) - |> Enum.join(", ") - - CommonAPI.post(sender, %{ - status: "Hi #{hellos}!", - visibility: "direct" - }) - end -end diff --git a/test/web/mastodon_api/controllers/custom_emoji_controller_test.exs b/test/web/mastodon_api/controllers/custom_emoji_controller_test.exs @@ -1,23 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.CustomEmojiControllerTest do - use Pleroma.Web.ConnCase, async: true - - test "with tags", %{conn: conn} do - assert resp = - conn - |> get("/api/v1/custom_emojis") - |> json_response_and_validate_schema(200) - - assert [emoji | _body] = resp - assert Map.has_key?(emoji, "shortcode") - assert Map.has_key?(emoji, "static_url") - assert Map.has_key?(emoji, "tags") - assert is_list(emoji["tags"]) - assert Map.has_key?(emoji, "category") - assert Map.has_key?(emoji, "url") - assert Map.has_key?(emoji, "visible_in_picker") - end -end diff --git a/test/web/mastodon_api/controllers/domain_block_controller_test.exs b/test/web/mastodon_api/controllers/domain_block_controller_test.exs @@ -1,79 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.User - - import Pleroma.Factory - - test "blocking / unblocking a domain" do - %{user: user, conn: conn} = oauth_access(["write:blocks"]) - other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"}) - - ret_conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"}) - - assert %{} == json_response_and_validate_schema(ret_conn, 200) - user = User.get_cached_by_ap_id(user.ap_id) - assert User.blocks?(user, other_user) - - ret_conn = - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"}) - - assert %{} == json_response_and_validate_schema(ret_conn, 200) - user = User.get_cached_by_ap_id(user.ap_id) - refute User.blocks?(user, other_user) - end - - test "blocking a domain via query params" do - %{user: user, conn: conn} = oauth_access(["write:blocks"]) - other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"}) - - ret_conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/domain_blocks?domain=dogwhistle.zone") - - assert %{} == json_response_and_validate_schema(ret_conn, 200) - user = User.get_cached_by_ap_id(user.ap_id) - assert User.blocks?(user, other_user) - end - - test "unblocking a domain via query params" do - %{user: user, conn: conn} = oauth_access(["write:blocks"]) - other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"}) - - User.block_domain(user, "dogwhistle.zone") - user = refresh_record(user) - assert User.blocks?(user, other_user) - - ret_conn = - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/v1/domain_blocks?domain=dogwhistle.zone") - - assert %{} == json_response_and_validate_schema(ret_conn, 200) - user = User.get_cached_by_ap_id(user.ap_id) - refute User.blocks?(user, other_user) - end - - test "getting a list of domain blocks" do - %{user: user, conn: conn} = oauth_access(["read:blocks"]) - - {:ok, user} = User.block_domain(user, "bad.site") - {:ok, user} = User.block_domain(user, "even.worse.site") - - assert ["even.worse.site", "bad.site"] == - conn - |> assign(:user, user) - |> get("/api/v1/domain_blocks") - |> json_response_and_validate_schema(200) - end -end diff --git a/test/web/mastodon_api/controllers/filter_controller_test.exs b/test/web/mastodon_api/controllers/filter_controller_test.exs @@ -1,152 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Web.MastodonAPI.FilterView - - test "creating a filter" do - %{conn: conn} = oauth_access(["write:filters"]) - - filter = %Pleroma.Filter{ - phrase: "knights", - context: ["home"] - } - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context}) - - assert response = json_response_and_validate_schema(conn, 200) - assert response["phrase"] == filter.phrase - assert response["context"] == filter.context - assert response["irreversible"] == false - assert response["id"] != nil - assert response["id"] != "" - end - - test "fetching a list of filters" do - %{user: user, conn: conn} = oauth_access(["read:filters"]) - - query_one = %Pleroma.Filter{ - user_id: user.id, - filter_id: 1, - phrase: "knights", - context: ["home"] - } - - query_two = %Pleroma.Filter{ - user_id: user.id, - filter_id: 2, - phrase: "who", - context: ["home"] - } - - {:ok, filter_one} = Pleroma.Filter.create(query_one) - {:ok, filter_two} = Pleroma.Filter.create(query_two) - - response = - conn - |> get("/api/v1/filters") - |> json_response_and_validate_schema(200) - - assert response == - render_json( - FilterView, - "index.json", - filters: [filter_two, filter_one] - ) - end - - test "get a filter" do - %{user: user, conn: conn} = oauth_access(["read:filters"]) - - # check whole_word false - query = %Pleroma.Filter{ - user_id: user.id, - filter_id: 2, - phrase: "knight", - context: ["home"], - whole_word: false - } - - {:ok, filter} = Pleroma.Filter.create(query) - - conn = get(conn, "/api/v1/filters/#{filter.filter_id}") - - assert response = json_response_and_validate_schema(conn, 200) - assert response["whole_word"] == false - - # check whole_word true - %{user: user, conn: conn} = oauth_access(["read:filters"]) - - query = %Pleroma.Filter{ - user_id: user.id, - filter_id: 3, - phrase: "knight", - context: ["home"], - whole_word: true - } - - {:ok, filter} = Pleroma.Filter.create(query) - - conn = get(conn, "/api/v1/filters/#{filter.filter_id}") - - assert response = json_response_and_validate_schema(conn, 200) - assert response["whole_word"] == true - end - - test "update a filter" do - %{user: user, conn: conn} = oauth_access(["write:filters"]) - - query = %Pleroma.Filter{ - user_id: user.id, - filter_id: 2, - phrase: "knight", - context: ["home"], - hide: true, - whole_word: true - } - - {:ok, _filter} = Pleroma.Filter.create(query) - - new = %Pleroma.Filter{ - phrase: "nii", - context: ["home"] - } - - conn = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/v1/filters/#{query.filter_id}", %{ - phrase: new.phrase, - context: new.context - }) - - assert response = json_response_and_validate_schema(conn, 200) - assert response["phrase"] == new.phrase - assert response["context"] == new.context - assert response["irreversible"] == true - assert response["whole_word"] == true - end - - test "delete a filter" do - %{user: user, conn: conn} = oauth_access(["write:filters"]) - - query = %Pleroma.Filter{ - user_id: user.id, - filter_id: 2, - phrase: "knight", - context: ["home"] - } - - {:ok, filter} = Pleroma.Filter.create(query) - - conn = delete(conn, "/api/v1/filters/#{filter.filter_id}") - - assert json_response_and_validate_schema(conn, 200) == %{} - end -end diff --git a/test/web/mastodon_api/controllers/follow_request_controller_test.exs b/test/web/mastodon_api/controllers/follow_request_controller_test.exs @@ -1,74 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "locked accounts" do - setup do - user = insert(:user, locked: true) - %{conn: conn} = oauth_access(["follow"], user: user) - %{user: user, conn: conn} - end - - test "/api/v1/follow_requests works", %{user: user, conn: conn} do - other_user = insert(:user) - - {:ok, _, _, _activity} = CommonAPI.follow(other_user, user) - {:ok, other_user} = User.follow(other_user, user, :follow_pending) - - assert User.following?(other_user, user) == false - - conn = get(conn, "/api/v1/follow_requests") - - assert [relationship] = json_response_and_validate_schema(conn, 200) - assert to_string(other_user.id) == relationship["id"] - end - - test "/api/v1/follow_requests/:id/authorize works", %{user: user, conn: conn} do - other_user = insert(:user) - - {:ok, _, _, _activity} = CommonAPI.follow(other_user, user) - {:ok, other_user} = User.follow(other_user, user, :follow_pending) - - user = User.get_cached_by_id(user.id) - other_user = User.get_cached_by_id(other_user.id) - - assert User.following?(other_user, user) == false - - conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/authorize") - - assert relationship = json_response_and_validate_schema(conn, 200) - assert to_string(other_user.id) == relationship["id"] - - user = User.get_cached_by_id(user.id) - other_user = User.get_cached_by_id(other_user.id) - - assert User.following?(other_user, user) == true - end - - test "/api/v1/follow_requests/:id/reject works", %{user: user, conn: conn} do - other_user = insert(:user) - - {:ok, _, _, _activity} = CommonAPI.follow(other_user, user) - - user = User.get_cached_by_id(user.id) - - conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/reject") - - assert relationship = json_response_and_validate_schema(conn, 200) - assert to_string(other_user.id) == relationship["id"] - - user = User.get_cached_by_id(user.id) - other_user = User.get_cached_by_id(other_user.id) - - assert User.following?(other_user, user) == false - end - end -end diff --git a/test/web/mastodon_api/controllers/instance_controller_test.exs b/test/web/mastodon_api/controllers/instance_controller_test.exs @@ -1,87 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.User - import Pleroma.Factory - - test "get instance information", %{conn: conn} do - conn = get(conn, "/api/v1/instance") - assert result = json_response_and_validate_schema(conn, 200) - - email = Pleroma.Config.get([:instance, :email]) - # Note: not checking for "max_toot_chars" since it's optional - assert %{ - "uri" => _, - "title" => _, - "description" => _, - "version" => _, - "email" => from_config_email, - "urls" => %{ - "streaming_api" => _ - }, - "stats" => _, - "thumbnail" => _, - "languages" => _, - "registrations" => _, - "approval_required" => _, - "poll_limits" => _, - "upload_limit" => _, - "avatar_upload_limit" => _, - "background_upload_limit" => _, - "banner_upload_limit" => _, - "background_image" => _, - "chat_limit" => _, - "description_limit" => _ - } = result - - assert result["pleroma"]["metadata"]["account_activation_required"] != nil - assert result["pleroma"]["metadata"]["features"] - assert result["pleroma"]["metadata"]["federation"] - assert result["pleroma"]["metadata"]["fields_limits"] - assert result["pleroma"]["vapid_public_key"] - - assert email == from_config_email - end - - test "get instance stats", %{conn: conn} do - user = insert(:user, %{local: true}) - - user2 = insert(:user, %{local: true}) - {:ok, _user2} = User.deactivate(user2, !user2.deactivated) - - insert(:user, %{local: false, nickname: "u@peer1.com"}) - insert(:user, %{local: false, nickname: "u@peer2.com"}) - - {:ok, _} = Pleroma.Web.CommonAPI.post(user, %{status: "cofe"}) - - Pleroma.Stats.force_update() - - conn = get(conn, "/api/v1/instance") - - assert result = json_response_and_validate_schema(conn, 200) - - stats = result["stats"] - - assert stats - assert stats["user_count"] == 1 - assert stats["status_count"] == 1 - assert stats["domain_count"] == 2 - end - - test "get peers", %{conn: conn} do - insert(:user, %{local: false, nickname: "u@peer1.com"}) - insert(:user, %{local: false, nickname: "u@peer2.com"}) - - Pleroma.Stats.force_update() - - conn = get(conn, "/api/v1/instance/peers") - - assert result = json_response_and_validate_schema(conn, 200) - - assert ["peer1.com", "peer2.com"] == Enum.sort(result) - end -end diff --git a/test/web/mastodon_api/controllers/list_controller_test.exs b/test/web/mastodon_api/controllers/list_controller_test.exs @@ -1,176 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.ListControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Repo - - import Pleroma.Factory - - test "creating a list" do - %{conn: conn} = oauth_access(["write:lists"]) - - assert %{"title" => "cuties"} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/lists", %{"title" => "cuties"}) - |> json_response_and_validate_schema(:ok) - end - - test "renders error for invalid params" do - %{conn: conn} = oauth_access(["write:lists"]) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/lists", %{"title" => nil}) - - assert %{"error" => "title - null value where string expected."} = - json_response_and_validate_schema(conn, 400) - end - - test "listing a user's lists" do - %{conn: conn} = oauth_access(["read:lists", "write:lists"]) - - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/lists", %{"title" => "cuties"}) - |> json_response_and_validate_schema(:ok) - - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/lists", %{"title" => "cofe"}) - |> json_response_and_validate_schema(:ok) - - conn = get(conn, "/api/v1/lists") - - assert [ - %{"id" => _, "title" => "cofe"}, - %{"id" => _, "title" => "cuties"} - ] = json_response_and_validate_schema(conn, :ok) - end - - test "adding users to a list" do - %{user: user, conn: conn} = oauth_access(["write:lists"]) - other_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) - - assert %{} == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) - |> json_response_and_validate_schema(:ok) - - %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) - assert following == [other_user.follower_address] - end - - test "removing users from a list, body params" do - %{user: user, conn: conn} = oauth_access(["write:lists"]) - other_user = insert(:user) - third_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) - {:ok, list} = Pleroma.List.follow(list, other_user) - {:ok, list} = Pleroma.List.follow(list, third_user) - - assert %{} == - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) - |> json_response_and_validate_schema(:ok) - - %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) - assert following == [third_user.follower_address] - end - - test "removing users from a list, query params" do - %{user: user, conn: conn} = oauth_access(["write:lists"]) - other_user = insert(:user) - third_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) - {:ok, list} = Pleroma.List.follow(list, other_user) - {:ok, list} = Pleroma.List.follow(list, third_user) - - assert %{} == - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/v1/lists/#{list.id}/accounts?account_ids[]=#{other_user.id}") - |> json_response_and_validate_schema(:ok) - - %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) - assert following == [third_user.follower_address] - end - - test "listing users in a list" do - %{user: user, conn: conn} = oauth_access(["read:lists"]) - other_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) - {:ok, list} = Pleroma.List.follow(list, other_user) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) - - assert [%{"id" => id}] = json_response_and_validate_schema(conn, 200) - assert id == to_string(other_user.id) - end - - test "retrieving a list" do - %{user: user, conn: conn} = oauth_access(["read:lists"]) - {:ok, list} = Pleroma.List.create("name", user) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/lists/#{list.id}") - - assert %{"id" => id} = json_response_and_validate_schema(conn, 200) - assert id == to_string(list.id) - end - - test "renders 404 if list is not found" do - %{conn: conn} = oauth_access(["read:lists"]) - - conn = get(conn, "/api/v1/lists/666") - - assert %{"error" => "List not found"} = json_response_and_validate_schema(conn, :not_found) - end - - test "renaming a list" do - %{user: user, conn: conn} = oauth_access(["write:lists"]) - {:ok, list} = Pleroma.List.create("name", user) - - assert %{"title" => "newname"} = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/v1/lists/#{list.id}", %{"title" => "newname"}) - |> json_response_and_validate_schema(:ok) - end - - test "validates title when renaming a list" do - %{user: user, conn: conn} = oauth_access(["write:lists"]) - {:ok, list} = Pleroma.List.create("name", user) - - conn = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/json") - |> put("/api/v1/lists/#{list.id}", %{"title" => " "}) - - assert %{"error" => "can't be blank"} == - json_response_and_validate_schema(conn, :unprocessable_entity) - end - - test "deleting a list" do - %{user: user, conn: conn} = oauth_access(["write:lists"]) - {:ok, list} = Pleroma.List.create("name", user) - - conn = delete(conn, "/api/v1/lists/#{list.id}") - - assert %{} = json_response_and_validate_schema(conn, 200) - assert is_nil(Repo.get(Pleroma.List, list.id)) - end -end diff --git a/test/web/mastodon_api/controllers/marker_controller_test.exs b/test/web/mastodon_api/controllers/marker_controller_test.exs @@ -1,131 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - describe "GET /api/v1/markers" do - test "gets markers with correct scopes", %{conn: conn} do - user = insert(:user) - token = insert(:oauth_token, user: user, scopes: ["read:statuses"]) - insert_list(7, :notification, user: user, activity: insert(:note_activity)) - - {:ok, %{"notifications" => marker}} = - Pleroma.Marker.upsert( - user, - %{"notifications" => %{"last_read_id" => "69420"}} - ) - - response = - conn - |> assign(:user, user) - |> assign(:token, token) - |> get("/api/v1/markers?timeline[]=notifications") - |> json_response_and_validate_schema(200) - - assert response == %{ - "notifications" => %{ - "last_read_id" => "69420", - "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), - "version" => 0, - "pleroma" => %{"unread_count" => 7} - } - } - end - - test "gets markers with missed scopes", %{conn: conn} do - user = insert(:user) - token = insert(:oauth_token, user: user, scopes: []) - - Pleroma.Marker.upsert(user, %{"notifications" => %{"last_read_id" => "69420"}}) - - response = - conn - |> assign(:user, user) - |> assign(:token, token) - |> get("/api/v1/markers", %{timeline: ["notifications"]}) - |> json_response_and_validate_schema(403) - - assert response == %{"error" => "Insufficient permissions: read:statuses."} - end - end - - describe "POST /api/v1/markers" do - test "creates a marker with correct scopes", %{conn: conn} do - user = insert(:user) - token = insert(:oauth_token, user: user, scopes: ["write:statuses"]) - - response = - conn - |> assign(:user, user) - |> assign(:token, token) - |> put_req_header("content-type", "application/json") - |> post("/api/v1/markers", %{ - home: %{last_read_id: "777"}, - notifications: %{"last_read_id" => "69420"} - }) - |> json_response_and_validate_schema(200) - - assert %{ - "notifications" => %{ - "last_read_id" => "69420", - "updated_at" => _, - "version" => 0, - "pleroma" => %{"unread_count" => 0} - } - } = response - end - - test "updates exist marker", %{conn: conn} do - user = insert(:user) - token = insert(:oauth_token, user: user, scopes: ["write:statuses"]) - - {:ok, %{"notifications" => marker}} = - Pleroma.Marker.upsert( - user, - %{"notifications" => %{"last_read_id" => "69477"}} - ) - - response = - conn - |> assign(:user, user) - |> assign(:token, token) - |> put_req_header("content-type", "application/json") - |> post("/api/v1/markers", %{ - home: %{last_read_id: "777"}, - notifications: %{"last_read_id" => "69888"} - }) - |> json_response_and_validate_schema(200) - - assert response == %{ - "notifications" => %{ - "last_read_id" => "69888", - "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), - "version" => 0, - "pleroma" => %{"unread_count" => 0} - } - } - end - - test "creates a marker with missed scopes", %{conn: conn} do - user = insert(:user) - token = insert(:oauth_token, user: user, scopes: []) - - response = - conn - |> assign(:user, user) - |> assign(:token, token) - |> put_req_header("content-type", "application/json") - |> post("/api/v1/markers", %{ - home: %{last_read_id: "777"}, - notifications: %{"last_read_id" => "69420"} - }) - |> json_response_and_validate_schema(403) - - assert response == %{"error" => "Insufficient permissions: write:statuses."} - end - end -end diff --git a/test/web/mastodon_api/controllers/media_controller_test.exs b/test/web/mastodon_api/controllers/media_controller_test.exs @@ -1,146 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - - describe "Upload media" do - setup do: oauth_access(["write:media"]) - - setup do - image = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - [image: image] - end - - setup do: clear_config([:media_proxy]) - setup do: clear_config([Pleroma.Upload]) - - test "/api/v1/media", %{conn: conn, image: image} do - 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) - - assert media["type"] == "image" - assert media["description"] == desc - assert media["id"] - - object = Object.get_by_id(media["id"]) - assert object.data["actor"] == User.ap_id(conn.assigns[:user]) - end - - test "/api/v2/media", %{conn: conn, user: user, image: image} do - desc = "Description of the image" - - response = - conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/v2/media", %{"file" => image, "description" => desc}) - |> json_response_and_validate_schema(202) - - assert media_id = response["id"] - - %{conn: conn} = oauth_access(["read:media"], user: user) - - media = - conn - |> get("/api/v1/media/#{media_id}") - |> json_response_and_validate_schema(200) - - assert media["type"] == "image" - assert media["description"] == desc - assert media["id"] - - object = Object.get_by_id(media["id"]) - assert object.data["actor"] == user.ap_id - end - end - - describe "Update media description" do - setup do: oauth_access(["write:media"]) - - setup %{user: actor} do - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, %Object{} = object} = - ActivityPub.upload( - file, - actor: User.ap_id(actor), - description: "test-m" - ) - - [object: object] - end - - test "/api/v1/media/:id good request", %{conn: conn, object: object} do - media = - conn - |> put_req_header("content-type", "multipart/form-data") - |> put("/api/v1/media/#{object.id}", %{"description" => "test-media"}) - |> json_response_and_validate_schema(:ok) - - assert media["description"] == "test-media" - assert refresh_record(object).data["name"] == "test-media" - end - end - - describe "Get media by id (/api/v1/media/:id)" do - setup do: oauth_access(["read:media"]) - - setup %{user: actor} do - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, %Object{} = object} = - ActivityPub.upload( - file, - actor: User.ap_id(actor), - description: "test-media" - ) - - [object: object] - end - - test "it returns media object when requested by owner", %{conn: conn, object: object} do - media = - conn - |> get("/api/v1/media/#{object.id}") - |> json_response_and_validate_schema(:ok) - - assert media["description"] == "test-media" - assert media["type"] == "image" - assert media["id"] - end - - test "it returns 403 if media object requested by non-owner", %{object: object, user: user} do - %{conn: conn, user: other_user} = oauth_access(["read:media"]) - - assert object.data["actor"] == user.ap_id - refute user.id == other_user.id - - conn - |> get("/api/v1/media/#{object.id}") - |> json_response(403) - end - end -end diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs @@ -1,626 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Notification - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "does NOT render account/pleroma/relationship by default" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - {:ok, [_notification]} = Notification.create_notifications(activity) - - response = - conn - |> assign(:user, user) - |> get("/api/v1/notifications") - |> json_response_and_validate_schema(200) - - assert Enum.all?(response, fn n -> - get_in(n, ["account", "pleroma", "relationship"]) == %{} - end) - end - - test "list of notifications" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - - {:ok, [_notification]} = Notification.create_notifications(activity) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/notifications") - - expected_response = - "hi <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{user.id}\" href=\"#{ - user.ap_id - }\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>" - - assert [%{"status" => %{"content" => response}} | _rest] = - json_response_and_validate_schema(conn, 200) - - assert response == expected_response - end - - test "by default, does not contain pleroma:chat_mention" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, _activity} = CommonAPI.post_chat_message(other_user, user, "hey") - - result = - conn - |> get("/api/v1/notifications") - |> json_response_and_validate_schema(200) - - assert [] == result - - result = - conn - |> get("/api/v1/notifications?include_types[]=pleroma:chat_mention") - |> json_response_and_validate_schema(200) - - assert [_] = result - end - - test "getting a single notification" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - - {:ok, [notification]} = Notification.create_notifications(activity) - - conn = get(conn, "/api/v1/notifications/#{notification.id}") - - expected_response = - "hi <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{user.id}\" href=\"#{ - user.ap_id - }\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>" - - assert %{"status" => %{"content" => response}} = json_response_and_validate_schema(conn, 200) - assert response == expected_response - end - - test "dismissing a single notification (deprecated endpoint)" do - %{user: user, conn: conn} = oauth_access(["write:notifications"]) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - - {:ok, [notification]} = Notification.create_notifications(activity) - - conn = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/json") - |> post("/api/v1/notifications/dismiss", %{"id" => to_string(notification.id)}) - - assert %{} = json_response_and_validate_schema(conn, 200) - end - - test "dismissing a single notification" do - %{user: user, conn: conn} = oauth_access(["write:notifications"]) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - - {:ok, [notification]} = Notification.create_notifications(activity) - - conn = - conn - |> assign(:user, user) - |> post("/api/v1/notifications/#{notification.id}/dismiss") - - assert %{} = json_response_and_validate_schema(conn, 200) - end - - test "clearing all notifications" do - %{user: user, conn: conn} = oauth_access(["write:notifications", "read:notifications"]) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - - {:ok, [_notification]} = Notification.create_notifications(activity) - - ret_conn = post(conn, "/api/v1/notifications/clear") - - assert %{} = json_response_and_validate_schema(ret_conn, 200) - - ret_conn = get(conn, "/api/v1/notifications") - - assert all = json_response_and_validate_schema(ret_conn, 200) - assert all == [] - end - - test "paginates notifications using min_id, since_id, max_id, and limit" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, activity1} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - {:ok, activity2} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - {:ok, activity3} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - {:ok, activity4} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - - notification1_id = get_notification_id_by_activity(activity1) - notification2_id = get_notification_id_by_activity(activity2) - notification3_id = get_notification_id_by_activity(activity3) - notification4_id = get_notification_id_by_activity(activity4) - - conn = assign(conn, :user, user) - - # min_id - result = - conn - |> get("/api/v1/notifications?limit=2&min_id=#{notification1_id}") - |> json_response_and_validate_schema(:ok) - - assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result - - # since_id - result = - conn - |> get("/api/v1/notifications?limit=2&since_id=#{notification1_id}") - |> json_response_and_validate_schema(:ok) - - assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result - - # max_id - result = - conn - |> get("/api/v1/notifications?limit=2&max_id=#{notification4_id}") - |> json_response_and_validate_schema(:ok) - - assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result - end - - describe "exclude_visibilities" do - test "filters notifications for mentions" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, public_activity} = - CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "public"}) - - {:ok, direct_activity} = - CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "direct"}) - - {:ok, unlisted_activity} = - CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "unlisted"}) - - {:ok, private_activity} = - CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "private"}) - - query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "private"]}) - conn_res = get(conn, "/api/v1/notifications?" <> query) - - assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200) - assert id == direct_activity.id - - query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "direct"]}) - conn_res = get(conn, "/api/v1/notifications?" <> query) - - assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200) - assert id == private_activity.id - - query = params_to_query(%{exclude_visibilities: ["public", "private", "direct"]}) - conn_res = get(conn, "/api/v1/notifications?" <> query) - - assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200) - assert id == unlisted_activity.id - - query = params_to_query(%{exclude_visibilities: ["unlisted", "private", "direct"]}) - conn_res = get(conn, "/api/v1/notifications?" <> query) - - assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200) - assert id == public_activity.id - end - - test "filters notifications for Like activities" do - user = insert(:user) - %{user: other_user, conn: conn} = oauth_access(["read:notifications"]) - - {:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"}) - - {:ok, direct_activity} = - CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "direct"}) - - {:ok, unlisted_activity} = - CommonAPI.post(other_user, %{status: ".", visibility: "unlisted"}) - - {:ok, private_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "private"}) - - {:ok, _} = CommonAPI.favorite(user, public_activity.id) - {:ok, _} = CommonAPI.favorite(user, direct_activity.id) - {:ok, _} = CommonAPI.favorite(user, unlisted_activity.id) - {:ok, _} = CommonAPI.favorite(user, private_activity.id) - - activity_ids = - conn - |> get("/api/v1/notifications?exclude_visibilities[]=direct") - |> json_response_and_validate_schema(200) - |> Enum.map(& &1["status"]["id"]) - - assert public_activity.id in activity_ids - assert unlisted_activity.id in activity_ids - assert private_activity.id in activity_ids - refute direct_activity.id in activity_ids - - activity_ids = - conn - |> get("/api/v1/notifications?exclude_visibilities[]=unlisted") - |> json_response_and_validate_schema(200) - |> Enum.map(& &1["status"]["id"]) - - assert public_activity.id in activity_ids - refute unlisted_activity.id in activity_ids - assert private_activity.id in activity_ids - assert direct_activity.id in activity_ids - - activity_ids = - conn - |> get("/api/v1/notifications?exclude_visibilities[]=private") - |> json_response_and_validate_schema(200) - |> Enum.map(& &1["status"]["id"]) - - assert public_activity.id in activity_ids - assert unlisted_activity.id in activity_ids - refute private_activity.id in activity_ids - assert direct_activity.id in activity_ids - - activity_ids = - conn - |> get("/api/v1/notifications?exclude_visibilities[]=public") - |> json_response_and_validate_schema(200) - |> Enum.map(& &1["status"]["id"]) - - refute public_activity.id in activity_ids - assert unlisted_activity.id in activity_ids - assert private_activity.id in activity_ids - assert direct_activity.id in activity_ids - end - - test "filters notifications for Announce activities" do - user = insert(:user) - %{user: other_user, conn: conn} = oauth_access(["read:notifications"]) - - {:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"}) - - {:ok, unlisted_activity} = - CommonAPI.post(other_user, %{status: ".", visibility: "unlisted"}) - - {:ok, _} = CommonAPI.repeat(public_activity.id, user) - {:ok, _} = CommonAPI.repeat(unlisted_activity.id, user) - - activity_ids = - conn - |> get("/api/v1/notifications?exclude_visibilities[]=unlisted") - |> json_response_and_validate_schema(200) - |> Enum.map(& &1["status"]["id"]) - - assert public_activity.id in activity_ids - refute unlisted_activity.id in activity_ids - end - - test "doesn't return less than the requested amount of records when the user's reply is liked" do - user = insert(:user) - %{user: other_user, conn: conn} = oauth_access(["read:notifications"]) - - {:ok, mention} = - CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "public"}) - - {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "public"}) - - {:ok, reply} = - CommonAPI.post(other_user, %{ - status: ".", - visibility: "public", - in_reply_to_status_id: activity.id - }) - - {:ok, _favorite} = CommonAPI.favorite(user, reply.id) - - activity_ids = - conn - |> get("/api/v1/notifications?exclude_visibilities[]=direct&limit=2") - |> json_response_and_validate_schema(200) - |> Enum.map(& &1["status"]["id"]) - - assert [reply.id, mention.id] == activity_ids - end - end - - test "filters notifications using exclude_types" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, mention_activity} = CommonAPI.post(other_user, %{status: "hey @#{user.nickname}"}) - {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, favorite_activity} = CommonAPI.favorite(other_user, create_activity.id) - {:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, other_user) - {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user) - - mention_notification_id = get_notification_id_by_activity(mention_activity) - favorite_notification_id = get_notification_id_by_activity(favorite_activity) - reblog_notification_id = get_notification_id_by_activity(reblog_activity) - follow_notification_id = get_notification_id_by_activity(follow_activity) - - query = params_to_query(%{exclude_types: ["mention", "favourite", "reblog"]}) - conn_res = get(conn, "/api/v1/notifications?" <> query) - - assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200) - - query = params_to_query(%{exclude_types: ["favourite", "reblog", "follow"]}) - conn_res = get(conn, "/api/v1/notifications?" <> query) - - assert [%{"id" => ^mention_notification_id}] = - json_response_and_validate_schema(conn_res, 200) - - query = params_to_query(%{exclude_types: ["reblog", "follow", "mention"]}) - conn_res = get(conn, "/api/v1/notifications?" <> query) - - assert [%{"id" => ^favorite_notification_id}] = - json_response_and_validate_schema(conn_res, 200) - - query = params_to_query(%{exclude_types: ["follow", "mention", "favourite"]}) - conn_res = get(conn, "/api/v1/notifications?" <> query) - - assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200) - end - - test "filters notifications using include_types" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, mention_activity} = CommonAPI.post(other_user, %{status: "hey @#{user.nickname}"}) - {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, favorite_activity} = CommonAPI.favorite(other_user, create_activity.id) - {:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, other_user) - {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user) - - mention_notification_id = get_notification_id_by_activity(mention_activity) - favorite_notification_id = get_notification_id_by_activity(favorite_activity) - 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") - - assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200) - - conn_res = get(conn, "/api/v1/notifications?include_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") - - assert [%{"id" => ^favorite_notification_id}] = - json_response_and_validate_schema(conn_res, 200) - - conn_res = get(conn, "/api/v1/notifications?include_types[]=reblog") - - assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200) - - result = conn |> get("/api/v1/notifications") |> json_response_and_validate_schema(200) - - assert length(result) == 4 - - query = params_to_query(%{include_types: ["follow", "mention", "favourite", "reblog"]}) - - result = - conn - |> get("/api/v1/notifications?" <> query) - |> json_response_and_validate_schema(200) - - assert length(result) == 4 - end - - test "destroy multiple" do - %{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"]) - other_user = insert(:user) - - {:ok, activity1} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - {:ok, activity2} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - {:ok, activity3} = CommonAPI.post(user, %{status: "hi @#{other_user.nickname}"}) - {:ok, activity4} = CommonAPI.post(user, %{status: "hi @#{other_user.nickname}"}) - - notification1_id = get_notification_id_by_activity(activity1) - notification2_id = get_notification_id_by_activity(activity2) - notification3_id = get_notification_id_by_activity(activity3) - notification4_id = get_notification_id_by_activity(activity4) - - result = - conn - |> get("/api/v1/notifications") - |> json_response_and_validate_schema(:ok) - - assert [%{"id" => ^notification2_id}, %{"id" => ^notification1_id}] = result - - conn2 = - conn - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:notifications"])) - - result = - conn2 - |> get("/api/v1/notifications") - |> json_response_and_validate_schema(:ok) - - assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result - - query = params_to_query(%{ids: [notification1_id, notification2_id]}) - conn_destroy = delete(conn, "/api/v1/notifications/destroy_multiple?" <> query) - - assert json_response_and_validate_schema(conn_destroy, 200) == %{} - - result = - conn2 - |> get("/api/v1/notifications") - |> json_response_and_validate_schema(:ok) - - assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result - end - - test "doesn't see notifications after muting user with notifications" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - user2 = insert(:user) - - {:ok, _, _, _} = CommonAPI.follow(user, user2) - {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"}) - - ret_conn = get(conn, "/api/v1/notifications") - - assert length(json_response_and_validate_schema(ret_conn, 200)) == 1 - - {:ok, _user_relationships} = User.mute(user, user2) - - conn = get(conn, "/api/v1/notifications") - - assert json_response_and_validate_schema(conn, 200) == [] - end - - test "see notifications after muting user without notifications" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - user2 = insert(:user) - - {:ok, _, _, _} = CommonAPI.follow(user, user2) - {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"}) - - ret_conn = get(conn, "/api/v1/notifications") - - assert length(json_response_and_validate_schema(ret_conn, 200)) == 1 - - {:ok, _user_relationships} = User.mute(user, user2, false) - - conn = get(conn, "/api/v1/notifications") - - assert length(json_response_and_validate_schema(conn, 200)) == 1 - end - - test "see notifications after muting user with notifications and with_muted parameter" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - user2 = insert(:user) - - {:ok, _, _, _} = CommonAPI.follow(user, user2) - {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"}) - - ret_conn = get(conn, "/api/v1/notifications") - - assert length(json_response_and_validate_schema(ret_conn, 200)) == 1 - - {:ok, _user_relationships} = User.mute(user, user2) - - conn = get(conn, "/api/v1/notifications?with_muted=true") - - assert length(json_response_and_validate_schema(conn, 200)) == 1 - end - - @tag capture_log: true - test "see move notifications" do - old_user = insert(:user) - new_user = insert(:user, also_known_as: [old_user.ap_id]) - %{user: follower, conn: conn} = oauth_access(["read:notifications"]) - - old_user_url = old_user.ap_id - - body = - File.read!("test/fixtures/users_mock/localhost.json") - |> String.replace("{{nickname}}", old_user.nickname) - |> Jason.encode!() - - Tesla.Mock.mock(fn - %{method: :get, url: ^old_user_url} -> - %Tesla.Env{status: 200, body: body} - end) - - User.follow(follower, old_user) - Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) - Pleroma.Tests.ObanHelpers.perform_all() - - conn = get(conn, "/api/v1/notifications") - - assert length(json_response_and_validate_schema(conn, 200)) == 1 - end - - describe "link headers" do - test "preserves parameters in link headers" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, activity1} = - CommonAPI.post(other_user, %{ - status: "hi @#{user.nickname}", - visibility: "public" - }) - - {:ok, activity2} = - CommonAPI.post(other_user, %{ - status: "hi @#{user.nickname}", - visibility: "public" - }) - - notification1 = Repo.get_by(Notification, activity_id: activity1.id) - notification2 = Repo.get_by(Notification, activity_id: activity2.id) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/notifications?limit=5") - - assert [link_header] = get_resp_header(conn, "link") - assert link_header =~ ~r/limit=5/ - assert link_header =~ ~r/min_id=#{notification2.id}/ - assert link_header =~ ~r/max_id=#{notification1.id}/ - end - end - - describe "from specified user" do - test "account_id" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - - %{id: account_id} = other_user1 = insert(:user) - other_user2 = insert(:user) - - {:ok, _activity} = CommonAPI.post(other_user1, %{status: "hi @#{user.nickname}"}) - {:ok, _activity} = CommonAPI.post(other_user2, %{status: "bye @#{user.nickname}"}) - - assert [%{"account" => %{"id" => ^account_id}}] = - conn - |> assign(:user, user) - |> get("/api/v1/notifications?account_id=#{account_id}") - |> json_response_and_validate_schema(200) - - assert %{"error" => "Account is not found"} = - conn - |> assign(:user, user) - |> get("/api/v1/notifications?account_id=cofe") - |> json_response_and_validate_schema(404) - end - end - - defp get_notification_id_by_activity(%{id: id}) do - Notification - |> Repo.get_by(activity_id: id) - |> Map.get(:id) - |> to_string() - end - - defp params_to_query(%{} = params) do - Enum.map_join(params, "&", fn - {k, v} when is_list(v) -> Enum.map_join(v, "&", &"#{k}[]=#{&1}") - {k, v} -> k <> "=" <> v - end) - end -end diff --git a/test/web/mastodon_api/controllers/poll_controller_test.exs b/test/web/mastodon_api/controllers/poll_controller_test.exs @@ -1,171 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.PollControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Object - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "GET /api/v1/polls/:id" do - setup do: oauth_access(["read:statuses"]) - - test "returns poll entity for object id", %{user: user, conn: conn} do - {:ok, activity} = - CommonAPI.post(user, %{ - status: "Pleroma does", - poll: %{options: ["what Mastodon't", "n't what Mastodoes"], expires_in: 20} - }) - - object = Object.normalize(activity) - - conn = get(conn, "/api/v1/polls/#{object.id}") - - response = json_response_and_validate_schema(conn, 200) - id = to_string(object.id) - assert %{"id" => ^id, "expired" => false, "multiple" => false} = response - end - - test "does not expose polls for private statuses", %{conn: conn} do - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(other_user, %{ - status: "Pleroma does", - poll: %{options: ["what Mastodon't", "n't what Mastodoes"], expires_in: 20}, - visibility: "private" - }) - - object = Object.normalize(activity) - - conn = get(conn, "/api/v1/polls/#{object.id}") - - assert json_response_and_validate_schema(conn, 404) - end - end - - describe "POST /api/v1/polls/:id/votes" do - setup do: oauth_access(["write:statuses"]) - - test "votes are added to the poll", %{conn: conn} do - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(other_user, %{ - status: "A very delicious sandwich", - poll: %{ - options: ["Lettuce", "Grilled Bacon", "Tomato"], - expires_in: 20, - multiple: true - } - }) - - object = Object.normalize(activity) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]}) - - assert json_response_and_validate_schema(conn, 200) - object = Object.get_by_id(object.id) - - assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => total_items}} -> - total_items == 1 - end) - end - - test "author can't vote", %{user: user, conn: conn} do - {:ok, activity} = - CommonAPI.post(user, %{ - status: "Am I cute?", - poll: %{options: ["Yes", "No"], expires_in: 20} - }) - - object = Object.normalize(activity) - - assert conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]}) - |> json_response_and_validate_schema(422) == %{"error" => "Poll's author can't vote"} - - object = Object.get_by_id(object.id) - - refute Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 1 - end - - test "does not allow multiple choices on a single-choice question", %{conn: conn} do - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(other_user, %{ - status: "The glass is", - poll: %{options: ["half empty", "half full"], expires_in: 20} - }) - - object = Object.normalize(activity) - - assert conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]}) - |> json_response_and_validate_schema(422) == %{"error" => "Too many choices"} - - object = Object.get_by_id(object.id) - - refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => total_items}} -> - total_items == 1 - end) - end - - test "does not allow choice index to be greater than options count", %{conn: conn} do - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(other_user, %{ - status: "Am I cute?", - poll: %{options: ["Yes", "No"], expires_in: 20} - }) - - object = Object.normalize(activity) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [2]}) - - assert json_response_and_validate_schema(conn, 422) == %{"error" => "Invalid indices"} - end - - test "returns 404 error when object is not exist", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/polls/1/votes", %{"choices" => [0]}) - - assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} - end - - test "returns 404 when poll is private and not available for user", %{conn: conn} do - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(other_user, %{ - status: "Am I cute?", - poll: %{options: ["Yes", "No"], expires_in: 20}, - visibility: "private" - }) - - object = Object.normalize(activity) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0]}) - - assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} - end - end -end diff --git a/test/web/mastodon_api/controllers/report_controller_test.exs b/test/web/mastodon_api/controllers/report_controller_test.exs @@ -1,95 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - setup do: oauth_access(["write:reports"]) - - setup do - target_user = insert(:user) - - {:ok, activity} = CommonAPI.post(target_user, %{status: "foobar"}) - - [target_user: target_user, activity: activity] - end - - test "submit a basic report", %{conn: conn, target_user: target_user} do - assert %{"action_taken" => false, "id" => _} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/reports", %{"account_id" => target_user.id}) - |> json_response_and_validate_schema(200) - end - - test "submit a report with statuses and comment", %{ - conn: conn, - target_user: target_user, - activity: activity - } do - assert %{"action_taken" => false, "id" => _} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/reports", %{ - "account_id" => target_user.id, - "status_ids" => [activity.id], - "comment" => "bad status!", - "forward" => "false" - }) - |> json_response_and_validate_schema(200) - end - - test "account_id is required", %{ - conn: conn, - activity: activity - } do - assert %{"error" => "Missing field: account_id."} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/reports", %{"status_ids" => [activity.id]}) - |> json_response_and_validate_schema(400) - end - - test "comment must be up to the size specified in the config", %{ - conn: conn, - target_user: target_user - } do - max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000) - comment = String.pad_trailing("a", max_size + 1, "a") - - error = %{"error" => "Comment must be up to #{max_size} characters"} - - assert ^error = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment}) - |> json_response_and_validate_schema(400) - end - - test "returns error when account is not exist", %{ - conn: conn, - activity: activity - } do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/reports", %{"status_ids" => [activity.id], "account_id" => "foo"}) - - assert json_response_and_validate_schema(conn, 400) == %{"error" => "Account not found"} - end - - test "doesn't fail if an admin has no email", %{conn: conn, target_user: target_user} do - insert(:user, %{is_admin: true, email: nil}) - - assert %{"action_taken" => false, "id" => _} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/reports", %{"account_id" => target_user.id}) - |> json_response_and_validate_schema(200) - end -end diff --git a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs @@ -1,139 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Repo - alias Pleroma.ScheduledActivity - - import Pleroma.Factory - import Ecto.Query - - setup do: clear_config([ScheduledActivity, :enabled]) - - test "shows scheduled activities" do - %{user: user, conn: conn} = oauth_access(["read:statuses"]) - - scheduled_activity_id1 = insert(:scheduled_activity, user: user).id |> to_string() - scheduled_activity_id2 = insert(:scheduled_activity, user: user).id |> to_string() - scheduled_activity_id3 = insert(:scheduled_activity, user: user).id |> to_string() - scheduled_activity_id4 = insert(:scheduled_activity, user: user).id |> to_string() - - # min_id - conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}") - - result = json_response_and_validate_schema(conn_res, 200) - assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result - - # since_id - conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}") - - result = json_response_and_validate_schema(conn_res, 200) - assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result - - # max_id - conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}") - - result = json_response_and_validate_schema(conn_res, 200) - assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result - end - - test "shows a scheduled activity" do - %{user: user, conn: conn} = oauth_access(["read:statuses"]) - scheduled_activity = insert(:scheduled_activity, user: user) - - res_conn = get(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}") - - assert %{"id" => scheduled_activity_id} = json_response_and_validate_schema(res_conn, 200) - assert scheduled_activity_id == scheduled_activity.id |> to_string() - - res_conn = get(conn, "/api/v1/scheduled_statuses/404") - - assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404) - end - - test "updates a scheduled activity" do - Pleroma.Config.put([ScheduledActivity, :enabled], true) - %{user: user, conn: conn} = oauth_access(["write:statuses"]) - - scheduled_at = Timex.shift(NaiveDateTime.utc_now(), minutes: 60) - - {:ok, scheduled_activity} = - ScheduledActivity.create( - user, - %{ - scheduled_at: scheduled_at, - params: build(:note).data - } - ) - - job = Repo.one(from(j in Oban.Job, where: j.queue == "scheduled_activities")) - - assert job.args == %{"activity_id" => scheduled_activity.id} - assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(scheduled_at) - - new_scheduled_at = - NaiveDateTime.utc_now() - |> Timex.shift(minutes: 120) - |> Timex.format!("%Y-%m-%dT%H:%M:%S.%fZ", :strftime) - - res_conn = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{ - scheduled_at: new_scheduled_at - }) - - assert %{"scheduled_at" => expected_scheduled_at} = - json_response_and_validate_schema(res_conn, 200) - - assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at) - job = refresh_record(job) - - assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(new_scheduled_at) - - res_conn = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at}) - - assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404) - end - - test "deletes a scheduled activity" do - Pleroma.Config.put([ScheduledActivity, :enabled], true) - %{user: user, conn: conn} = oauth_access(["write:statuses"]) - scheduled_at = Timex.shift(NaiveDateTime.utc_now(), minutes: 60) - - {:ok, scheduled_activity} = - ScheduledActivity.create( - user, - %{ - scheduled_at: scheduled_at, - params: build(:note).data - } - ) - - job = Repo.one(from(j in Oban.Job, where: j.queue == "scheduled_activities")) - - assert job.args == %{"activity_id" => scheduled_activity.id} - - res_conn = - conn - |> assign(:user, user) - |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}") - - assert %{} = json_response_and_validate_schema(res_conn, 200) - refute Repo.get(ScheduledActivity, scheduled_activity.id) - refute Repo.get(Oban.Job, job.id) - - res_conn = - conn - |> assign(:user, user) - |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}") - - assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404) - end -end diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs @@ -1,413 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Object - alias Pleroma.Web - alias Pleroma.Web.CommonAPI - import Pleroma.Factory - import ExUnit.CaptureLog - import Tesla.Mock - import Mock - - setup_all do - mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe ".search2" do - test "it returns empty result if user or status search return undefined error", %{conn: conn} do - with_mocks [ - {Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]}, - {Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]} - ] do - capture_log(fn -> - results = - conn - |> get("/api/v2/search?q=2hu") - |> json_response_and_validate_schema(200) - - assert results["accounts"] == [] - assert results["statuses"] == [] - end) =~ - "[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}" - end - end - - test "search", %{conn: conn} do - user = insert(:user) - user_two = insert(:user, %{nickname: "shp@shitposter.club"}) - user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - {:ok, activity} = CommonAPI.post(user, %{status: "This is about 2hu private 天子"}) - - {:ok, _activity} = - CommonAPI.post(user, %{ - status: "This is about 2hu, but private", - visibility: "private" - }) - - {:ok, _} = CommonAPI.post(user_two, %{status: "This isn't"}) - - results = - conn - |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu #private"})}") - |> json_response_and_validate_schema(200) - - [account | _] = results["accounts"] - assert account["id"] == to_string(user_three.id) - - assert results["hashtags"] == [ - %{"name" => "private", "url" => "#{Web.base_url()}/tag/private"} - ] - - [status] = results["statuses"] - assert status["id"] == to_string(activity.id) - - results = - get(conn, "/api/v2/search?q=天子") - |> json_response_and_validate_schema(200) - - assert results["hashtags"] == [ - %{"name" => "天子", "url" => "#{Web.base_url()}/tag/天子"} - ] - - [status] = results["statuses"] - assert status["id"] == to_string(activity.id) - end - - @tag capture_log: true - test "constructs hashtags from search query", %{conn: conn} do - results = - conn - |> get("/api/v2/search?#{URI.encode_query(%{q: "some text with #explicit #hashtags"})}") - |> json_response_and_validate_schema(200) - - assert results["hashtags"] == [ - %{"name" => "explicit", "url" => "#{Web.base_url()}/tag/explicit"}, - %{"name" => "hashtags", "url" => "#{Web.base_url()}/tag/hashtags"} - ] - - results = - conn - |> get("/api/v2/search?#{URI.encode_query(%{q: "john doe JOHN DOE"})}") - |> json_response_and_validate_schema(200) - - assert results["hashtags"] == [ - %{"name" => "john", "url" => "#{Web.base_url()}/tag/john"}, - %{"name" => "doe", "url" => "#{Web.base_url()}/tag/doe"}, - %{"name" => "JohnDoe", "url" => "#{Web.base_url()}/tag/JohnDoe"} - ] - - results = - conn - |> get("/api/v2/search?#{URI.encode_query(%{q: "accident-prone"})}") - |> json_response_and_validate_schema(200) - - assert results["hashtags"] == [ - %{"name" => "accident", "url" => "#{Web.base_url()}/tag/accident"}, - %{"name" => "prone", "url" => "#{Web.base_url()}/tag/prone"}, - %{"name" => "AccidentProne", "url" => "#{Web.base_url()}/tag/AccidentProne"} - ] - - results = - conn - |> get("/api/v2/search?#{URI.encode_query(%{q: "https://shpposter.club/users/shpuld"})}") - |> json_response_and_validate_schema(200) - - assert results["hashtags"] == [ - %{"name" => "shpuld", "url" => "#{Web.base_url()}/tag/shpuld"} - ] - - results = - conn - |> get( - "/api/v2/search?#{ - URI.encode_query(%{ - q: - "https://www.washingtonpost.com/sports/2020/06/10/" <> - "nascar-ban-display-confederate-flag-all-events-properties/" - }) - }" - ) - |> json_response_and_validate_schema(200) - - assert results["hashtags"] == [ - %{"name" => "nascar", "url" => "#{Web.base_url()}/tag/nascar"}, - %{"name" => "ban", "url" => "#{Web.base_url()}/tag/ban"}, - %{"name" => "display", "url" => "#{Web.base_url()}/tag/display"}, - %{"name" => "confederate", "url" => "#{Web.base_url()}/tag/confederate"}, - %{"name" => "flag", "url" => "#{Web.base_url()}/tag/flag"}, - %{"name" => "all", "url" => "#{Web.base_url()}/tag/all"}, - %{"name" => "events", "url" => "#{Web.base_url()}/tag/events"}, - %{"name" => "properties", "url" => "#{Web.base_url()}/tag/properties"}, - %{ - "name" => "NascarBanDisplayConfederateFlagAllEventsProperties", - "url" => - "#{Web.base_url()}/tag/NascarBanDisplayConfederateFlagAllEventsProperties" - } - ] - end - - test "supports pagination of hashtags search results", %{conn: conn} do - results = - conn - |> get( - "/api/v2/search?#{ - URI.encode_query(%{q: "#some #text #with #hashtags", limit: 2, offset: 1}) - }" - ) - |> json_response_and_validate_schema(200) - - assert results["hashtags"] == [ - %{"name" => "text", "url" => "#{Web.base_url()}/tag/text"}, - %{"name" => "with", "url" => "#{Web.base_url()}/tag/with"} - ] - end - - test "excludes a blocked users from search results", %{conn: conn} do - user = insert(:user) - user_smith = insert(:user, %{nickname: "Agent", name: "I love 2hu"}) - user_neo = insert(:user, %{nickname: "Agent Neo", name: "Agent"}) - - {:ok, act1} = CommonAPI.post(user, %{status: "This is about 2hu private 天子"}) - {:ok, act2} = CommonAPI.post(user_smith, %{status: "Agent Smith"}) - {:ok, act3} = CommonAPI.post(user_neo, %{status: "Agent Smith"}) - Pleroma.User.block(user, user_smith) - - results = - conn - |> assign(:user, user) - |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) - |> get("/api/v2/search?q=Agent") - |> json_response_and_validate_schema(200) - - status_ids = Enum.map(results["statuses"], fn g -> g["id"] end) - - assert act3.id in status_ids - refute act2.id in status_ids - refute act1.id in status_ids - end - end - - describe ".account_search" do - test "account search", %{conn: conn} do - user_two = insert(:user, %{nickname: "shp@shitposter.club"}) - user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - results = - conn - |> get("/api/v1/accounts/search?q=shp") - |> json_response_and_validate_schema(200) - - result_ids = for result <- results, do: result["acct"] - - assert user_two.nickname in result_ids - assert user_three.nickname in result_ids - - results = - conn - |> get("/api/v1/accounts/search?q=2hu") - |> json_response_and_validate_schema(200) - - result_ids = for result <- results, do: result["acct"] - - assert user_three.nickname in result_ids - end - - test "returns account if query contains a space", %{conn: conn} do - insert(:user, %{nickname: "shp@shitposter.club"}) - - results = - conn - |> get("/api/v1/accounts/search?q=shp@shitposter.club xxx") - |> json_response_and_validate_schema(200) - - assert length(results) == 1 - end - end - - describe ".search" do - test "it returns empty result if user or status search return undefined error", %{conn: conn} do - with_mocks [ - {Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]}, - {Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]} - ] do - capture_log(fn -> - results = - conn - |> get("/api/v1/search?q=2hu") - |> json_response_and_validate_schema(200) - - assert results["accounts"] == [] - assert results["statuses"] == [] - end) =~ - "[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}" - end - end - - test "search", %{conn: conn} do - user = insert(:user) - user_two = insert(:user, %{nickname: "shp@shitposter.club"}) - user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - {:ok, activity} = CommonAPI.post(user, %{status: "This is about 2hu"}) - - {:ok, _activity} = - CommonAPI.post(user, %{ - status: "This is about 2hu, but private", - visibility: "private" - }) - - {:ok, _} = CommonAPI.post(user_two, %{status: "This isn't"}) - - results = - conn - |> get("/api/v1/search?q=2hu") - |> json_response_and_validate_schema(200) - - [account | _] = results["accounts"] - assert account["id"] == to_string(user_three.id) - - assert results["hashtags"] == ["2hu"] - - [status] = results["statuses"] - assert status["id"] == to_string(activity.id) - end - - test "search fetches remote statuses and prefers them over other results", %{conn: conn} do - capture_log(fn -> - {: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) - - assert [ - %{"url" => "http://mastodon.example.org/@admin/99541947525187367"}, - %{"id" => ^activity_id} - ] = results["statuses"] - end) - end - - test "search doesn't show statuses that it shouldn't", %{conn: conn} do - {:ok, activity} = - CommonAPI.post(insert(:user), %{ - status: "This is about 2hu, but private", - visibility: "private" - }) - - capture_log(fn -> - q = Object.normalize(activity).data["id"] - - results = - conn - |> get("/api/v1/search?q=#{q}") - |> json_response_and_validate_schema(200) - - [] = results["statuses"] - end) - end - - test "search fetches remote accounts", %{conn: conn} do - user = insert(:user) - - query = URI.encode_query(%{q: " mike@osada.macgirvin.com ", resolve: true}) - - results = - conn - |> assign(:user, user) - |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) - |> get("/api/v1/search?#{query}") - |> json_response_and_validate_schema(200) - - [account] = results["accounts"] - assert account["acct"] == "mike@osada.macgirvin.com" - end - - test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do - results = - conn - |> get("/api/v1/search?q=mike@osada.macgirvin.com&resolve=false") - |> json_response_and_validate_schema(200) - - assert [] == results["accounts"] - end - - test "search with limit and offset", %{conn: conn} do - user = insert(:user) - _user_two = insert(:user, %{nickname: "shp@shitposter.club"}) - _user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - {:ok, _activity1} = CommonAPI.post(user, %{status: "This is about 2hu"}) - {:ok, _activity2} = CommonAPI.post(user, %{status: "This is also about 2hu"}) - - result = - conn - |> get("/api/v1/search?q=2hu&limit=1") - - assert results = json_response_and_validate_schema(result, 200) - assert [%{"id" => activity_id1}] = results["statuses"] - assert [_] = results["accounts"] - - results = - conn - |> get("/api/v1/search?q=2hu&limit=1&offset=1") - |> json_response_and_validate_schema(200) - - assert [%{"id" => activity_id2}] = results["statuses"] - assert [] = results["accounts"] - - assert activity_id1 != activity_id2 - end - - test "search returns results only for the given type", %{conn: conn} do - user = insert(:user) - _user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - {:ok, _activity} = CommonAPI.post(user, %{status: "This is about 2hu"}) - - assert %{"statuses" => [_activity], "accounts" => [], "hashtags" => []} = - conn - |> get("/api/v1/search?q=2hu&type=statuses") - |> json_response_and_validate_schema(200) - - assert %{"statuses" => [], "accounts" => [_user_two], "hashtags" => []} = - conn - |> get("/api/v1/search?q=2hu&type=accounts") - |> json_response_and_validate_schema(200) - end - - test "search uses account_id to filter statuses by the author", %{conn: conn} do - user = insert(:user, %{nickname: "shp@shitposter.club"}) - user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - {:ok, activity1} = CommonAPI.post(user, %{status: "This is about 2hu"}) - {:ok, activity2} = CommonAPI.post(user_two, %{status: "This is also about 2hu"}) - - results = - conn - |> get("/api/v1/search?q=2hu&account_id=#{user.id}") - |> json_response_and_validate_schema(200) - - assert [%{"id" => activity_id1}] = results["statuses"] - assert activity_id1 == activity1.id - assert [_] = results["accounts"] - - results = - conn - |> get("/api/v1/search?q=2hu&account_id=#{user_two.id}") - |> json_response_and_validate_schema(200) - - assert [%{"id" => activity_id2}] = results["statuses"] - assert activity_id2 == activity2.id - end - end -end diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -1,1743 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do - use Pleroma.Web.ConnCase - use Oban.Testing, repo: Pleroma.Repo - - alias Pleroma.Activity - alias Pleroma.Config - alias Pleroma.Conversation.Participation - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.ScheduledActivity - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.CommonAPI - - 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]) - - describe "posting statuses" do - setup do: oauth_access(["write:statuses"]) - - test "posting a status does not increment reblog_count when relaying", %{conn: conn} do - Config.put([:instance, :federating], true) - Config.get([:instance, :allow_relay], true) - - response = - conn - |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ - "content_type" => "text/plain", - "source" => "Pleroma FE", - "status" => "Hello world", - "visibility" => "public" - }) - |> json_response_and_validate_schema(200) - - assert response["reblogs_count"] == 0 - ObanHelpers.perform_all() - - response = - conn - |> get("api/v1/statuses/#{response["id"]}", %{}) - |> json_response_and_validate_schema(200) - - assert response["reblogs_count"] == 0 - end - - test "posting a status", %{conn: conn} do - idempotency_key = "Pikachu rocks!" - - conn_one = - conn - |> put_req_header("content-type", "application/json") - |> put_req_header("idempotency-key", idempotency_key) - |> post("/api/v1/statuses", %{ - "status" => "cofe", - "spoiler_text" => "2hu", - "sensitive" => "0" - }) - - {:ok, ttl} = Cachex.ttl(:idempotency_cache, idempotency_key) - # Six hours - assert ttl > :timer.seconds(6 * 60 * 60 - 1) - - assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} = - json_response_and_validate_schema(conn_one, 200) - - assert Activity.get_by_id(id) - - conn_two = - conn - |> put_req_header("content-type", "application/json") - |> put_req_header("idempotency-key", idempotency_key) - |> post("/api/v1/statuses", %{ - "status" => "cofe", - "spoiler_text" => "2hu", - "sensitive" => 0 - }) - - assert %{"id" => second_id} = json_response(conn_two, 200) - assert id == second_id - - conn_three = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "cofe", - "spoiler_text" => "2hu", - "sensitive" => "False" - }) - - assert %{"id" => third_id} = json_response_and_validate_schema(conn_three, 200) - refute id == third_id - - # An activity that will expire: - # 2 hours - expires_in = 2 * 60 * 60 - - expires_at = DateTime.add(DateTime.utc_now(), expires_in) - - conn_four = - conn - |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ - "status" => "oolong", - "expires_in" => expires_in - }) - - assert %{"id" => fourth_id} = json_response_and_validate_schema(conn_four, 200) - - assert Activity.get_by_id(fourth_id) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: fourth_id}, - scheduled_at: expires_at - ) - end - - test "it fails to create a status if `expires_in` is less or equal than an hour", %{ - conn: conn - } do - # 1 minute - expires_in = 1 * 60 - - assert %{"error" => "Expiry date is too soon"} = - conn - |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ - "status" => "oolong", - "expires_in" => expires_in - }) - |> json_response_and_validate_schema(422) - - # 5 minutes - expires_in = 5 * 60 - - assert %{"error" => "Expiry date is too soon"} = - conn - |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ - "status" => "oolong", - "expires_in" => expires_in - }) - |> json_response_and_validate_schema(422) - end - - test "Get MRF reason when posting a status is rejected by one", %{conn: conn} do - Config.put([:mrf_keyword, :reject], ["GNO"]) - Config.put([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) - - assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} = - conn - |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{"status" => "GNO/Linux"}) - |> json_response_and_validate_schema(422) - end - - test "posting an undefined status with an attachment", %{user: user, conn: conn} do - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "media_ids" => [to_string(upload.id)] - }) - - assert json_response_and_validate_schema(conn, 200) - end - - test "replying to a status", %{user: user, conn: conn} do - {:ok, replied_to} = CommonAPI.post(user, %{status: "cofe"}) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id}) - - assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200) - - activity = Activity.get_by_id(id) - - assert activity.data["context"] == replied_to.data["context"] - assert Activity.get_in_reply_to_activity(activity).id == replied_to.id - end - - test "replying to a direct message with visibility other than direct", %{ - user: user, - conn: conn - } do - {:ok, replied_to} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"}) - - Enum.each(["public", "private", "unlisted"], fn visibility -> - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "@#{user.nickname} hey", - "in_reply_to_id" => replied_to.id, - "visibility" => visibility - }) - - assert json_response_and_validate_schema(conn, 422) == %{ - "error" => "The message visibility must be direct" - } - end) - end - - test "posting a status with an invalid in_reply_to_id", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""}) - - assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200) - assert Activity.get_by_id(id) - end - - test "posting a sensitive status", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true}) - - assert %{"content" => "cofe", "id" => id, "sensitive" => true} = - json_response_and_validate_schema(conn, 200) - - assert Activity.get_by_id(id) - end - - test "posting a fake status", %{conn: conn} do - real_conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => - "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it" - }) - - real_status = json_response_and_validate_schema(real_conn, 200) - - assert real_status - assert Object.get_by_ap_id(real_status["uri"]) - - real_status = - real_status - |> Map.put("id", nil) - |> Map.put("url", nil) - |> Map.put("uri", nil) - |> Map.put("created_at", nil) - |> Kernel.put_in(["pleroma", "conversation_id"], nil) - - fake_conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => - "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it", - "preview" => true - }) - - fake_status = json_response_and_validate_schema(fake_conn, 200) - - assert fake_status - refute Object.get_by_ap_id(fake_status["uri"]) - - fake_status = - fake_status - |> Map.put("id", nil) - |> Map.put("url", nil) - |> Map.put("uri", nil) - |> Map.put("created_at", 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(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}" - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"}) - - assert %{"id" => id} = response = json_response_and_validate_schema(conn, 200) - assert response["visibility"] == "direct" - assert response["pleroma"]["direct_conversation_id"] - assert activity = Activity.get_by_id(id) - assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id] - assert activity.data["to"] == [user2.ap_id] - assert activity.data["cc"] == [] - end - end - - describe "posting scheduled statuses" do - setup do: oauth_access(["write:statuses"]) - - test "creates a scheduled activity", %{conn: conn} do - scheduled_at = - NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) - |> NaiveDateTime.to_iso8601() - |> Kernel.<>("Z") - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "scheduled", - "scheduled_at" => scheduled_at - }) - - assert %{"scheduled_at" => expected_scheduled_at} = - json_response_and_validate_schema(conn, 200) - - assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at) - assert [] == Repo.all(Activity) - end - - test "ignores nil values", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "not scheduled", - "scheduled_at" => nil - }) - - assert result = json_response_and_validate_schema(conn, 200) - assert Activity.get_by_id(result["id"]) - end - - test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do - scheduled_at = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(120), :millisecond) - |> NaiveDateTime.to_iso8601() - |> Kernel.<>("Z") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "media_ids" => [to_string(upload.id)], - "status" => "scheduled", - "scheduled_at" => scheduled_at - }) - - assert %{"media_attachments" => [media_attachment]} = - json_response_and_validate_schema(conn, 200) - - assert %{"type" => "image"} = media_attachment - end - - test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now", - %{conn: conn} do - scheduled_at = - NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond) - |> NaiveDateTime.to_iso8601() - |> Kernel.<>("Z") - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "not scheduled", - "scheduled_at" => scheduled_at - }) - - assert %{"content" => "not scheduled"} = json_response_and_validate_schema(conn, 200) - assert [] == Repo.all(ScheduledActivity) - end - - test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do - today = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(6), :millisecond) - |> NaiveDateTime.to_iso8601() - # TODO - |> Kernel.<>("Z") - - attrs = %{params: %{}, scheduled_at: today} - {:ok, _} = ScheduledActivity.create(user, attrs) - {:ok, _} = ScheduledActivity.create(user, attrs) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today}) - - assert %{"error" => "daily limit exceeded"} == json_response_and_validate_schema(conn, 422) - end - - test "returns error when total user limit is exceeded", %{user: user, conn: conn} do - today = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(6), :millisecond) - |> NaiveDateTime.to_iso8601() - |> Kernel.<>("Z") - - tomorrow = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.hours(36), :millisecond) - |> NaiveDateTime.to_iso8601() - |> Kernel.<>("Z") - - attrs = %{params: %{}, scheduled_at: today} - {:ok, _} = ScheduledActivity.create(user, attrs) - {:ok, _} = ScheduledActivity.create(user, attrs) - {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow}) - - assert %{"error" => "total limit exceeded"} == json_response_and_validate_schema(conn, 422) - end - end - - describe "posting polls" do - setup do: oauth_access(["write:statuses"]) - - test "posting a poll", %{conn: conn} do - time = NaiveDateTime.utc_now() - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "Who is the #bestgrill?", - "poll" => %{ - "options" => ["Rei", "Asuka", "Misato"], - "expires_in" => 420 - } - }) - - response = json_response_and_validate_schema(conn, 200) - - assert Enum.all?(response["poll"]["options"], fn %{"title" => title} -> - title in ["Rei", "Asuka", "Misato"] - end) - - assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430 - refute response["poll"]["expred"] - - question = Object.get_by_id(response["poll"]["id"]) - - # closed contains utc timezone - assert question.data["closed"] =~ "Z" - end - - test "option limit is enforced", %{conn: conn} do - limit = Config.get([:instance, :poll_limits, :max_options]) - - conn = - conn - |> 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} - }) - - %{"error" => error} = json_response_and_validate_schema(conn, 422) - assert error == "Poll can't contain more than #{limit} options" - end - - test "option character limit is enforced", %{conn: conn} do - limit = Config.get([:instance, :poll_limits, :max_option_chars]) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "...", - "poll" => %{ - "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)], - "expires_in" => 1 - } - }) - - %{"error" => error} = json_response_and_validate_schema(conn, 422) - assert error == "Poll options cannot be longer than #{limit} characters each" - end - - test "minimal date limit is enforced", %{conn: conn} do - limit = Config.get([:instance, :poll_limits, :min_expiration]) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "imagine arbitrary limits", - "poll" => %{ - "options" => ["this post was made by pleroma gang"], - "expires_in" => limit - 1 - } - }) - - %{"error" => error} = json_response_and_validate_schema(conn, 422) - assert error == "Expiration date is too soon" - end - - test "maximum date limit is enforced", %{conn: conn} do - limit = Config.get([:instance, :poll_limits, :max_expiration]) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "imagine arbitrary limits", - "poll" => %{ - "options" => ["this post was made by pleroma gang"], - "expires_in" => limit + 1 - } - }) - - %{"error" => error} = json_response_and_validate_schema(conn, 422) - assert error == "Expiration date is too far in the future" - end - end - - test "get a status" do - %{conn: conn} = oauth_access(["read:statuses"]) - activity = insert(:note_activity) - - conn = get(conn, "/api/v1/statuses/#{activity.id}") - - assert %{"id" => id} = json_response_and_validate_schema(conn, 200) - assert id == to_string(activity.id) - end - - defp local_and_remote_activities do - local = insert(:note_activity) - remote = insert(:note_activity, local: false) - {:ok, local: local, remote: remote} - end - - describe "status with restrict unauthenticated activities for local and remote" do - setup do: local_and_remote_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, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/statuses/#{local.id}") - - assert json_response_and_validate_schema(res_conn, :not_found) == %{ - "error" => "Record not found" - } - - res_conn = get(conn, "/api/v1/statuses/#{remote.id}") - - assert json_response_and_validate_schema(res_conn, :not_found) == %{ - "error" => "Record not found" - } - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - res_conn = get(conn, "/api/v1/statuses/#{local.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - - res_conn = get(conn, "/api/v1/statuses/#{remote.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - end - end - - describe "status with restrict unauthenticated activities for local" do - setup do: local_and_remote_activities() - - setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/statuses/#{local.id}") - - assert json_response_and_validate_schema(res_conn, :not_found) == %{ - "error" => "Record not found" - } - - res_conn = get(conn, "/api/v1/statuses/#{remote.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - res_conn = get(conn, "/api/v1/statuses/#{local.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - - res_conn = get(conn, "/api/v1/statuses/#{remote.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - end - end - - describe "status with restrict unauthenticated activities for remote" do - setup do: local_and_remote_activities() - - setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/statuses/#{local.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - - res_conn = get(conn, "/api/v1/statuses/#{remote.id}") - - assert json_response_and_validate_schema(res_conn, :not_found) == %{ - "error" => "Record not found" - } - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - res_conn = get(conn, "/api/v1/statuses/#{local.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - - res_conn = get(conn, "/api/v1/statuses/#{remote.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - end - end - - test "getting a status that doesn't exist returns 404" do - %{conn: conn} = oauth_access(["read:statuses"]) - activity = insert(:note_activity) - - conn = get(conn, "/api/v1/statuses/#{String.downcase(activity.id)}") - - assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} - end - - test "get a direct status" do - %{user: user, conn: conn} = oauth_access(["read:statuses"]) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "direct"}) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/statuses/#{activity.id}") - - [participation] = Participation.for_user(user) - - res = json_response_and_validate_schema(conn, 200) - assert res["pleroma"]["direct_conversation_id"] == participation.id - end - - test "get statuses by IDs" do - %{conn: conn} = oauth_access(["read:statuses"]) - %{id: id1} = insert(:note_activity) - %{id: id2} = insert(:note_activity) - - query_string = "ids[]=#{id1}&ids[]=#{id2}" - conn = get(conn, "/api/v1/statuses/?#{query_string}") - - assert [%{"id" => ^id1}, %{"id" => ^id2}] = - Enum.sort_by(json_response_and_validate_schema(conn, :ok), & &1["id"]) - end - - describe "getting statuses by ids with restricted unauthenticated for local and remote" do - setup do: local_and_remote_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, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") - - assert json_response_and_validate_schema(res_conn, 200) == [] - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") - - assert length(json_response_and_validate_schema(res_conn, 200)) == 2 - end - end - - describe "getting statuses by ids with restricted unauthenticated for local" do - setup do: local_and_remote_activities() - - setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") - - remote_id = remote.id - assert [%{"id" => ^remote_id}] = json_response_and_validate_schema(res_conn, 200) - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") - - assert length(json_response_and_validate_schema(res_conn, 200)) == 2 - end - end - - describe "getting statuses by ids with restricted unauthenticated for remote" do - setup do: local_and_remote_activities() - - setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") - - local_id = local.id - assert [%{"id" => ^local_id}] = json_response_and_validate_schema(res_conn, 200) - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") - - assert length(json_response_and_validate_schema(res_conn, 200)) == 2 - end - end - - describe "deleting a status" do - test "when you created it" do - %{user: author, conn: conn} = oauth_access(["write:statuses"]) - activity = insert(:note_activity, user: author) - object = Object.normalize(activity) - - content = object.data["content"] - source = object.data["source"] - - result = - conn - |> assign(:user, author) - |> delete("/api/v1/statuses/#{activity.id}") - |> json_response_and_validate_schema(200) - - assert match?(%{"content" => ^content, "text" => ^source}, result) - - refute Activity.get_by_id(activity.id) - end - - test "when it doesn't exist" do - %{user: author, conn: conn} = oauth_access(["write:statuses"]) - activity = insert(:note_activity, user: author) - - conn = - conn - |> assign(:user, author) - |> delete("/api/v1/statuses/#{String.downcase(activity.id)}") - - assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404) - end - - test "when you didn't create it" do - %{conn: conn} = oauth_access(["write:statuses"]) - activity = insert(:note_activity) - - conn = delete(conn, "/api/v1/statuses/#{activity.id}") - - assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404) - - 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) - - res_conn = - conn - |> assign(:user, admin) - |> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"])) - |> delete("/api/v1/statuses/#{activity1.id}") - - assert %{} = json_response_and_validate_schema(res_conn, 200) - - res_conn = - conn - |> assign(:user, moderator) - |> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"])) - |> delete("/api/v1/statuses/#{activity2.id}") - - assert %{} = json_response_and_validate_schema(res_conn, 200) - - refute Activity.get_by_id(activity1.id) - refute Activity.get_by_id(activity2.id) - end - end - - describe "reblogging" do - setup do: oauth_access(["write:statuses"]) - - test "reblogs and returns the reblogged status", %{conn: conn} do - activity = insert(:note_activity) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/reblog") - - assert %{ - "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}, - "reblogged" => true - } = json_response_and_validate_schema(conn, 200) - - assert to_string(activity.id) == id - end - - test "returns 404 if the reblogged status doesn't exist", %{conn: conn} do - activity = insert(:note_activity) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{String.downcase(activity.id)}/reblog") - - assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404) - end - - test "reblogs privately and returns the reblogged status", %{conn: conn} do - activity = insert(:note_activity) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post( - "/api/v1/statuses/#{activity.id}/reblog", - %{"visibility" => "private"} - ) - - assert %{ - "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}, - "reblogged" => true, - "visibility" => "private" - } = json_response_and_validate_schema(conn, 200) - - assert to_string(activity.id) == id - end - - test "reblogged status for another user" do - activity = insert(:note_activity) - user1 = insert(:user) - user2 = insert(:user) - user3 = insert(:user) - {:ok, _} = CommonAPI.favorite(user2, activity.id) - {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id) - {:ok, reblog_activity1} = CommonAPI.repeat(activity.id, user1) - {:ok, _} = CommonAPI.repeat(activity.id, user2) - - conn_res = - build_conn() - |> assign(:user, user3) - |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"])) - |> get("/api/v1/statuses/#{reblog_activity1.id}") - - assert %{ - "reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2}, - "reblogged" => false, - "favourited" => false, - "bookmarked" => false - } = json_response_and_validate_schema(conn_res, 200) - - conn_res = - build_conn() - |> assign(:user, user2) - |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"])) - |> get("/api/v1/statuses/#{reblog_activity1.id}") - - assert %{ - "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2}, - "reblogged" => true, - "favourited" => true, - "bookmarked" => true - } = json_response_and_validate_schema(conn_res, 200) - - assert to_string(activity.id) == id - end - end - - describe "unreblogging" do - setup do: oauth_access(["write:statuses"]) - - test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do - activity = insert(:note_activity) - - {:ok, _} = CommonAPI.repeat(activity.id, user) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/unreblog") - - assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = - json_response_and_validate_schema(conn, 200) - - assert to_string(activity.id) == id - end - - test "returns 404 error when activity does not exist", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/foo/unreblog") - - assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} - end - end - - describe "favoriting" do - setup do: oauth_access(["write:favourites"]) - - test "favs a status and returns it", %{conn: conn} do - activity = insert(:note_activity) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/favourite") - - assert %{"id" => id, "favourites_count" => 1, "favourited" => true} = - json_response_and_validate_schema(conn, 200) - - assert to_string(activity.id) == id - end - - test "favoriting twice will just return 200", %{conn: conn} do - activity = insert(:note_activity) - - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/favourite") - - assert conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/favourite") - |> json_response_and_validate_schema(200) - end - - test "returns 404 error for a wrong id", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/1/favourite") - - assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} - end - end - - describe "unfavoriting" do - setup do: oauth_access(["write:favourites"]) - - test "unfavorites a status and returns it", %{user: user, conn: conn} do - activity = insert(:note_activity) - - {:ok, _} = CommonAPI.favorite(user, activity.id) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/unfavourite") - - assert %{"id" => id, "favourites_count" => 0, "favourited" => false} = - json_response_and_validate_schema(conn, 200) - - assert to_string(activity.id) == id - end - - test "returns 404 error for a wrong id", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/1/unfavourite") - - assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} - end - end - - describe "pinned statuses" do - setup do: oauth_access(["write:accounts"]) - - setup %{user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"}) - - %{activity: activity} - end - - setup do: clear_config([:instance, :max_pinned_statuses], 1) - - test "pin status", %{conn: conn, user: user, activity: activity} do - id_str = to_string(activity.id) - - assert %{"id" => ^id_str, "pinned" => true} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/pin") - |> json_response_and_validate_schema(200) - - assert [%{"id" => ^id_str, "pinned" => true}] = - conn - |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") - |> json_response_and_validate_schema(200) - end - - test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do - {:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"}) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{dm.id}/pin") - - assert json_response_and_validate_schema(conn, 400) == %{"error" => "Could not pin"} - end - - test "unpin status", %{conn: conn, user: user, activity: activity} do - {:ok, _} = CommonAPI.pin(activity.id, user) - user = refresh_record(user) - - id_str = to_string(activity.id) - - assert %{"id" => ^id_str, "pinned" => false} = - conn - |> assign(:user, user) - |> post("/api/v1/statuses/#{activity.id}/unpin") - |> json_response_and_validate_schema(200) - - assert [] = - conn - |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") - |> json_response_and_validate_schema(200) - end - - test "/unpin: returns 400 error when activity is not exist", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/1/unpin") - - assert json_response_and_validate_schema(conn, 400) == %{"error" => "Could not unpin"} - end - - test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do - {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"}) - - id_str_one = to_string(activity_one.id) - - assert %{"id" => ^id_str_one, "pinned" => true} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{id_str_one}/pin") - |> json_response_and_validate_schema(200) - - user = refresh_record(user) - - assert %{"error" => "You have already pinned the maximum number of statuses"} = - conn - |> assign(:user, user) - |> post("/api/v1/statuses/#{activity_two.id}/pin") - |> json_response_and_validate_schema(400) - end - - test "on pin removes deletion job, on unpin reschedule deletion" do - %{conn: conn} = oauth_access(["write:accounts", "write:statuses"]) - expires_in = 2 * 60 * 60 - - expires_at = DateTime.add(DateTime.utc_now(), expires_in) - - assert %{"id" => id} = - conn - |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ - "status" => "oolong", - "expires_in" => expires_in - }) - |> json_response_and_validate_schema(200) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: id}, - scheduled_at: expires_at - ) - - assert %{"id" => ^id, "pinned" => true} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{id}/pin") - |> json_response_and_validate_schema(200) - - refute_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: id}, - scheduled_at: expires_at - ) - - assert %{"id" => ^id, "pinned" => false} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{id}/unpin") - |> json_response_and_validate_schema(200) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: id}, - scheduled_at: expires_at - ) - end - end - - describe "cards" do - setup do - Config.put([:rich_media, :enabled], true) - - oauth_access(["read:statuses"]) - end - - test "returns rich-media card", %{conn: conn, user: user} do - Tesla.Mock.mock(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(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" - - %{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"]) - author = insert(:user) - - {:ok, activity1} = CommonAPI.post(author, %{status: "heweoo?"}) - {:ok, activity2} = CommonAPI.post(author, %{status: "heweoo!"}) - - response1 = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity1.id}/bookmark") - - assert json_response_and_validate_schema(response1, 200)["bookmarked"] == true - - response2 = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity2.id}/bookmark") - - assert json_response_and_validate_schema(response2, 200)["bookmarked"] == true - - bookmarks = get(conn, bookmarks_uri) - - assert [ - json_response_and_validate_schema(response2, 200), - json_response_and_validate_schema(response1, 200) - ] == - json_response_and_validate_schema(bookmarks, 200) - - response1 = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity1.id}/unbookmark") - - assert json_response_and_validate_schema(response1, 200)["bookmarked"] == false - - bookmarks = get(conn, bookmarks_uri) - - assert [json_response_and_validate_schema(response2, 200)] == - json_response_and_validate_schema(bookmarks, 200) - end - - describe "conversation muting" do - setup do: oauth_access(["write:mutes"]) - - setup do - post_user = insert(:user) - {:ok, activity} = CommonAPI.post(post_user, %{status: "HIE"}) - %{activity: activity} - end - - test "mute conversation", %{conn: conn, activity: activity} do - id_str = to_string(activity.id) - - assert %{"id" => ^id_str, "muted" => true} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/mute") - |> json_response_and_validate_schema(200) - end - - test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do - {:ok, _} = CommonAPI.add_mute(user, activity) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/mute") - - assert json_response_and_validate_schema(conn, 400) == %{ - "error" => "conversation is already muted" - } - end - - test "unmute conversation", %{conn: conn, user: user, activity: activity} do - {:ok, _} = CommonAPI.add_mute(user, activity) - - id_str = to_string(activity.id) - - assert %{"id" => ^id_str, "muted" => false} = - conn - # |> assign(:user, user) - |> post("/api/v1/statuses/#{activity.id}/unmute") - |> json_response_and_validate_schema(200) - end - end - - test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do - user1 = insert(:user) - user2 = insert(:user) - user3 = insert(:user) - - {:ok, replied_to} = CommonAPI.post(user1, %{status: "cofe"}) - - # Reply to status from another user - conn1 = - conn - |> assign(:user, user2) - |> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"])) - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id}) - - assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn1, 200) - - activity = Activity.get_by_id_with_object(id) - - assert Object.normalize(activity).data["inReplyTo"] == Object.normalize(replied_to).data["id"] - assert Activity.get_in_reply_to_activity(activity).id == replied_to.id - - # Reblog from the third user - conn2 = - conn - |> assign(:user, user3) - |> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"])) - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/reblog") - - assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} = - json_response_and_validate_schema(conn2, 200) - - assert to_string(activity.id) == id - - # Getting third user status - conn3 = - conn - |> assign(:user, user3) - |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"])) - |> get("api/v1/timelines/home") - - [reblogged_activity] = json_response(conn3, 200) - - assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id - - replied_to_user = User.get_by_ap_id(replied_to.data["actor"]) - assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id - end - - describe "GET /api/v1/statuses/:id/favourited_by" do - setup do: oauth_access(["read:accounts"]) - - setup %{user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "test"}) - - %{activity: activity} - end - - test "returns users who have favorited the status", %{conn: conn, activity: activity} do - other_user = insert(:user) - {:ok, _} = CommonAPI.favorite(other_user, activity.id) - - response = - conn - |> get("/api/v1/statuses/#{activity.id}/favourited_by") - |> json_response_and_validate_schema(:ok) - - [%{"id" => id}] = response - - assert id == other_user.id - end - - test "returns empty array when status has not been favorited yet", %{ - conn: conn, - activity: activity - } do - response = - conn - |> get("/api/v1/statuses/#{activity.id}/favourited_by") - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response) - end - - test "does not return users who have favorited the status but are blocked", %{ - conn: %{assigns: %{user: user}} = conn, - activity: activity - } do - other_user = insert(:user) - {:ok, _user_relationship} = User.block(user, other_user) - - {:ok, _} = CommonAPI.favorite(other_user, activity.id) - - response = - conn - |> get("/api/v1/statuses/#{activity.id}/favourited_by") - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response) - end - - test "does not fail on an unauthenticated request", %{activity: activity} do - other_user = insert(:user) - {:ok, _} = CommonAPI.favorite(other_user, activity.id) - - response = - build_conn() - |> get("/api/v1/statuses/#{activity.id}/favourited_by") - |> json_response_and_validate_schema(:ok) - - [%{"id" => id}] = response - assert id == other_user.id - end - - test "requires authentication for private posts", %{user: user} do - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "@#{other_user.nickname} wanna get some #cofe together?", - visibility: "direct" - }) - - {:ok, _} = CommonAPI.favorite(other_user, activity.id) - - favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by" - - build_conn() - |> get(favourited_by_url) - |> json_response_and_validate_schema(404) - - conn = - build_conn() - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) - - conn - |> assign(:token, nil) - |> get(favourited_by_url) - |> json_response_and_validate_schema(404) - - response = - conn - |> get(favourited_by_url) - |> json_response_and_validate_schema(200) - - [%{"id" => id}] = response - assert id == other_user.id - end - - test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do - clear_config([:instance, :show_reactions], false) - - other_user = insert(:user) - {:ok, _} = CommonAPI.favorite(other_user, activity.id) - - response = - conn - |> get("/api/v1/statuses/#{activity.id}/favourited_by") - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response) - end - end - - describe "GET /api/v1/statuses/:id/reblogged_by" do - setup do: oauth_access(["read:accounts"]) - - setup %{user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "test"}) - - %{activity: activity} - end - - test "returns users who have reblogged the status", %{conn: conn, activity: activity} do - other_user = insert(:user) - {:ok, _} = CommonAPI.repeat(activity.id, other_user) - - response = - conn - |> get("/api/v1/statuses/#{activity.id}/reblogged_by") - |> json_response_and_validate_schema(:ok) - - [%{"id" => id}] = response - - assert id == other_user.id - end - - test "returns empty array when status has not been reblogged yet", %{ - conn: conn, - activity: activity - } do - response = - conn - |> get("/api/v1/statuses/#{activity.id}/reblogged_by") - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response) - end - - test "does not return users who have reblogged the status but are blocked", %{ - conn: %{assigns: %{user: user}} = conn, - activity: activity - } do - other_user = insert(:user) - {:ok, _user_relationship} = User.block(user, other_user) - - {:ok, _} = CommonAPI.repeat(activity.id, other_user) - - response = - conn - |> get("/api/v1/statuses/#{activity.id}/reblogged_by") - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response) - end - - test "does not return users who have reblogged the status privately", %{ - conn: conn - } do - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(other_user, %{status: "my secret post"}) - - {:ok, _} = CommonAPI.repeat(activity.id, other_user, %{visibility: "private"}) - - response = - conn - |> get("/api/v1/statuses/#{activity.id}/reblogged_by") - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response) - end - - test "does not fail on an unauthenticated request", %{activity: activity} do - other_user = insert(:user) - {:ok, _} = CommonAPI.repeat(activity.id, other_user) - - response = - build_conn() - |> get("/api/v1/statuses/#{activity.id}/reblogged_by") - |> json_response_and_validate_schema(:ok) - - [%{"id" => id}] = response - assert id == other_user.id - end - - test "requires authentication for private posts", %{user: user} do - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "@#{other_user.nickname} wanna get some #cofe together?", - visibility: "direct" - }) - - build_conn() - |> get("/api/v1/statuses/#{activity.id}/reblogged_by") - |> json_response_and_validate_schema(404) - - response = - build_conn() - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) - |> get("/api/v1/statuses/#{activity.id}/reblogged_by") - |> json_response_and_validate_schema(200) - - assert [] == response - end - end - - test "context" do - user = insert(:user) - - {:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"}) - {:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1}) - {:ok, %{id: id3}} = CommonAPI.post(user, %{status: "3", in_reply_to_status_id: id2}) - {:ok, %{id: id4}} = CommonAPI.post(user, %{status: "4", in_reply_to_status_id: id3}) - {:ok, %{id: id5}} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4}) - - response = - build_conn() - |> get("/api/v1/statuses/#{id3}/context") - |> json_response_and_validate_schema(:ok) - - assert %{ - "ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}], - "descendants" => [%{"id" => ^id4}, %{"id" => ^id5}] - } = response - end - - test "favorites paginate correctly" do - %{user: user, conn: conn} = oauth_access(["read:favourites"]) - other_user = insert(:user) - {:ok, first_post} = CommonAPI.post(other_user, %{status: "bla"}) - {:ok, second_post} = CommonAPI.post(other_user, %{status: "bla"}) - {:ok, third_post} = CommonAPI.post(other_user, %{status: "bla"}) - - {:ok, _first_favorite} = CommonAPI.favorite(user, third_post.id) - {:ok, _second_favorite} = CommonAPI.favorite(user, first_post.id) - {:ok, third_favorite} = CommonAPI.favorite(user, second_post.id) - - result = - conn - |> get("/api/v1/favourites?limit=1") - - assert [%{"id" => post_id}] = json_response_and_validate_schema(result, 200) - assert post_id == second_post.id - - # Using the header for pagination works correctly - [next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ") - [_, max_id] = Regex.run(~r/max_id=([^&]+)/, next) - - assert max_id == third_favorite.id - - result = - conn - |> get("/api/v1/favourites?max_id=#{max_id}") - - assert [%{"id" => first_post_id}, %{"id" => third_post_id}] = - json_response_and_validate_schema(result, 200) - - assert first_post_id == first_post.id - assert third_post_id == third_post.id - end - - test "returns the favorites of a user" do - %{user: user, conn: conn} = oauth_access(["read:favourites"]) - other_user = insert(:user) - - {:ok, _} = CommonAPI.post(other_user, %{status: "bla"}) - {:ok, activity} = CommonAPI.post(other_user, %{status: "trees are happy"}) - - {:ok, last_like} = CommonAPI.favorite(user, activity.id) - - first_conn = get(conn, "/api/v1/favourites") - - assert [status] = json_response_and_validate_schema(first_conn, 200) - assert status["id"] == to_string(activity.id) - - assert [{"link", _link_header}] = - Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end) - - # Honours query params - {:ok, second_activity} = - CommonAPI.post(other_user, %{ - status: "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful." - }) - - {:ok, _} = CommonAPI.favorite(user, second_activity.id) - - second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like.id}") - - assert [second_status] = json_response_and_validate_schema(second_conn, 200) - assert second_status["id"] == to_string(second_activity.id) - - third_conn = get(conn, "/api/v1/favourites?limit=0") - - assert [] = json_response_and_validate_schema(third_conn, 200) - end - - test "expires_at is nil for another user" do - %{conn: conn, user: user} = oauth_access(["read:statuses"]) - expires_at = DateTime.add(DateTime.utc_now(), 1_000_000) - {:ok, activity} = CommonAPI.post(user, %{status: "foobar", expires_in: 1_000_000}) - - assert %{"pleroma" => %{"expires_at" => a_expires_at}} = - conn - |> get("/api/v1/statuses/#{activity.id}") - |> json_response_and_validate_schema(:ok) - - {:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at) - assert DateTime.diff(expires_at, a_expires_at) == 0 - - %{conn: conn} = oauth_access(["read:statuses"]) - - assert %{"pleroma" => %{"expires_at" => nil}} = - conn - |> get("/api/v1/statuses/#{activity.id}") - |> json_response_and_validate_schema(:ok) - end -end diff --git a/test/web/mastodon_api/controllers/subscription_controller_test.exs b/test/web/mastodon_api/controllers/subscription_controller_test.exs @@ -1,199 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - alias Pleroma.Web.Push - alias Pleroma.Web.Push.Subscription - - @sub %{ - "endpoint" => "https://example.com/example/1234", - "keys" => %{ - "auth" => "8eDyX_uCN0XRhSbY5hs7Hg==", - "p256dh" => - "BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=" - } - } - @server_key Keyword.get(Push.vapid_config(), :public_key) - - setup do - user = insert(:user) - token = insert(:oauth_token, user: user, scopes: ["push"]) - - conn = - build_conn() - |> assign(:user, user) - |> assign(:token, token) - |> put_req_header("content-type", "application/json") - - %{conn: conn, user: user, token: token} - end - - 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 "creates push subscription" do - test "returns error when push disabled ", %{conn: conn} do - assert_error_when_disable_push do - conn - |> post("/api/v1/push/subscription", %{subscription: @sub}) - |> json_response_and_validate_schema(403) - end - end - - test "successful creation", %{conn: conn} do - result = - conn - |> post("/api/v1/push/subscription", %{ - "data" => %{ - "alerts" => %{"mention" => true, "test" => true, "pleroma:chat_mention" => true} - }, - "subscription" => @sub - }) - |> json_response_and_validate_schema(200) - - [subscription] = Pleroma.Repo.all(Subscription) - - assert %{ - "alerts" => %{"mention" => true, "pleroma:chat_mention" => true}, - "endpoint" => subscription.endpoint, - "id" => to_string(subscription.id), - "server_key" => @server_key - } == result - end - end - - describe "gets a user subscription" do - test "returns error when push disabled ", %{conn: conn} do - assert_error_when_disable_push do - conn - |> get("/api/v1/push/subscription", %{}) - |> json_response_and_validate_schema(403) - end - end - - test "returns error when user hasn't subscription", %{conn: conn} do - res = - conn - |> get("/api/v1/push/subscription", %{}) - |> json_response_and_validate_schema(404) - - assert %{"error" => "Record not found"} == res - end - - test "returns a user subsciption", %{conn: conn, user: user, token: token} do - subscription = - insert(:push_subscription, - user: user, - token: token, - data: %{"alerts" => %{"mention" => true}} - ) - - res = - conn - |> get("/api/v1/push/subscription", %{}) - |> json_response_and_validate_schema(200) - - expect = %{ - "alerts" => %{"mention" => true}, - "endpoint" => "https://example.com/example/1234", - "id" => to_string(subscription.id), - "server_key" => @server_key - } - - assert expect == res - end - end - - describe "updates a user subsciption" do - setup %{conn: conn, user: user, token: token} do - subscription = - insert(:push_subscription, - user: user, - token: token, - data: %{"alerts" => %{"mention" => true}} - ) - - %{conn: conn, user: user, token: token, subscription: subscription} - end - - test "returns error when push disabled ", %{conn: conn} do - assert_error_when_disable_push do - conn - |> put("/api/v1/push/subscription", %{data: %{"alerts" => %{"mention" => false}}}) - |> json_response_and_validate_schema(403) - end - end - - test "returns updated subsciption", %{conn: conn, subscription: subscription} do - res = - conn - |> put("/api/v1/push/subscription", %{ - data: %{"alerts" => %{"mention" => false, "follow" => true}} - }) - |> json_response_and_validate_schema(200) - - expect = %{ - "alerts" => %{"follow" => true, "mention" => false}, - "endpoint" => "https://example.com/example/1234", - "id" => to_string(subscription.id), - "server_key" => @server_key - } - - assert expect == res - end - end - - describe "deletes the user subscription" do - test "returns error when push disabled ", %{conn: conn} do - assert_error_when_disable_push do - conn - |> delete("/api/v1/push/subscription", %{}) - |> json_response_and_validate_schema(403) - end - end - - test "returns error when user hasn't subscription", %{conn: conn} do - res = - conn - |> delete("/api/v1/push/subscription", %{}) - |> json_response_and_validate_schema(404) - - assert %{"error" => "Record not found"} == res - end - - test "returns empty result and delete user subsciption", %{ - conn: conn, - user: user, - token: token - } do - subscription = - insert(:push_subscription, - user: user, - token: token, - data: %{"alerts" => %{"mention" => true}} - ) - - res = - conn - |> delete("/api/v1/push/subscription", %{}) - |> json_response_and_validate_schema(200) - - assert %{} == res - refute Pleroma.Repo.get(Subscription, subscription.id) - end - end -end diff --git a/test/web/mastodon_api/controllers/suggestion_controller_test.exs b/test/web/mastodon_api/controllers/suggestion_controller_test.exs @@ -1,18 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do - use Pleroma.Web.ConnCase - - setup do: oauth_access(["read"]) - - test "returns empty result", %{conn: conn} do - res = - conn - |> get("/api/v1/suggestions") - |> json_response_and_validate_schema(200) - - assert res == [] - end -end diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -1,560 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - import Tesla.Mock - - alias Pleroma.Config - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe "home" do - setup do: oauth_access(["read:statuses"]) - - test "does NOT embed account/pleroma/relationship in statuses", %{ - user: user, - conn: conn - } do - other_user = insert(:user) - - {:ok, _} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - - response = - conn - |> assign(:user, user) - |> get("/api/v1/timelines/home") - |> json_response_and_validate_schema(200) - - assert Enum.all?(response, fn n -> - get_in(n, ["account", "pleroma", "relationship"]) == %{} - end) - end - - test "the home timeline when the direct messages are excluded", %{user: user, conn: conn} do - {:ok, public_activity} = CommonAPI.post(user, %{status: ".", visibility: "public"}) - {:ok, direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) - - {:ok, unlisted_activity} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) - - {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"}) - - conn = get(conn, "/api/v1/timelines/home?exclude_visibilities[]=direct") - - assert status_ids = json_response_and_validate_schema(conn, :ok) |> Enum.map(& &1["id"]) - assert public_activity.id in status_ids - assert unlisted_activity.id in status_ids - assert private_activity.id in status_ids - refute direct_activity.id in status_ids - end - end - - describe "public" do - @tag capture_log: true - test "the public timeline", %{conn: conn} do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test"}) - - _activity = insert(:note_activity, local: false) - - conn = get(conn, "/api/v1/timelines/public?local=False") - - assert length(json_response_and_validate_schema(conn, :ok)) == 2 - - conn = get(build_conn(), "/api/v1/timelines/public?local=True") - - assert [%{"content" => "test"}] = json_response_and_validate_schema(conn, :ok) - - conn = get(build_conn(), "/api/v1/timelines/public?local=1") - - assert [%{"content" => "test"}] = json_response_and_validate_schema(conn, :ok) - - # does not contain repeats - {:ok, _} = CommonAPI.repeat(activity.id, user) - - conn = get(build_conn(), "/api/v1/timelines/public?local=true") - - assert [_] = json_response_and_validate_schema(conn, :ok) - end - - test "the public timeline includes only public statuses for an authenticated user" do - %{user: user, conn: conn} = oauth_access(["read:statuses"]) - - {:ok, _activity} = CommonAPI.post(user, %{status: "test"}) - {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "private"}) - {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "unlisted"}) - {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "direct"}) - - res_conn = get(conn, "/api/v1/timelines/public") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - end - - test "doesn't return replies if follower is posting with blocked user" do - %{conn: conn, user: blocker} = oauth_access(["read:statuses"]) - [blockee, friend] = insert_list(2, :user) - {:ok, blocker} = User.follow(blocker, friend) - {:ok, _} = User.block(blocker, blockee) - - conn = assign(conn, :user, blocker) - - {:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"}) - - {:ok, reply_from_blockee} = - CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity}) - - {:ok, _reply_from_friend} = - CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee}) - - # Still shows replies from yourself - {:ok, %{id: reply_from_me}} = - CommonAPI.post(blocker, %{status: "status", in_reply_to_status_id: reply_from_blockee}) - - response = - get(conn, "/api/v1/timelines/public") - |> json_response_and_validate_schema(200) - - assert length(response) == 2 - [%{"id" => ^reply_from_me}, %{"id" => ^activity_id}] = response - end - - test "doesn't return replies if follow is posting with users from blocked domain" do - %{conn: conn, user: blocker} = oauth_access(["read:statuses"]) - friend = insert(:user) - blockee = insert(:user, ap_id: "https://example.com/users/blocked") - {:ok, blocker} = User.follow(blocker, friend) - {:ok, blocker} = User.block_domain(blocker, "example.com") - - conn = assign(conn, :user, blocker) - - {:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"}) - - {:ok, reply_from_blockee} = - CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity}) - - {:ok, _reply_from_friend} = - CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee}) - - res_conn = get(conn, "/api/v1/timelines/public") - - activities = json_response_and_validate_schema(res_conn, 200) - [%{"id" => ^activity_id}] = activities - end - end - - defp local_and_remote_activities do - insert(:note_activity) - insert(:note_activity, local: false) - :ok - end - - describe "public with restrict unauthenticated timeline for local and federated timelines" do - setup do: local_and_remote_activities() - - setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true) - - setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true) - - test "if user is unauthenticated", %{conn: conn} do - res_conn = get(conn, "/api/v1/timelines/public?local=true") - - assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ - "error" => "authorization required for timeline view" - } - - res_conn = get(conn, "/api/v1/timelines/public?local=false") - - assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ - "error" => "authorization required for timeline view" - } - end - - test "if user is authenticated" do - %{conn: conn} = oauth_access(["read:statuses"]) - - res_conn = get(conn, "/api/v1/timelines/public?local=true") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - - res_conn = get(conn, "/api/v1/timelines/public?local=false") - assert length(json_response_and_validate_schema(res_conn, 200)) == 2 - end - end - - describe "public with restrict unauthenticated timeline for local" do - setup do: local_and_remote_activities() - - setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true) - - test "if user is unauthenticated", %{conn: conn} do - res_conn = get(conn, "/api/v1/timelines/public?local=true") - - assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ - "error" => "authorization required for timeline view" - } - - res_conn = get(conn, "/api/v1/timelines/public?local=false") - assert length(json_response_and_validate_schema(res_conn, 200)) == 2 - end - - test "if user is authenticated", %{conn: _conn} do - %{conn: conn} = oauth_access(["read:statuses"]) - - res_conn = get(conn, "/api/v1/timelines/public?local=true") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - - res_conn = get(conn, "/api/v1/timelines/public?local=false") - assert length(json_response_and_validate_schema(res_conn, 200)) == 2 - end - end - - describe "public with restrict unauthenticated timeline for remote" do - setup do: local_and_remote_activities() - - setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true) - - test "if user is unauthenticated", %{conn: conn} do - res_conn = get(conn, "/api/v1/timelines/public?local=true") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - - res_conn = get(conn, "/api/v1/timelines/public?local=false") - - assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ - "error" => "authorization required for timeline view" - } - end - - test "if user is authenticated", %{conn: _conn} do - %{conn: conn} = oauth_access(["read:statuses"]) - - res_conn = get(conn, "/api/v1/timelines/public?local=true") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - - res_conn = get(conn, "/api/v1/timelines/public?local=false") - assert length(json_response_and_validate_schema(res_conn, 200)) == 2 - end - end - - describe "direct" do - test "direct timeline", %{conn: conn} do - user_one = insert(:user) - user_two = insert(:user) - - {:ok, user_two} = User.follow(user_two, user_one) - - {:ok, direct} = - CommonAPI.post(user_one, %{ - status: "Hi @#{user_two.nickname}!", - visibility: "direct" - }) - - {:ok, _follower_only} = - CommonAPI.post(user_one, %{ - status: "Hi @#{user_two.nickname}!", - visibility: "private" - }) - - conn_user_two = - conn - |> assign(:user, user_two) - |> 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") - - assert [status] = json_response_and_validate_schema(res_conn, :ok) - - assert %{"visibility" => "direct"} = status - assert status["url"] != direct.data["id"] - - # User should be able to see their own direct message - res_conn = - build_conn() - |> assign(:user, user_one) - |> assign(:token, insert(:oauth_token, user: user_one, scopes: ["read:statuses"])) - |> 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") - - [_s1, _s2] = json_response_and_validate_schema(res_conn, :ok) - - # Test pagination - Enum.each(1..20, fn _ -> - {:ok, _} = - CommonAPI.post(user_one, %{ - status: "Hi @#{user_two.nickname}!", - visibility: "direct" - }) - end) - - 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}") - - assert [status] = json_response_and_validate_schema(res_conn, :ok) - - assert status["url"] != direct.data["id"] - end - - test "doesn't include DMs from blocked users" do - %{user: blocker, conn: conn} = oauth_access(["read:statuses"]) - blocked = insert(:user) - other_user = insert(:user) - {:ok, _user_relationship} = User.block(blocker, blocked) - - {:ok, _blocked_direct} = - CommonAPI.post(blocked, %{ - status: "Hi @#{blocker.nickname}!", - visibility: "direct" - }) - - {:ok, direct} = - CommonAPI.post(other_user, %{ - status: "Hi @#{blocker.nickname}!", - visibility: "direct" - }) - - res_conn = get(conn, "api/v1/timelines/direct") - - [status] = json_response_and_validate_schema(res_conn, :ok) - assert status["id"] == direct.id - end - end - - describe "list" do - setup do: oauth_access(["read:lists"]) - - test "does not contain retoots", %{user: user, conn: conn} do - other_user = insert(:user) - {:ok, activity_one} = CommonAPI.post(user, %{status: "Marisa is cute."}) - {:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is stupid."}) - {:ok, _} = CommonAPI.repeat(activity_one.id, other_user) - - {:ok, list} = Pleroma.List.create("name", user) - {:ok, list} = Pleroma.List.follow(list, other_user) - - conn = get(conn, "/api/v1/timelines/list/#{list.id}") - - assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok) - - assert id == to_string(activity_two.id) - end - - test "works with pagination", %{user: user, conn: conn} do - other_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) - {:ok, list} = Pleroma.List.follow(list, other_user) - - Enum.each(1..30, fn i -> - CommonAPI.post(other_user, %{status: "post number #{i}"}) - end) - - res = - get(conn, "/api/v1/timelines/list/#{list.id}?limit=1") - |> json_response_and_validate_schema(:ok) - - assert length(res) == 1 - - [first] = res - - res = - get(conn, "/api/v1/timelines/list/#{list.id}?max_id=#{first["id"]}&limit=30") - |> json_response_and_validate_schema(:ok) - - assert length(res) == 29 - end - - test "list timeline", %{user: user, conn: conn} do - other_user = insert(:user) - {:ok, _activity_one} = CommonAPI.post(user, %{status: "Marisa is cute."}) - {:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is cute."}) - {:ok, list} = Pleroma.List.create("name", user) - {:ok, list} = Pleroma.List.follow(list, other_user) - - conn = get(conn, "/api/v1/timelines/list/#{list.id}") - - assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok) - - assert id == to_string(activity_two.id) - end - - test "list timeline does not leak non-public statuses for unfollowed users", %{ - user: user, - conn: conn - } do - other_user = insert(:user) - {:ok, activity_one} = CommonAPI.post(other_user, %{status: "Marisa is cute."}) - - {:ok, _activity_two} = - CommonAPI.post(other_user, %{ - status: "Marisa is cute.", - visibility: "private" - }) - - {:ok, list} = Pleroma.List.create("name", user) - {:ok, list} = Pleroma.List.follow(list, other_user) - - conn = get(conn, "/api/v1/timelines/list/#{list.id}") - - assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok) - - assert id == to_string(activity_one.id) - end - end - - describe "hashtag" do - setup do: oauth_access(["n/a"]) - - @tag capture_log: true - test "hashtag timeline", %{conn: conn} do - following = insert(:user) - - {:ok, activity} = CommonAPI.post(following, %{status: "test #2hu"}) - - nconn = get(conn, "/api/v1/timelines/tag/2hu") - - assert [%{"id" => id}] = json_response_and_validate_schema(nconn, :ok) - - assert id == to_string(activity.id) - - # works for different capitalization too - nconn = get(conn, "/api/v1/timelines/tag/2HU") - - assert [%{"id" => id}] = json_response_and_validate_schema(nconn, :ok) - - assert id == to_string(activity.id) - end - - test "multi-hashtag timeline", %{conn: conn} do - user = insert(:user) - - {:ok, activity_test} = CommonAPI.post(user, %{status: "#test"}) - {:ok, activity_test1} = CommonAPI.post(user, %{status: "#test #test1"}) - {:ok, activity_none} = CommonAPI.post(user, %{status: "#test #none"}) - - any_test = get(conn, "/api/v1/timelines/tag/test?any[]=test1") - - [status_none, status_test1, status_test] = json_response_and_validate_schema(any_test, :ok) - - assert to_string(activity_test.id) == status_test["id"] - assert to_string(activity_test1.id) == status_test1["id"] - assert to_string(activity_none.id) == status_none["id"] - - restricted_test = get(conn, "/api/v1/timelines/tag/test?all[]=test1&none[]=none") - - assert [status_test1] == json_response_and_validate_schema(restricted_test, :ok) - - all_test = get(conn, "/api/v1/timelines/tag/test?all[]=none") - - assert [status_none] == json_response_and_validate_schema(all_test, :ok) - end - end - - describe "hashtag timeline handling of :restrict_unauthenticated setting" do - setup do - user = insert(:user) - {:ok, activity1} = CommonAPI.post(user, %{status: "test #tag1"}) - {:ok, _activity2} = CommonAPI.post(user, %{status: "test #tag1"}) - - activity1 - |> Ecto.Changeset.change(%{local: false}) - |> Pleroma.Repo.update() - - base_uri = "/api/v1/timelines/tag/tag1" - error_response = %{"error" => "authorization required for timeline view"} - - %{base_uri: base_uri, error_response: error_response} - end - - defp ensure_authenticated_access(base_uri) do - %{conn: auth_conn} = oauth_access(["read:statuses"]) - - res_conn = get(auth_conn, "#{base_uri}?local=true") - assert length(json_response(res_conn, 200)) == 1 - - res_conn = get(auth_conn, "#{base_uri}?local=false") - assert length(json_response(res_conn, 200)) == 2 - end - - test "with default settings on private instances, returns 403 for unauthenticated users", %{ - conn: conn, - base_uri: base_uri, - error_response: error_response - } do - clear_config([:instance, :public], false) - clear_config([:restrict_unauthenticated, :timelines]) - - for local <- [true, false] do - res_conn = get(conn, "#{base_uri}?local=#{local}") - - assert json_response(res_conn, :unauthorized) == error_response - end - - ensure_authenticated_access(base_uri) - end - - test "with `%{local: true, federated: true}`, returns 403 for unauthenticated users", %{ - conn: conn, - base_uri: base_uri, - error_response: error_response - } do - clear_config([:restrict_unauthenticated, :timelines, :local], true) - clear_config([:restrict_unauthenticated, :timelines, :federated], true) - - for local <- [true, false] do - res_conn = get(conn, "#{base_uri}?local=#{local}") - - assert json_response(res_conn, :unauthorized) == error_response - end - - ensure_authenticated_access(base_uri) - end - - test "with `%{local: false, federated: true}`, forbids unauthenticated access to federated timeline", - %{conn: conn, base_uri: base_uri, error_response: error_response} do - clear_config([:restrict_unauthenticated, :timelines, :local], false) - clear_config([:restrict_unauthenticated, :timelines, :federated], true) - - res_conn = get(conn, "#{base_uri}?local=true") - assert length(json_response(res_conn, 200)) == 1 - - res_conn = get(conn, "#{base_uri}?local=false") - assert json_response(res_conn, :unauthorized) == error_response - - ensure_authenticated_access(base_uri) - end - - test "with `%{local: true, federated: false}`, forbids unauthenticated access to public timeline" <> - "(but not to local public activities which are delivered as part of federated timeline)", - %{conn: conn, base_uri: base_uri, error_response: error_response} do - clear_config([:restrict_unauthenticated, :timelines, :local], true) - clear_config([:restrict_unauthenticated, :timelines, :federated], false) - - res_conn = get(conn, "#{base_uri}?local=true") - assert json_response(res_conn, :unauthorized) == error_response - - # Note: local activities get delivered as part of federated timeline - res_conn = get(conn, "#{base_uri}?local=false") - assert length(json_response(res_conn, 200)) == 2 - - ensure_authenticated_access(base_uri) - end - end -end diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -1,34 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do - use Pleroma.Web.ConnCase - - describe "empty_array/2 (stubs)" do - test "GET /api/v1/accounts/:id/identity_proofs" do - %{user: user, conn: conn} = oauth_access(["read:accounts"]) - - assert [] == - conn - |> get("/api/v1/accounts/#{user.id}/identity_proofs") - |> json_response(200) - end - - test "GET /api/v1/endorsements" do - %{conn: conn} = oauth_access(["read:accounts"]) - - assert [] == - conn - |> get("/api/v1/endorsements") - |> json_response(200) - end - - test "GET /api/v1/trends", %{conn: conn} do - assert [] == - conn - |> get("/api/v1/trends") - |> json_response(200) - end - end -end diff --git a/test/web/mastodon_api/mastodon_api_test.exs b/test/web/mastodon_api/mastodon_api_test.exs @@ -1,103 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.MastodonAPITest do - use Pleroma.Web.ConnCase - - alias Pleroma.Notification - alias Pleroma.ScheduledActivity - alias Pleroma.User - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.MastodonAPI - - import Pleroma.Factory - - describe "follow/3" do - test "returns error when followed user is deactivated" do - follower = insert(:user) - user = insert(:user, local: true, deactivated: true) - assert {:error, _error} = MastodonAPI.follow(follower, user) - end - - test "following for user" do - follower = insert(:user) - user = insert(:user) - {:ok, follower} = MastodonAPI.follow(follower, user) - assert User.following?(follower, user) - end - - test "returns ok if user already followed" do - follower = insert(:user) - user = insert(:user) - {:ok, follower} = User.follow(follower, user) - {:ok, follower} = MastodonAPI.follow(follower, refresh_record(user)) - assert User.following?(follower, user) - end - end - - describe "get_followers/2" do - test "returns user followers" do - follower1_user = insert(:user) - follower2_user = insert(:user) - user = insert(:user) - {:ok, _follower1_user} = User.follow(follower1_user, user) - {:ok, follower2_user} = User.follow(follower2_user, user) - - assert MastodonAPI.get_followers(user, %{"limit" => 1}) == [follower2_user] - end - end - - describe "get_friends/2" do - test "returns user friends" do - user = insert(:user) - followed_one = insert(:user) - followed_two = insert(:user) - followed_three = insert(:user) - - {:ok, user} = User.follow(user, followed_one) - {:ok, user} = User.follow(user, followed_two) - {:ok, user} = User.follow(user, followed_three) - res = MastodonAPI.get_friends(user) - - assert length(res) == 3 - assert Enum.member?(res, refresh_record(followed_three)) - assert Enum.member?(res, refresh_record(followed_two)) - assert Enum.member?(res, refresh_record(followed_one)) - end - end - - describe "get_notifications/2" do - test "returns notifications for user" do - user = insert(:user) - subscriber = insert(:user) - - User.subscribe(subscriber, user) - - {:ok, status} = CommonAPI.post(user, %{status: "Akariiiin"}) - - {:ok, status1} = CommonAPI.post(user, %{status: "Magi"}) - {:ok, [notification]} = Notification.create_notifications(status) - {:ok, [notification1]} = Notification.create_notifications(status1) - res = MastodonAPI.get_notifications(subscriber) - - assert Enum.member?(Enum.map(res, & &1.id), notification.id) - assert Enum.member?(Enum.map(res, & &1.id), notification1.id) - end - end - - describe "get_scheduled_activities/2" do - test "returns user scheduled activities" do - user = insert(:user) - - today = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(6), :millisecond) - |> NaiveDateTime.to_iso8601() - - attrs = %{params: %{}, scheduled_at: today} - {:ok, schedule} = ScheduledActivity.create(user, attrs) - assert MastodonAPI.get_scheduled_activities(user) == [schedule] - end - end -end diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs @@ -1,575 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.AccountViewTest do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.User - alias Pleroma.UserRelationship - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.AccountView - - import Pleroma.Factory - import Tesla.Mock - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "Represent a user account" do - background_image = %{ - "url" => [%{"href" => "https://example.com/images/asuka_hospital.png"}] - } - - user = - insert(:user, %{ - follower_count: 3, - note_count: 5, - background: background_image, - nickname: "shp@shitposter.club", - name: ":karjalanpiirakka: shp", - bio: - "<script src=\"invalid-html\"></script><span>valid html</span>. a<br>b<br/>c<br >d<br />f '&<>\"", - inserted_at: ~N[2017-08-15 15:47:06.597036], - emoji: %{"karjalanpiirakka" => "/file.png"}, - raw_bio: "valid html. a\nb\nc\nd\nf '&<>\"" - }) - - expected = %{ - id: to_string(user.id), - username: "shp", - acct: user.nickname, - display_name: user.name, - locked: false, - created_at: "2017-08-15T15:47:06.000Z", - followers_count: 3, - following_count: 0, - statuses_count: 5, - note: "<span>valid html</span>. a<br/>b<br/>c<br/>d<br/>f &#39;&amp;&lt;&gt;&quot;", - url: user.ap_id, - avatar: "http://localhost:4001/images/avi.png", - avatar_static: "http://localhost:4001/images/avi.png", - header: "http://localhost:4001/images/banner.png", - header_static: "http://localhost:4001/images/banner.png", - emojis: [ - %{ - static_url: "/file.png", - url: "/file.png", - shortcode: "karjalanpiirakka", - visible_in_picker: false - } - ], - fields: [], - bot: false, - source: %{ - note: "valid html. a\nb\nc\nd\nf '&<>\"", - sensitive: false, - pleroma: %{ - actor_type: "Person", - discoverable: true - }, - fields: [] - }, - pleroma: %{ - ap_id: user.ap_id, - background_image: "https://example.com/images/asuka_hospital.png", - favicon: nil, - confirmation_pending: false, - tags: [], - is_admin: false, - is_moderator: false, - hide_favorites: true, - hide_followers: false, - hide_follows: false, - hide_followers_count: false, - hide_follows_count: false, - relationship: %{}, - skip_thread_containment: false, - accepts_chat_messages: nil - } - } - - assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - end - - describe "favicon" do - setup do - [user: insert(:user)] - end - - test "is parsed when :instance_favicons is enabled", %{user: user} do - clear_config([:instances_favicons, :enabled], true) - - assert %{ - pleroma: %{ - favicon: - "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png" - } - } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - end - - test "is nil when :instances_favicons is disabled", %{user: user} do - assert %{pleroma: %{favicon: nil}} = - AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - end - end - - test "Represent the user account for the account owner" do - user = insert(:user) - - notification_settings = %{ - block_from_strangers: false, - hide_notification_contents: false - } - - privacy = user.default_scope - - assert %{ - pleroma: %{notification_settings: ^notification_settings, allow_following_move: true}, - source: %{privacy: ^privacy} - } = AccountView.render("show.json", %{user: user, for: user}) - end - - test "Represent a Service(bot) account" do - user = - insert(:user, %{ - follower_count: 3, - note_count: 5, - actor_type: "Service", - nickname: "shp@shitposter.club", - inserted_at: ~N[2017-08-15 15:47:06.597036] - }) - - expected = %{ - id: to_string(user.id), - username: "shp", - acct: user.nickname, - display_name: user.name, - locked: false, - created_at: "2017-08-15T15:47:06.000Z", - followers_count: 3, - following_count: 0, - statuses_count: 5, - note: user.bio, - url: user.ap_id, - avatar: "http://localhost:4001/images/avi.png", - avatar_static: "http://localhost:4001/images/avi.png", - header: "http://localhost:4001/images/banner.png", - header_static: "http://localhost:4001/images/banner.png", - emojis: [], - fields: [], - bot: true, - source: %{ - note: user.bio, - sensitive: false, - pleroma: %{ - actor_type: "Service", - discoverable: true - }, - fields: [] - }, - pleroma: %{ - ap_id: user.ap_id, - background_image: nil, - favicon: nil, - confirmation_pending: false, - tags: [], - is_admin: false, - is_moderator: false, - hide_favorites: true, - hide_followers: false, - hide_follows: false, - hide_followers_count: false, - hide_follows_count: false, - relationship: %{}, - skip_thread_containment: false, - accepts_chat_messages: nil - } - } - - assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - end - - test "Represent a Funkwhale channel" do - {:ok, user} = - User.get_or_fetch_by_ap_id( - "https://channels.tests.funkwhale.audio/federation/actors/compositions" - ) - - assert represented = - AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - - assert represented.acct == "compositions@channels.tests.funkwhale.audio" - 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) - deactivated_user = insert(:user, deactivated: true) - represented = AccountView.render("show.json", %{user: deactivated_user, for: admin}) - assert represented[:pleroma][:deactivated] == true - end - - test "Represent a smaller mention" do - user = insert(:user) - - expected = %{ - id: to_string(user.id), - acct: user.nickname, - username: user.nickname, - url: user.ap_id - } - - assert expected == AccountView.render("mention.json", %{user: user}) - end - - test "demands :for or :skip_visibility_check option for account rendering" do - clear_config([:restrict_unauthenticated, :profiles, :local], false) - - user = insert(:user) - user_id = user.id - - assert %{id: ^user_id} = AccountView.render("show.json", %{user: user, for: nil}) - assert %{id: ^user_id} = AccountView.render("show.json", %{user: user, for: user}) - - assert %{id: ^user_id} = - AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - - assert_raise RuntimeError, ~r/:skip_visibility_check or :for option is required/, fn -> - AccountView.render("show.json", %{user: user}) - end - end - - describe "relationship" do - defp test_relationship_rendering(user, other_user, expected_result) do - opts = %{user: user, target: other_user, relationships: nil} - assert expected_result == AccountView.render("relationship.json", opts) - - relationships_opt = UserRelationship.view_relationships_option(user, [other_user]) - opts = Map.put(opts, :relationships, relationships_opt) - assert expected_result == AccountView.render("relationship.json", opts) - - assert [expected_result] == - AccountView.render("relationships.json", %{user: user, targets: [other_user]}) - end - - @blank_response %{ - following: false, - followed_by: false, - blocking: false, - blocked_by: false, - muting: false, - muting_notifications: false, - subscribing: false, - requested: false, - domain_blocking: false, - showing_reblogs: true, - endorsed: false - } - - test "represent a relationship for the following and followed user" do - user = insert(:user) - other_user = insert(:user) - - {:ok, user} = User.follow(user, other_user) - {:ok, other_user} = User.follow(other_user, user) - {:ok, _subscription} = User.subscribe(user, other_user) - {:ok, _user_relationships} = User.mute(user, other_user, true) - {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user) - - expected = - Map.merge( - @blank_response, - %{ - following: true, - followed_by: true, - muting: true, - muting_notifications: true, - subscribing: true, - showing_reblogs: false, - id: to_string(other_user.id) - } - ) - - test_relationship_rendering(user, other_user, expected) - end - - test "represent a relationship for the blocking and blocked user" do - user = insert(:user) - other_user = insert(:user) - - {:ok, user} = User.follow(user, other_user) - {:ok, _subscription} = User.subscribe(user, other_user) - {:ok, _user_relationship} = User.block(user, other_user) - {:ok, _user_relationship} = User.block(other_user, user) - - expected = - Map.merge( - @blank_response, - %{following: false, blocking: true, blocked_by: true, id: to_string(other_user.id)} - ) - - test_relationship_rendering(user, other_user, expected) - end - - test "represent a relationship for the user blocking a domain" do - user = insert(:user) - other_user = insert(:user, ap_id: "https://bad.site/users/other_user") - - {:ok, user} = User.block_domain(user, "bad.site") - - expected = - Map.merge( - @blank_response, - %{domain_blocking: true, blocking: false, id: to_string(other_user.id)} - ) - - test_relationship_rendering(user, other_user, expected) - end - - test "represent a relationship for the user with a pending follow request" do - user = insert(:user) - other_user = insert(:user, locked: true) - - {:ok, user, other_user, _} = CommonAPI.follow(user, other_user) - user = User.get_cached_by_id(user.id) - other_user = User.get_cached_by_id(other_user.id) - - expected = - Map.merge( - @blank_response, - %{requested: true, following: false, id: to_string(other_user.id)} - ) - - test_relationship_rendering(user, other_user, expected) - end - end - - test "returns the settings store if the requesting user is the represented user and it's requested specifically" do - user = insert(:user, pleroma_settings_store: %{fe: "test"}) - - result = - AccountView.render("show.json", %{user: user, for: user, with_pleroma_settings: true}) - - assert result.pleroma.settings_store == %{:fe => "test"} - - result = AccountView.render("show.json", %{user: user, for: nil, with_pleroma_settings: true}) - assert result.pleroma[:settings_store] == nil - - result = AccountView.render("show.json", %{user: user, for: user}) - assert result.pleroma[:settings_store] == nil - end - - test "doesn't sanitize display names" do - user = insert(:user, name: "<marquee> username </marquee>") - result = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - assert result.display_name == "<marquee> username </marquee>" - end - - test "never display nil user follow counts" do - user = insert(:user, following_count: 0, follower_count: 0) - result = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - - assert result.following_count == 0 - assert result.followers_count == 0 - end - - describe "hiding follows/following" do - test "shows when follows/followers stats are hidden and sets follow/follower count to 0" do - user = - insert(:user, %{ - hide_followers: true, - hide_followers_count: true, - hide_follows: true, - hide_follows_count: true - }) - - other_user = insert(:user) - {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - - assert %{ - followers_count: 0, - following_count: 0, - pleroma: %{hide_follows_count: true, hide_followers_count: true} - } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - end - - test "shows when follows/followers are hidden" do - user = insert(:user, hide_followers: true, hide_follows: true) - other_user = insert(:user) - {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - - assert %{ - followers_count: 1, - following_count: 1, - pleroma: %{hide_follows: true, hide_followers: true} - } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - end - - test "shows actual follower/following count to the account owner" do - user = insert(:user, hide_followers: true, hide_follows: true) - other_user = insert(:user) - {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) - - assert User.following?(user, other_user) - assert Pleroma.FollowingRelationship.follower_count(other_user) == 1 - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - - assert %{ - followers_count: 1, - following_count: 1 - } = AccountView.render("show.json", %{user: user, for: user}) - end - - test "shows unread_conversation_count only to the account owner" do - user = insert(:user) - other_user = insert(:user) - - {:ok, _activity} = - CommonAPI.post(other_user, %{ - status: "Hey @#{user.nickname}.", - visibility: "direct" - }) - - user = User.get_cached_by_ap_id(user.ap_id) - - assert AccountView.render("show.json", %{user: user, for: other_user})[:pleroma][ - :unread_conversation_count - ] == nil - - assert AccountView.render("show.json", %{user: user, for: user})[:pleroma][ - :unread_conversation_count - ] == 1 - end - - test "shows unread_count only to the account owner" do - user = insert(:user) - insert_list(7, :notification, user: user, activity: insert(:note_activity)) - other_user = insert(:user) - - user = User.get_cached_by_ap_id(user.ap_id) - - assert AccountView.render( - "show.json", - %{user: user, for: other_user} - )[:pleroma][:unread_notifications_count] == nil - - assert AccountView.render( - "show.json", - %{user: user, for: user} - )[:pleroma][:unread_notifications_count] == 7 - end - end - - describe "follow requests counter" do - test "shows zero when no follow requests are pending" do - user = insert(:user) - - assert %{follow_requests_count: 0} = - AccountView.render("show.json", %{user: user, for: user}) - - other_user = insert(:user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - - assert %{follow_requests_count: 0} = - AccountView.render("show.json", %{user: user, for: user}) - end - - test "shows non-zero when follow requests are pending" do - user = insert(:user, locked: true) - - assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) - - other_user = insert(:user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - - assert %{locked: true, follow_requests_count: 1} = - AccountView.render("show.json", %{user: user, for: user}) - end - - test "decreases when accepting a follow request" do - user = insert(:user, locked: true) - - assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) - - other_user = insert(:user) - {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user) - - assert %{locked: true, follow_requests_count: 1} = - AccountView.render("show.json", %{user: user, for: user}) - - {:ok, _other_user} = CommonAPI.accept_follow_request(other_user, user) - - assert %{locked: true, follow_requests_count: 0} = - AccountView.render("show.json", %{user: user, for: user}) - end - - test "decreases when rejecting a follow request" do - user = insert(:user, locked: true) - - assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) - - other_user = insert(:user) - {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user) - - assert %{locked: true, follow_requests_count: 1} = - AccountView.render("show.json", %{user: user, for: user}) - - {:ok, _other_user} = CommonAPI.reject_follow_request(other_user, user) - - assert %{locked: true, follow_requests_count: 0} = - AccountView.render("show.json", %{user: user, for: user}) - end - - test "shows non-zero when historical unapproved requests are present" do - user = insert(:user, locked: true) - - assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) - - other_user = insert(:user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - - {:ok, user} = User.update_and_set_cache(user, %{locked: false}) - - assert %{locked: false, follow_requests_count: 1} = - AccountView.render("show.json", %{user: user, for: user}) - end - end - - test "uses mediaproxy urls when it's enabled (regardless of media preview proxy state)" do - clear_config([:media_proxy, :enabled], true) - clear_config([:media_preview_proxy, :enabled]) - - user = - insert(:user, - avatar: %{"url" => [%{"href" => "https://evil.website/avatar.png"}]}, - banner: %{"url" => [%{"href" => "https://evil.website/banner.png"}]}, - emoji: %{"joker_smile" => "https://evil.website/society.png"} - ) - - with media_preview_enabled <- [false, true] do - Config.put([:media_preview_proxy, :enabled], media_preview_enabled) - - AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - |> Enum.all?(fn - {key, url} when key in [:avatar, :avatar_static, :header, :header_static] -> - String.starts_with?(url, Pleroma.Web.base_url()) - - {:emojis, emojis} -> - Enum.all?(emojis, fn %{url: url, static_url: static_url} -> - String.starts_with?(url, Pleroma.Web.base_url()) && - String.starts_with?(static_url, Pleroma.Web.base_url()) - end) - - _ -> - true - end) - |> assert() - end - end -end diff --git a/test/web/mastodon_api/views/conversation_view_test.exs b/test/web/mastodon_api/views/conversation_view_test.exs @@ -1,44 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.ConversationViewTest do - use Pleroma.DataCase - - alias Pleroma.Conversation.Participation - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.ConversationView - - import Pleroma.Factory - - test "represents a Mastodon Conversation entity" do - user = insert(:user) - other_user = insert(:user) - - {:ok, parent} = CommonAPI.post(user, %{status: "parent"}) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "hey @#{other_user.nickname}", - visibility: "direct", - in_reply_to_id: parent.id - }) - - {:ok, _reply_activity} = - CommonAPI.post(user, %{status: "hu", visibility: "public", in_reply_to_id: parent.id}) - - [participation] = Participation.for_user_with_last_activity_id(user) - - assert participation - - conversation = - ConversationView.render("participation.json", %{participation: participation, for: user}) - - assert conversation.id == participation.id |> to_string() - assert conversation.last_status.id == activity.id - - assert [account] = conversation.accounts - assert account.id == other_user.id - assert conversation.last_status.pleroma.direct_conversation_id == participation.id - end -end diff --git a/test/web/mastodon_api/views/list_view_test.exs b/test/web/mastodon_api/views/list_view_test.exs @@ -1,32 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.ListViewTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Web.MastodonAPI.ListView - - test "show" do - user = insert(:user) - title = "mortal enemies" - {:ok, list} = Pleroma.List.create(title, user) - - expected = %{ - id: to_string(list.id), - title: title - } - - assert expected == ListView.render("show.json", %{list: list}) - end - - test "index" do - user = insert(:user) - - {:ok, list} = Pleroma.List.create("my list", user) - {:ok, list2} = Pleroma.List.create("cofe", user) - - assert [%{id: _, title: "my list"}, %{id: _, title: "cofe"}] = - ListView.render("index.json", lists: [list, list2]) - end -end diff --git a/test/web/mastodon_api/views/marker_view_test.exs b/test/web/mastodon_api/views/marker_view_test.exs @@ -1,29 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do - use Pleroma.DataCase - alias Pleroma.Web.MastodonAPI.MarkerView - import Pleroma.Factory - - test "returns markers" do - marker1 = insert(:marker, timeline: "notifications", last_read_id: "17", unread_count: 5) - marker2 = insert(:marker, timeline: "home", last_read_id: "42") - - assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{ - "home" => %{ - last_read_id: "42", - updated_at: NaiveDateTime.to_iso8601(marker2.updated_at), - version: 0, - pleroma: %{unread_count: 0} - }, - "notifications" => %{ - last_read_id: "17", - updated_at: NaiveDateTime.to_iso8601(marker1.updated_at), - version: 0, - pleroma: %{unread_count: 5} - } - } - end -end diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs @@ -1,231 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Chat - alias Pleroma.Chat.MessageReference - alias Pleroma.Notification - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.CommonAPI.Utils - alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.MastodonAPI.NotificationView - alias Pleroma.Web.MastodonAPI.StatusView - alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView - import Pleroma.Factory - - defp test_notifications_rendering(notifications, user, expected_result) do - result = NotificationView.render("index.json", %{notifications: notifications, for: user}) - - assert expected_result == result - - result = - NotificationView.render("index.json", %{ - notifications: notifications, - for: user, - relationships: nil - }) - - assert expected_result == result - end - - test "ChatMessage notification" do - user = insert(:user) - recipient = insert(:user) - {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "what's up my dude") - - {:ok, [notification]} = Notification.create_notifications(activity) - - object = Object.normalize(activity) - chat = Chat.get(recipient.id, user.ap_id) - - cm_ref = MessageReference.for_chat_and_object(chat, object) - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: false, is_muted: false}, - type: "pleroma:chat_mention", - account: AccountView.render("show.json", %{user: user, for: recipient}), - chat_message: MessageReferenceView.render("show.json", %{chat_message_reference: cm_ref}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - test_notifications_rendering([notification], recipient, [expected]) - end - - test "Mention notification" do - user = insert(:user) - mentioned_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{mentioned_user.nickname}"}) - {: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: "mention", - account: - AccountView.render("show.json", %{ - user: user, - for: mentioned_user - }), - status: StatusView.render("show.json", %{activity: activity, for: mentioned_user}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - test_notifications_rendering([notification], mentioned_user, [expected]) - end - - test "Favourite notification" do - user = insert(:user) - another_user = insert(:user) - {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, favorite_activity} = CommonAPI.favorite(another_user, create_activity.id) - {:ok, [notification]} = Notification.create_notifications(favorite_activity) - create_activity = Activity.get_by_id(create_activity.id) - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: false, is_muted: false}, - type: "favourite", - account: AccountView.render("show.json", %{user: another_user, for: user}), - status: StatusView.render("show.json", %{activity: create_activity, for: user}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - test_notifications_rendering([notification], user, [expected]) - end - - test "Reblog notification" do - user = insert(:user) - another_user = insert(:user) - {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, another_user) - {:ok, [notification]} = Notification.create_notifications(reblog_activity) - reblog_activity = Activity.get_by_id(create_activity.id) - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: false, is_muted: false}, - type: "reblog", - account: AccountView.render("show.json", %{user: another_user, for: user}), - status: StatusView.render("show.json", %{activity: reblog_activity, for: user}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - test_notifications_rendering([notification], user, [expected]) - end - - test "Follow notification" do - follower = insert(:user) - followed = insert(:user) - {:ok, follower, followed, _activity} = CommonAPI.follow(follower, followed) - notification = Notification |> Repo.one() |> Repo.preload(:activity) - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: false, is_muted: false}, - type: "follow", - account: AccountView.render("show.json", %{user: follower, for: followed}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - test_notifications_rendering([notification], followed, [expected]) - - User.perform(:delete, follower) - refute Repo.one(Notification) - end - - @tag capture_log: true - test "Move notification" do - old_user = insert(:user) - new_user = insert(:user, also_known_as: [old_user.ap_id]) - follower = insert(:user) - - old_user_url = old_user.ap_id - - body = - File.read!("test/fixtures/users_mock/localhost.json") - |> String.replace("{{nickname}}", old_user.nickname) - |> Jason.encode!() - - Tesla.Mock.mock(fn - %{method: :get, url: ^old_user_url} -> - %Tesla.Env{status: 200, body: body} - end) - - User.follow(follower, old_user) - Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) - Pleroma.Tests.ObanHelpers.perform_all() - - old_user = refresh_record(old_user) - new_user = refresh_record(new_user) - - [notification] = Notification.for_user(follower) - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: false, is_muted: false}, - type: "move", - account: AccountView.render("show.json", %{user: old_user, for: follower}), - target: AccountView.render("show.json", %{user: new_user, for: follower}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - test_notifications_rendering([notification], follower, [expected]) - end - - test "EmojiReact notification" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - {:ok, _activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") - - 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: "☕", - 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) - } - - test_notifications_rendering([notification], user, [expected]) - end - - test "muted notification" do - user = insert(:user) - another_user = insert(:user) - - {:ok, _} = Pleroma.UserRelationship.create_mute(user, another_user) - {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, favorite_activity} = CommonAPI.favorite(another_user, create_activity.id) - {:ok, [notification]} = Notification.create_notifications(favorite_activity) - create_activity = Activity.get_by_id(create_activity.id) - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: true, is_muted: true}, - type: "favourite", - account: AccountView.render("show.json", %{user: another_user, for: user}), - status: StatusView.render("show.json", %{activity: create_activity, for: user}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - test_notifications_rendering([notification], user, [expected]) - end -end diff --git a/test/web/mastodon_api/views/poll_view_test.exs b/test/web/mastodon_api/views/poll_view_test.exs @@ -1,167 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.PollViewTest do - use Pleroma.DataCase - - alias Pleroma.Object - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.PollView - - import Pleroma.Factory - import Tesla.Mock - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "renders a poll" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "Is Tenshi eating a corndog cute?", - poll: %{ - options: ["absolutely!", "sure", "yes", "why are you even asking?"], - expires_in: 20 - } - }) - - object = Object.normalize(activity) - - expected = %{ - emojis: [], - expired: false, - id: to_string(object.id), - multiple: false, - options: [ - %{title: "absolutely!", votes_count: 0}, - %{title: "sure", votes_count: 0}, - %{title: "yes", votes_count: 0}, - %{title: "why are you even asking?", votes_count: 0} - ], - voted: false, - votes_count: 0, - voters_count: nil - } - - result = PollView.render("show.json", %{object: object}) - expires_at = result.expires_at - result = Map.delete(result, :expires_at) - - assert result == expected - - expires_at = NaiveDateTime.from_iso8601!(expires_at) - assert NaiveDateTime.diff(expires_at, NaiveDateTime.utc_now()) in 15..20 - end - - test "detects if it is multiple choice" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "Which Mastodon developer is your favourite?", - poll: %{ - options: ["Gargron", "Eugen"], - expires_in: 20, - multiple: true - } - }) - - voter = insert(:user) - - object = Object.normalize(activity) - - {:ok, _votes, object} = CommonAPI.vote(voter, object, [0, 1]) - - assert match?( - %{ - multiple: true, - voters_count: 1, - votes_count: 2 - }, - PollView.render("show.json", %{object: object}) - ) - end - - test "detects emoji" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "What's with the smug face?", - poll: %{ - options: [":blank: sip", ":blank::blank: sip", ":blank::blank::blank: sip"], - expires_in: 20 - } - }) - - object = Object.normalize(activity) - - assert %{emojis: [%{shortcode: "blank"}]} = PollView.render("show.json", %{object: object}) - end - - test "detects vote status" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "Which input devices do you use?", - poll: %{ - options: ["mouse", "trackball", "trackpoint"], - multiple: true, - expires_in: 20 - } - }) - - object = Object.normalize(activity) - - {:ok, _, object} = CommonAPI.vote(other_user, object, [1, 2]) - - result = PollView.render("show.json", %{object: object, for: other_user}) - - assert result[:voted] == true - assert Enum.at(result[:options], 1)[:votes_count] == 1 - assert Enum.at(result[:options], 2)[:votes_count] == 1 - end - - test "does not crash on polls with no end date" do - object = Object.normalize("https://skippers-bin.com/notes/7x9tmrp97i") - result = PollView.render("show.json", %{object: object}) - - assert result[:expires_at] == nil - assert result[:expired] == false - end - - test "doesn't strips HTML tags" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "What's with the smug face?", - poll: %{ - options: [ - "<input type=\"date\">", - "<input type=\"date\" >", - "<input type=\"date\"/>", - "<input type=\"date\"></input>" - ], - expires_in: 20 - } - }) - - object = Object.normalize(activity) - - assert %{ - options: [ - %{title: "<input type=\"date\">", votes_count: 0}, - %{title: "<input type=\"date\" >", votes_count: 0}, - %{title: "<input type=\"date\"/>", votes_count: 0}, - %{title: "<input type=\"date\"></input>", votes_count: 0} - ] - } = PollView.render("show.json", %{object: object}) - end -end diff --git a/test/web/mastodon_api/views/scheduled_activity_view_test.exs b/test/web/mastodon_api/views/scheduled_activity_view_test.exs @@ -1,68 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.ScheduledActivityViewTest do - use Pleroma.DataCase - alias Pleroma.ScheduledActivity - 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 Pleroma.Factory - - test "A scheduled activity with a media attachment" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "hi"}) - - scheduled_at = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(10), :millisecond) - |> NaiveDateTime.to_iso8601() - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) - - attrs = %{ - params: %{ - "media_ids" => [upload.id], - "status" => "hi", - "sensitive" => true, - "spoiler_text" => "spoiler", - "visibility" => "unlisted", - "in_reply_to_id" => to_string(activity.id) - }, - scheduled_at: scheduled_at - } - - {:ok, scheduled_activity} = ScheduledActivity.create(user, attrs) - result = ScheduledActivityView.render("show.json", %{scheduled_activity: scheduled_activity}) - - expected = %{ - id: to_string(scheduled_activity.id), - media_attachments: - %{media_ids: [upload.id]} - |> Utils.attachments_from_ids() - |> Enum.map(&StatusView.render("attachment.json", %{attachment: &1})), - params: %{ - in_reply_to_id: to_string(activity.id), - media_ids: [upload.id], - poll: nil, - scheduled_at: nil, - sensitive: true, - spoiler_text: "spoiler", - text: "hi", - visibility: "unlisted" - }, - scheduled_at: Utils.to_masto_date(scheduled_activity.scheduled_at) - } - - assert expected == result - end -end diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs @@ -1,664 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.StatusViewTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Bookmark - alias Pleroma.Conversation.Participation - alias Pleroma.HTML - alias Pleroma.Object - alias Pleroma.Repo - 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 - - import Pleroma.Factory - import Tesla.Mock - import OpenApiSpex.TestAssertions - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "has an emoji reaction list" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "dae cofe??"}) - - {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "☕") - {:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵") - {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") - 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} - ] - - status = StatusView.render("show.json", activity: activity, for: user) - - assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) - - assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 2, me: true}, - %{name: "🍵", count: 1, me: false} - ] - end - - test "works correctly with badly formatted emojis" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "yo"}) - - activity - |> Object.normalize(false) - |> 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} - ] - end - - test "loads and returns the direct conversation id when given the `with_direct_conversation_id` option" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"}) - [participation] = Participation.for_user(user) - - status = - StatusView.render("show.json", - activity: activity, - with_direct_conversation_id: true, - for: user - ) - - assert status[:pleroma][:direct_conversation_id] == participation.id - - status = StatusView.render("show.json", activity: activity, for: user) - assert status[:pleroma][:direct_conversation_id] == nil - assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "returns the direct conversation id when given the `direct_conversation_id` option" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"}) - [participation] = Participation.for_user(user) - - status = - StatusView.render("show.json", - activity: activity, - direct_conversation_id: participation.id, - for: user - ) - - assert status[:pleroma][:direct_conversation_id] == participation.id - assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "returns a temporary ap_id based user for activities missing db users" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"}) - - Repo.delete(user) - Cachex.clear(:user_cache) - - finger_url = - "https://localhost/.well-known/webfinger?resource=acct:#{user.nickname}@localhost" - - Tesla.Mock.mock_global(fn - %{method: :get, url: "http://localhost/.well-known/host-meta"} -> - %Tesla.Env{status: 404, body: ""} - - %{method: :get, url: "https://localhost/.well-known/host-meta"} -> - %Tesla.Env{status: 404, body: ""} - - %{ - method: :get, - url: ^finger_url - } -> - %Tesla.Env{status: 404, body: ""} - end) - - %{account: ms_user} = StatusView.render("show.json", activity: activity) - - assert ms_user.acct == "erroruser@example.com" - end - - test "tries to get a user by nickname if fetching by ap_id doesn't work" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"}) - - {:ok, user} = - user - |> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"}) - |> Repo.update() - - Cachex.clear(:user_cache) - - result = StatusView.render("show.json", activity: activity) - - assert result[:account][:id] == to_string(user.id) - assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "a note with null content" do - note = insert(:note_activity) - note_object = Object.normalize(note) - - data = - note_object.data - |> Map.put("content", nil) - - Object.change(note_object, %{data: data}) - |> Object.update_and_set_cache() - - User.get_cached_by_ap_id(note.data["actor"]) - - status = StatusView.render("show.json", %{activity: note}) - - assert status.content == "" - assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "a note activity" do - note = insert(:note_activity) - object_data = Object.normalize(note).data - user = User.get_cached_by_ap_id(note.data["actor"]) - - convo_id = Utils.context_to_conversation_id(object_data["context"]) - - status = StatusView.render("show.json", %{activity: note}) - - created_at = - (object_data["published"] || "") - |> String.replace(~r/\.\d+Z/, ".000Z") - - expected = %{ - id: to_string(note.id), - uri: object_data["id"], - url: Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, note), - account: AccountView.render("show.json", %{user: user, skip_visibility_check: true}), - in_reply_to_id: nil, - in_reply_to_account_id: nil, - card: nil, - reblog: nil, - content: HTML.filter_tags(object_data["content"]), - text: nil, - created_at: created_at, - reblogs_count: 0, - replies_count: 0, - favourites_count: 0, - reblogged: false, - bookmarked: false, - favourited: false, - muted: false, - pinned: false, - sensitive: false, - poll: nil, - spoiler_text: HTML.filter_tags(object_data["summary"]), - visibility: "public", - media_attachments: [], - mentions: [], - tags: [ - %{ - name: "#{object_data["tag"]}", - url: "/tag/#{object_data["tag"]}" - } - ], - application: %{ - name: "Web", - website: nil - }, - language: nil, - emojis: [ - %{ - shortcode: "2hu", - url: "corndog.png", - static_url: "corndog.png", - visible_in_picker: false - } - ], - pleroma: %{ - local: true, - conversation_id: convo_id, - in_reply_to_account_acct: nil, - content: %{"text/plain" => HTML.strip_tags(object_data["content"])}, - spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])}, - expires_at: nil, - direct_conversation_id: nil, - thread_muted: false, - emoji_reactions: [], - parent_visible: false - } - } - - assert status == expected - assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "tells if the message is muted for some reason" do - user = insert(:user) - other_user = insert(:user) - - {:ok, _user_relationships} = User.mute(user, other_user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "test"}) - - relationships_opt = UserRelationship.view_relationships_option(user, [other_user]) - - opts = %{activity: activity} - status = StatusView.render("show.json", opts) - assert status.muted == false - assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) - - status = StatusView.render("show.json", Map.put(opts, :relationships, relationships_opt)) - assert status.muted == false - - for_opts = %{activity: activity, for: user} - status = StatusView.render("show.json", for_opts) - assert status.muted == true - - status = StatusView.render("show.json", Map.put(for_opts, :relationships, relationships_opt)) - assert status.muted == true - assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "tells if the message is thread muted" do - user = insert(:user) - other_user = insert(:user) - - {:ok, _user_relationships} = User.mute(user, other_user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "test"}) - status = StatusView.render("show.json", %{activity: activity, for: user}) - - assert status.pleroma.thread_muted == false - - {:ok, activity} = CommonAPI.add_mute(user, activity) - - status = StatusView.render("show.json", %{activity: activity, for: user}) - - assert status.pleroma.thread_muted == true - end - - test "tells if the status is bookmarked" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "Cute girls doing cute things"}) - status = StatusView.render("show.json", %{activity: activity}) - - assert status.bookmarked == false - - status = StatusView.render("show.json", %{activity: activity, for: user}) - - assert status.bookmarked == false - - {:ok, _bookmark} = Bookmark.create(user.id, activity.id) - - activity = Activity.get_by_id_with_object(activity.id) - - status = StatusView.render("show.json", %{activity: activity, for: user}) - - assert status.bookmarked == true - end - - test "a reply" do - note = insert(:note_activity) - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "he", in_reply_to_status_id: note.id}) - - status = StatusView.render("show.json", %{activity: activity}) - - assert status.in_reply_to_id == to_string(note.id) - - [status] = StatusView.render("index.json", %{activities: [activity], as: :activity}) - - assert status.in_reply_to_id == to_string(note.id) - end - - test "contains mentions" do - user = insert(:user) - mentioned = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hi @#{mentioned.nickname}"}) - - status = StatusView.render("show.json", %{activity: activity}) - - assert status.mentions == - Enum.map([mentioned], fn u -> AccountView.render("mention.json", %{user: u}) end) - - assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "create mentions from the 'to' field" do - %User{ap_id: recipient_ap_id} = insert(:user) - cc = insert_pair(:user) |> Enum.map(& &1.ap_id) - - object = - insert(:note, %{ - data: %{ - "to" => [recipient_ap_id], - "cc" => cc - } - }) - - activity = - insert(:note_activity, %{ - note: object, - recipients: [recipient_ap_id | cc] - }) - - assert length(activity.recipients) == 3 - - %{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity}) - - assert length(mentions) == 1 - assert mention.url == recipient_ap_id - end - - test "create mentions from the 'tag' field" do - recipient = insert(:user) - cc = insert_pair(:user) |> Enum.map(& &1.ap_id) - - object = - insert(:note, %{ - data: %{ - "cc" => cc, - "tag" => [ - %{ - "href" => recipient.ap_id, - "name" => recipient.nickname, - "type" => "Mention" - }, - %{ - "href" => "https://example.com/search?tag=test", - "name" => "#test", - "type" => "Hashtag" - } - ] - } - }) - - activity = - insert(:note_activity, %{ - note: object, - recipients: [recipient.ap_id | cc] - }) - - assert length(activity.recipients) == 3 - - %{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity}) - - assert length(mentions) == 1 - assert mention.url == recipient.ap_id - end - - test "attachments" do - object = %{ - "type" => "Image", - "url" => [ - %{ - "mediaType" => "image/png", - "href" => "someurl" - } - ], - "uuid" => 6 - } - - expected = %{ - id: "1638338801", - type: "image", - url: "someurl", - remote_url: "someurl", - preview_url: "someurl", - text_url: "someurl", - description: nil, - pleroma: %{mime_type: "image/png"} - } - - api_spec = Pleroma.Web.ApiSpec.spec() - - assert expected == StatusView.render("attachment.json", %{attachment: object}) - assert_schema(expected, "Attachment", api_spec) - - # If theres a "id", use that instead of the generated one - object = Map.put(object, "id", 2) - result = StatusView.render("attachment.json", %{attachment: object}) - - assert %{id: "2"} = result - assert_schema(result, "Attachment", api_spec) - end - - test "put the url advertised in the Activity in to the url attribute" do - id = "https://wedistribute.org/wp-json/pterotype/v1/object/85810" - [activity] = Activity.search(nil, id) - - status = StatusView.render("show.json", %{activity: activity}) - - assert status.uri == id - assert status.url == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/" - end - - test "a reblog" do - user = insert(:user) - activity = insert(:note_activity) - - {:ok, reblog} = CommonAPI.repeat(activity.id, user) - - represented = StatusView.render("show.json", %{for: user, activity: reblog}) - - assert represented[:id] == to_string(reblog.id) - assert represented[:reblog][:id] == to_string(activity.id) - assert represented[:emojis] == [] - assert_schema(represented, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "a peertube video" do - user = insert(:user) - - {:ok, object} = - Pleroma.Object.Fetcher.fetch_object_from_id( - "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" - ) - - %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) - - represented = StatusView.render("show.json", %{for: user, activity: activity}) - - assert represented[:id] == to_string(activity.id) - assert length(represented[:media_attachments]) == 1 - assert_schema(represented, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "funkwhale audio" do - user = insert(:user) - - {:ok, object} = - Pleroma.Object.Fetcher.fetch_object_from_id( - "https://channels.tests.funkwhale.audio/federation/music/uploads/42342395-0208-4fee-a38d-259a6dae0871" - ) - - %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) - - represented = StatusView.render("show.json", %{for: user, activity: activity}) - - assert represented[:id] == to_string(activity.id) - assert length(represented[:media_attachments]) == 1 - end - - test "a Mobilizon event" do - user = insert(:user) - - {:ok, object} = - Pleroma.Object.Fetcher.fetch_object_from_id( - "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" - ) - - %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) - - represented = StatusView.render("show.json", %{for: user, activity: activity}) - - assert represented[:id] == to_string(activity.id) - - assert represented[:url] == - "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" - - assert represented[:content] == - "<p><a href=\"https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39\">Mobilizon Launching Party</a></p><p>Mobilizon is now federated! 🎉</p><p></p><p>You can view this event from other instances if they are subscribed to mobilizon.org, and soon directly from Mastodon and Pleroma. It is possible that you may see some comments from other instances, including Mastodon ones, just below.</p><p></p><p>With a Mobilizon account on an instance, you may <strong>participate</strong> at events from other instances and <strong>add comments</strong> on events.</p><p></p><p>Of course, it&#39;s still <u>a work in progress</u>: if reports made from an instance on events and comments can be federated, you can&#39;t block people right now, and moderators actions are rather limited, but this <strong>will definitely get fixed over time</strong> until first stable version next year.</p><p></p><p>Anyway, if you want to come up with some feedback, head over to our forum or - if you feel you have technical skills and are familiar with it - on our Gitlab repository.</p><p></p><p>Also, to people that want to set Mobilizon themselves even though we really don&#39;t advise to do that for now, we have a little documentation but it&#39;s quite the early days and you&#39;ll probably need some help. No worries, you can chat with us on our Forum or though our Matrix channel.</p><p></p><p>Check our website for more informations and follow us on Twitter or Mastodon.</p>" - end - - describe "build_tags/1" do - test "it returns a a dictionary tags" do - object_tags = [ - "fediverse", - "mastodon", - "nextcloud", - %{ - "href" => "https://kawen.space/users/lain", - "name" => "@lain@kawen.space", - "type" => "Mention" - } - ] - - assert StatusView.build_tags(object_tags) == [ - %{name: "fediverse", url: "/tag/fediverse"}, - %{name: "mastodon", url: "/tag/mastodon"}, - %{name: "nextcloud", url: "/tag/nextcloud"} - ] - end - end - - describe "rich media cards" do - test "a rich media card without a site name renders correctly" do - page_url = "http://example.com" - - card = %{ - url: 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}) - end - - test "a rich media card without a site name or image renders correctly" do - page_url = "http://example.com" - - card = %{ - url: page_url, - title: "Example website" - } - - %{provider_name: "example.com"} = - StatusView.render("card.json", %{page_url: page_url, rich_media: card}) - end - - test "a rich media card without an image renders correctly" do - page_url = "http://example.com" - - card = %{ - 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}) - 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" - } - - %{provider_name: "example.com"} = - StatusView.render("card.json", %{page_url: page_url, rich_media: card}) - end - end - - test "does not embed a relationship in the account" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "drink more water" - }) - - result = StatusView.render("show.json", %{activity: activity, for: other_user}) - - assert result[:account][:pleroma][:relationship] == %{} - assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "does not embed a relationship in the account in reposts" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "˙˙ɐʎns" - }) - - {:ok, activity} = CommonAPI.repeat(activity.id, other_user) - - result = StatusView.render("show.json", %{activity: activity, for: user}) - - assert result[:account][:pleroma][:relationship] == %{} - assert result[:reblog][:account][:pleroma][:relationship] == %{} - assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "visibility/list" do - user = insert(:user) - - {:ok, list} = Pleroma.List.create("foo", user) - - {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) - - status = StatusView.render("show.json", activity: activity) - - assert status.visibility == "list" - end - - test "has a field for parent visibility" do - user = insert(:user) - poster = insert(:user) - - {:ok, invisible} = CommonAPI.post(poster, %{status: "hey", visibility: "private"}) - - {:ok, visible} = - CommonAPI.post(poster, %{status: "hey", visibility: "private", in_reply_to_id: invisible.id}) - - status = StatusView.render("show.json", activity: visible, for: user) - refute status.pleroma.parent_visible - - status = StatusView.render("show.json", activity: visible, for: poster) - assert status.pleroma.parent_visible - end -end diff --git a/test/web/mastodon_api/views/subscription_view_test.exs b/test/web/mastodon_api/views/subscription_view_test.exs @@ -1,23 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.SubscriptionViewTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Web.MastodonAPI.SubscriptionView, as: View - alias Pleroma.Web.Push - - test "Represent a subscription" do - subscription = insert(:push_subscription, data: %{"alerts" => %{"mention" => true}}) - - expected = %{ - alerts: %{"mention" => true}, - endpoint: subscription.endpoint, - id: to_string(subscription.id), - server_key: Keyword.get(Push.vapid_config(), :public_key) - } - - assert expected == View.render("show.json", %{subscription: subscription}) - end -end diff --git a/test/web/media_proxy/invalidation_test.exs b/test/web/media_proxy/invalidation_test.exs @@ -1,68 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MediaProxy.InvalidationTest do - use ExUnit.Case - use Pleroma.Tests.Helpers - - alias Pleroma.Config - alias Pleroma.Web.MediaProxy.Invalidation - - import ExUnit.CaptureLog - import Mock - import Tesla.Mock - - setup do: clear_config([:media_proxy]) - - setup do - on_exit(fn -> Cachex.clear(:banned_urls_cache) end) - end - - describe "Invalidation.Http" do - test "perform request to clear cache" do - Config.put([:media_proxy, :enabled], false) - Config.put([:media_proxy, :invalidation, :enabled], true) - Config.put([:media_proxy, :invalidation, :provider], Invalidation.Http) - - Config.put([Invalidation.Http], method: :purge, headers: [{"x-refresh", 1}]) - image_url = "http://example.com/media/example.jpg" - Pleroma.Web.MediaProxy.put_in_banned_urls(image_url) - - mock(fn - %{ - method: :purge, - url: "http://example.com/media/example.jpg", - headers: [{"x-refresh", 1}] - } -> - %Tesla.Env{status: 200} - end) - - assert capture_log(fn -> - assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) - assert Invalidation.purge([image_url]) == {:ok, [image_url]} - assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) - end) =~ "Running cache purge: [\"#{image_url}\"]" - end - end - - describe "Invalidation.Script" do - test "run script to clear cache" do - Config.put([:media_proxy, :enabled], false) - Config.put([:media_proxy, :invalidation, :enabled], true) - Config.put([:media_proxy, :invalidation, :provider], Invalidation.Script) - Config.put([Invalidation.Script], script_path: "purge-nginx") - - image_url = "http://example.com/media/example.jpg" - Pleroma.Web.MediaProxy.put_in_banned_urls(image_url) - - with_mocks [{System, [], [cmd: fn _, _ -> {"ok", 0} end]}] do - assert capture_log(fn -> - assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) - assert Invalidation.purge([image_url]) == {:ok, [image_url]} - assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) - end) =~ "Running cache purge: [\"#{image_url}\"]" - end - end - end -end diff --git a/test/web/media_proxy/invalidations/http_test.exs b/test/web/media_proxy/invalidations/http_test.exs @@ -1,43 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MediaProxy.Invalidation.HttpTest do - use ExUnit.Case - alias Pleroma.Web.MediaProxy.Invalidation - - import ExUnit.CaptureLog - import Tesla.Mock - - setup do - on_exit(fn -> Cachex.clear(:banned_urls_cache) end) - end - - test "logs hasn't error message when request is valid" do - mock(fn - %{method: :purge, url: "http://example.com/media/example.jpg"} -> - %Tesla.Env{status: 200} - end) - - refute capture_log(fn -> - assert Invalidation.Http.purge( - ["http://example.com/media/example.jpg"], - [] - ) == {:ok, ["http://example.com/media/example.jpg"]} - end) =~ "Error while cache purge" - end - - test "it write error message in logs when request invalid" do - mock(fn - %{method: :purge, url: "http://example.com/media/example1.jpg"} -> - %Tesla.Env{status: 404} - end) - - assert capture_log(fn -> - assert Invalidation.Http.purge( - ["http://example.com/media/example1.jpg"], - [] - ) == {:ok, ["http://example.com/media/example1.jpg"]} - end) =~ "Error while cache purge: url - http://example.com/media/example1.jpg" - end -end diff --git a/test/web/media_proxy/invalidations/script_test.exs b/test/web/media_proxy/invalidations/script_test.exs @@ -1,30 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MediaProxy.Invalidation.ScriptTest do - use ExUnit.Case - alias Pleroma.Web.MediaProxy.Invalidation - - import ExUnit.CaptureLog - - setup do - on_exit(fn -> Cachex.clear(:banned_urls_cache) end) - end - - test "it logger error when script 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}" - - capture_log(fn -> - assert Invalidation.Script.purge( - ["http://example.com/media/example.jpg"], - [] - ) == {:error, "\"not found script path\""} - end) - end -end diff --git a/test/web/media_proxy/media_proxy_controller_test.exs b/test/web/media_proxy/media_proxy_controller_test.exs @@ -1,342 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do - use Pleroma.Web.ConnCase - - import Mock - - alias Pleroma.Web.MediaProxy - alias Plug.Conn - - setup do - on_exit(fn -> Cachex.clear(:banned_urls_cache) end) - end - - describe "Media Proxy" do - setup do - clear_config([:media_proxy, :enabled], true) - clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") - - [url: MediaProxy.encode_url("https://google.fn/test.png")] - end - - test "it returns 404 when disabled", %{conn: conn} do - clear_config([:media_proxy, :enabled], false) - - assert %Conn{ - status: 404, - resp_body: "Not Found" - } = get(conn, "/proxy/hhgfh/eeeee") - - assert %Conn{ - status: 404, - resp_body: "Not Found" - } = get(conn, "/proxy/hhgfh/eeee/fff") - end - - test "it returns 403 for invalid signature", %{conn: conn, url: url} do - Pleroma.Config.put([Pleroma.Web.Endpoint, :secret_key_base], "000") - %{path: path} = URI.parse(url) - - assert %Conn{ - status: 403, - resp_body: "Forbidden" - } = get(conn, path) - - assert %Conn{ - status: 403, - resp_body: "Forbidden" - } = get(conn, "/proxy/hhgfh/eeee") - - assert %Conn{ - status: 403, - resp_body: "Forbidden" - } = get(conn, "/proxy/hhgfh/eeee/fff") - end - - test "redirects to valid url when filename is invalidated", %{conn: conn, url: url} do - invalid_url = String.replace(url, "test.png", "test-file.png") - response = get(conn, invalid_url) - assert response.status == 302 - assert redirected_to(response) == url - end - - test "it performs ReverseProxy.call with valid signature", %{conn: conn, url: url} do - with_mock Pleroma.ReverseProxy, - call: fn _conn, _url, _opts -> %Conn{status: :success} end do - assert %Conn{status: :success} = get(conn, url) - end - end - - test "it returns 404 when url is in banned_urls cache", %{conn: conn, url: url} do - MediaProxy.put_in_banned_urls("https://google.fn/test.png") - - with_mock Pleroma.ReverseProxy, - call: fn _conn, _url, _opts -> %Conn{status: :success} end do - assert %Conn{status: 404, resp_body: "Not Found"} = get(conn, url) - end - end - end - - describe "Media Preview Proxy" do - def assert_dependencies_installed do - missing_dependencies = Pleroma.Helpers.MediaHelper.missing_dependencies() - - assert missing_dependencies == [], - "Error: missing dependencies (please refer to `docs/installation`): #{ - inspect(missing_dependencies) - }" - end - - setup do - clear_config([:media_proxy, :enabled], true) - clear_config([:media_preview_proxy, :enabled], true) - clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") - - original_url = "https://google.fn/test.png" - - [ - url: MediaProxy.encode_preview_url(original_url), - media_proxy_url: MediaProxy.encode_url(original_url) - ] - end - - test "returns 404 when media proxy is disabled", %{conn: conn} do - clear_config([:media_proxy, :enabled], false) - - assert %Conn{ - status: 404, - resp_body: "Not Found" - } = get(conn, "/proxy/preview/hhgfh/eeeee") - - assert %Conn{ - status: 404, - resp_body: "Not Found" - } = get(conn, "/proxy/preview/hhgfh/fff") - end - - test "returns 404 when disabled", %{conn: conn} do - clear_config([:media_preview_proxy, :enabled], false) - - assert %Conn{ - status: 404, - resp_body: "Not Found" - } = get(conn, "/proxy/preview/hhgfh/eeeee") - - assert %Conn{ - status: 404, - resp_body: "Not Found" - } = get(conn, "/proxy/preview/hhgfh/fff") - end - - test "it returns 403 for invalid signature", %{conn: conn, url: url} do - Pleroma.Config.put([Pleroma.Web.Endpoint, :secret_key_base], "000") - %{path: path} = URI.parse(url) - - assert %Conn{ - status: 403, - resp_body: "Forbidden" - } = get(conn, path) - - assert %Conn{ - status: 403, - resp_body: "Forbidden" - } = get(conn, "/proxy/preview/hhgfh/eeee") - - assert %Conn{ - status: 403, - resp_body: "Forbidden" - } = get(conn, "/proxy/preview/hhgfh/eeee/fff") - end - - test "redirects to valid url when filename is invalidated", %{conn: conn, url: url} do - invalid_url = String.replace(url, "test.png", "test-file.png") - response = get(conn, invalid_url) - assert response.status == 302 - assert redirected_to(response) == url - end - - test "responds with 424 Failed Dependency if HEAD request to media proxy fails", %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{status: 500, body: ""} - end) - - response = get(conn, url) - assert response.status == 424 - assert response.resp_body == "Can't fetch HTTP headers (HTTP 500)." - end - - test "redirects to media proxy URI on unsupported content type", %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: "", headers: [{"content-type", "application/pdf"}]} - end) - - response = get(conn, url) - assert response.status == 302 - assert redirected_to(response) == media_proxy_url - end - - test "with `static=true` and GIF image preview requested, responds with JPEG image", %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - assert_dependencies_installed() - - # Setting a high :min_content_length to ensure this scenario is not affected by its logic - clear_config([:media_preview_proxy, :min_content_length], 1_000_000_000) - - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{ - status: 200, - body: "", - headers: [{"content-type", "image/gif"}, {"content-length", "1001718"}] - } - - %{method: :get, url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.gif")} - end) - - response = get(conn, url <> "?static=true") - - assert response.status == 200 - assert Conn.get_resp_header(response, "content-type") == ["image/jpeg"] - assert response.resp_body != "" - end - - test "with GIF image preview requested and no `static` param, redirects to media proxy URI", - %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/gif"}]} - end) - - response = get(conn, url) - - assert response.status == 302 - assert redirected_to(response) == media_proxy_url - end - - test "with `static` param and non-GIF image preview requested, " <> - "redirects to media preview proxy URI without `static` param", - %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} - end) - - response = get(conn, url <> "?static=true") - - assert response.status == 302 - assert redirected_to(response) == url - end - - test "with :min_content_length setting not matched by Content-Length header, " <> - "redirects to media proxy URI", - %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - clear_config([:media_preview_proxy, :min_content_length], 100_000) - - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{ - status: 200, - body: "", - headers: [{"content-type", "image/gif"}, {"content-length", "5000"}] - } - end) - - response = get(conn, url) - - assert response.status == 302 - assert redirected_to(response) == media_proxy_url - end - - test "thumbnails PNG images into PNG", %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - assert_dependencies_installed() - - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/png"}]} - - %{method: :get, url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.png")} - end) - - response = get(conn, url) - - assert response.status == 200 - assert Conn.get_resp_header(response, "content-type") == ["image/png"] - assert response.resp_body != "" - end - - test "thumbnails JPEG images into JPEG", %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - assert_dependencies_installed() - - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} - - %{method: :get, url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")} - end) - - response = get(conn, url) - - assert response.status == 200 - assert Conn.get_resp_header(response, "content-type") == ["image/jpeg"] - assert response.resp_body != "" - end - - test "redirects to media proxy URI in case of thumbnailing error", %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} - - %{method: :get, url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: "<html><body>error</body></html>"} - end) - - response = get(conn, url) - - assert response.status == 302 - assert redirected_to(response) == media_proxy_url - end - end -end diff --git a/test/web/media_proxy/media_proxy_test.exs b/test/web/media_proxy/media_proxy_test.exs @@ -1,234 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MediaProxyTest do - use ExUnit.Case - use Pleroma.Tests.Helpers - - alias Pleroma.Config - alias Pleroma.Web.Endpoint - alias Pleroma.Web.MediaProxy - - defp decode_result(encoded) do - [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/") - {:ok, decoded} = MediaProxy.decode_url(sig, base64) - decoded - end - - describe "when enabled" do - setup do: clear_config([:media_proxy, :enabled], true) - - test "ignores invalid url" do - assert MediaProxy.url(nil) == nil - assert MediaProxy.url("") == nil - end - - test "ignores relative url" do - assert MediaProxy.url("/local") == "/local" - assert MediaProxy.url("/") == "/" - end - - test "ignores local url" do - local_url = Endpoint.url() <> "/hello" - local_root = Endpoint.url() - assert MediaProxy.url(local_url) == local_url - assert MediaProxy.url(local_root) == local_root - end - - test "encodes and decodes URL" do - url = "https://pleroma.soykaf.com/static/logo.png" - encoded = MediaProxy.url(url) - - assert String.starts_with?( - encoded, - Config.get([:media_proxy, :base_url], Pleroma.Web.base_url()) - ) - - assert String.ends_with?(encoded, "/logo.png") - - assert decode_result(encoded) == url - end - - test "encodes and decodes URL without a path" do - url = "https://pleroma.soykaf.com" - encoded = MediaProxy.url(url) - assert decode_result(encoded) == url - end - - test "encodes and decodes URL without an extension" do - url = "https://pleroma.soykaf.com/path/" - encoded = MediaProxy.url(url) - assert String.ends_with?(encoded, "/path") - assert decode_result(encoded) == url - end - - test "encodes and decodes URL and ignores query params for the path" do - url = "https://pleroma.soykaf.com/static/logo.png?93939393939&bunny=true" - encoded = MediaProxy.url(url) - assert String.ends_with?(encoded, "/logo.png") - assert decode_result(encoded) == url - end - - test "validates signature" do - encoded = MediaProxy.url("https://pleroma.social") - - clear_config( - [Endpoint, :secret_key_base], - "00000000000000000000000000000000000000000000000" - ) - - [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/") - assert MediaProxy.decode_url(sig, base64) == {:error, :invalid_signature} - end - - def test_verify_request_path_and_url(request_path, url, expected_result) do - assert MediaProxy.verify_request_path_and_url(request_path, url) == expected_result - - assert MediaProxy.verify_request_path_and_url( - %Plug.Conn{ - params: %{"filename" => Path.basename(request_path)}, - request_path: request_path - }, - url - ) == expected_result - end - - test "if first arg of `verify_request_path_and_url/2` is a Plug.Conn without \"filename\" " <> - "parameter, `verify_request_path_and_url/2` returns :ok " do - assert MediaProxy.verify_request_path_and_url( - %Plug.Conn{params: %{}, request_path: "/some/path"}, - "https://instance.com/file.jpg" - ) == :ok - - assert MediaProxy.verify_request_path_and_url( - %Plug.Conn{params: %{}, request_path: "/path/to/file.jpg"}, - "https://instance.com/file.jpg" - ) == :ok - end - - test "`verify_request_path_and_url/2` preserves the encoded or decoded path" do - test_verify_request_path_and_url( - "/Hello world.jpg", - "http://pleroma.social/Hello world.jpg", - :ok - ) - - test_verify_request_path_and_url( - "/Hello%20world.jpg", - "http://pleroma.social/Hello%20world.jpg", - :ok - ) - - test_verify_request_path_and_url( - "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", - "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", - :ok - ) - - test_verify_request_path_and_url( - # Note: `conn.request_path` returns encoded url - "/ANALYSE-DAI-_-LE-STABLECOIN-100-D%C3%89CENTRALIS%C3%89-BQ.jpg", - "https://mydomain.com/uploads/2019/07/ANALYSE-DAI-_-LE-STABLECOIN-100-DÉCENTRALISÉ-BQ.jpg", - :ok - ) - - test_verify_request_path_and_url( - "/my%2Flong%2Furl%2F2019%2F07%2FS", - "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", - {:wrong_filename, "my%2Flong%2Furl%2F2019%2F07%2FS.jpg"} - ) - end - - test "uses the configured base_url" do - base_url = "https://cache.pleroma.social" - clear_config([:media_proxy, :base_url], base_url) - - url = "https://pleroma.soykaf.com/static/logo.png" - encoded = MediaProxy.url(url) - - assert String.starts_with?(encoded, base_url) - end - - # Some sites expect ASCII encoded characters in the URL to be preserved even if - # unnecessary. - # Issues: https://git.pleroma.social/pleroma/pleroma/issues/580 - # https://git.pleroma.social/pleroma/pleroma/issues/1055 - test "preserve ASCII encoding" do - url = - "https://pleroma.com/%20/%21/%22/%23/%24/%25/%26/%27/%28/%29/%2A/%2B/%2C/%2D/%2E/%2F/%30/%31/%32/%33/%34/%35/%36/%37/%38/%39/%3A/%3B/%3C/%3D/%3E/%3F/%40/%41/%42/%43/%44/%45/%46/%47/%48/%49/%4A/%4B/%4C/%4D/%4E/%4F/%50/%51/%52/%53/%54/%55/%56/%57/%58/%59/%5A/%5B/%5C/%5D/%5E/%5F/%60/%61/%62/%63/%64/%65/%66/%67/%68/%69/%6A/%6B/%6C/%6D/%6E/%6F/%70/%71/%72/%73/%74/%75/%76/%77/%78/%79/%7A/%7B/%7C/%7D/%7E/%7F/%80/%81/%82/%83/%84/%85/%86/%87/%88/%89/%8A/%8B/%8C/%8D/%8E/%8F/%90/%91/%92/%93/%94/%95/%96/%97/%98/%99/%9A/%9B/%9C/%9D/%9E/%9F/%C2%A0/%A1/%A2/%A3/%A4/%A5/%A6/%A7/%A8/%A9/%AA/%AB/%AC/%C2%AD/%AE/%AF/%B0/%B1/%B2/%B3/%B4/%B5/%B6/%B7/%B8/%B9/%BA/%BB/%BC/%BD/%BE/%BF/%C0/%C1/%C2/%C3/%C4/%C5/%C6/%C7/%C8/%C9/%CA/%CB/%CC/%CD/%CE/%CF/%D0/%D1/%D2/%D3/%D4/%D5/%D6/%D7/%D8/%D9/%DA/%DB/%DC/%DD/%DE/%DF/%E0/%E1/%E2/%E3/%E4/%E5/%E6/%E7/%E8/%E9/%EA/%EB/%EC/%ED/%EE/%EF/%F0/%F1/%F2/%F3/%F4/%F5/%F6/%F7/%F8/%F9/%FA/%FB/%FC/%FD/%FE/%FF" - - encoded = MediaProxy.url(url) - assert decode_result(encoded) == url - end - - # This includes unsafe/reserved characters which are not interpreted as part of the URL - # and would otherwise have to be ASCII encoded. It is our role to ensure the proxied URL - # is unmodified, so we are testing these characters anyway. - test "preserve non-unicode characters per RFC3986" do - url = - "https://pleroma.com/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-._~:/?#[]@!$&'()*+,;=|^`{}" - - encoded = MediaProxy.url(url) - assert decode_result(encoded) == url - end - - test "preserve unicode characters" do - url = "https://ko.wikipedia.org/wiki/위키백과:대문" - - encoded = MediaProxy.url(url) - assert decode_result(encoded) == url - end - end - - describe "when disabled" do - setup do: clear_config([:media_proxy, :enabled], false) - - test "does not encode remote urls" do - assert MediaProxy.url("https://google.fr") == "https://google.fr" - end - end - - describe "whitelist" do - setup do: clear_config([:media_proxy, :enabled], true) - - test "mediaproxy whitelist" do - clear_config([:media_proxy, :whitelist], ["https://google.com", "https://feld.me"]) - url = "https://feld.me/foo.png" - - unencoded = MediaProxy.url(url) - assert unencoded == url - end - - # TODO: delete after removing support bare domains for media proxy whitelist - test "mediaproxy whitelist bare domains whitelist (deprecated)" do - clear_config([:media_proxy, :whitelist], ["google.com", "feld.me"]) - url = "https://feld.me/foo.png" - - unencoded = MediaProxy.url(url) - assert unencoded == url - end - - test "does not change whitelisted urls" do - clear_config([:media_proxy, :whitelist], ["mycdn.akamai.com"]) - clear_config([:media_proxy, :base_url], "https://cache.pleroma.social") - - media_url = "https://mycdn.akamai.com" - - url = "#{media_url}/static/logo.png" - encoded = MediaProxy.url(url) - - assert String.starts_with?(encoded, media_url) - end - - test "ensure Pleroma.Upload base_url is always whitelisted" do - media_url = "https://media.pleroma.social" - clear_config([Pleroma.Upload, :base_url], media_url) - - url = "#{media_url}/static/logo.png" - encoded = MediaProxy.url(url) - - assert String.starts_with?(encoded, media_url) - end - end -end diff --git a/test/web/metadata/feed_test.exs b/test/web/metadata/feed_test.exs @@ -1,18 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.FeedTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Web.Metadata.Providers.Feed - - test "it renders a link to user's atom feed" do - user = insert(:user, nickname: "lain") - - assert Feed.build_tags(%{user: user}) == [ - {:link, - [rel: "alternate", type: "application/atom+xml", href: "/users/lain/feed.atom"], []} - ] - end -end diff --git a/test/web/metadata/metadata_test.exs b/test/web/metadata/metadata_test.exs @@ -1,49 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MetadataTest do - use Pleroma.DataCase, async: true - - import Pleroma.Factory - - describe "restrict indexing remote users" do - test "for remote user" do - user = insert(:user, local: false) - - assert Pleroma.Web.Metadata.build_tags(%{user: user}) =~ - "<meta content=\"noindex, noarchive\" name=\"robots\">" - end - - test "for local user" do - user = insert(:user, discoverable: false) - - assert Pleroma.Web.Metadata.build_tags(%{user: user}) =~ - "<meta content=\"noindex, noarchive\" name=\"robots\">" - end - - test "for local user set to discoverable" do - user = insert(:user, discoverable: true) - - refute Pleroma.Web.Metadata.build_tags(%{user: user}) =~ - "<meta content=\"noindex, noarchive\" name=\"robots\">" - end - end - - describe "no metadata for private instances" do - test "for local user set to discoverable" do - clear_config([:instance, :public], false) - user = insert(:user, bio: "This is my secret fedi account bio", discoverable: true) - - assert "" = Pleroma.Web.Metadata.build_tags(%{user: user}) - end - - test "search exclusion metadata is included" do - clear_config([:instance, :public], false) - user = insert(:user, bio: "This is my secret fedi account bio", discoverable: false) - - assert ~s(<meta content="noindex, noarchive" name="robots">) == - Pleroma.Web.Metadata.build_tags(%{user: user}) - end - end -end diff --git a/test/web/metadata/opengraph_test.exs b/test/web/metadata/opengraph_test.exs @@ -1,96 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.OpenGraphTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Web.Metadata.Providers.OpenGraph - - setup do: clear_config([Pleroma.Web.Metadata, :unfurl_nsfw]) - - test "it renders all supported types of attachments and skips unknown types" do - user = insert(:user) - - note = - insert(:note, %{ - data: %{ - "actor" => user.ap_id, - "tag" => [], - "id" => "https://pleroma.gov/objects/whatever", - "content" => "pleroma in a nutshell", - "attachment" => [ - %{ - "url" => [ - %{"mediaType" => "image/png", "href" => "https://pleroma.gov/tenshi.png"} - ] - }, - %{ - "url" => [ - %{ - "mediaType" => "application/octet-stream", - "href" => "https://pleroma.gov/fqa/badapple.sfc" - } - ] - }, - %{ - "url" => [ - %{"mediaType" => "video/webm", "href" => "https://pleroma.gov/about/juche.webm"} - ] - }, - %{ - "url" => [ - %{ - "mediaType" => "audio/basic", - "href" => "http://www.gnu.org/music/free-software-song.au" - } - ] - } - ] - } - }) - - result = OpenGraph.build_tags(%{object: note, url: note.data["id"], user: user}) - - assert Enum.all?( - [ - {:meta, [property: "og:image", content: "https://pleroma.gov/tenshi.png"], []}, - {:meta, - [property: "og:audio", content: "http://www.gnu.org/music/free-software-song.au"], - []}, - {:meta, [property: "og:video", content: "https://pleroma.gov/about/juche.webm"], - []} - ], - fn element -> element in result end - ) - end - - test "it does not render attachments if post is nsfw" do - Pleroma.Config.put([Pleroma.Web.Metadata, :unfurl_nsfw], false) - user = insert(:user, avatar: %{"url" => [%{"href" => "https://pleroma.gov/tenshi.png"}]}) - - note = - insert(:note, %{ - data: %{ - "actor" => user.ap_id, - "id" => "https://pleroma.gov/objects/whatever", - "content" => "#cuteposting #nsfw #hambaga", - "tag" => ["cuteposting", "nsfw", "hambaga"], - "sensitive" => true, - "attachment" => [ - %{ - "url" => [ - %{"mediaType" => "image/png", "href" => "https://misskey.microsoft/corndog.png"} - ] - } - ] - } - }) - - result = OpenGraph.build_tags(%{object: note, url: note.data["id"], user: user}) - - assert {:meta, [property: "og:image", content: "https://pleroma.gov/tenshi.png"], []} in result - - refute {:meta, [property: "og:image", content: "https://misskey.microsoft/corndog.png"], []} in result - end -end diff --git a/test/web/metadata/player_view_test.exs b/test/web/metadata/player_view_test.exs @@ -1,33 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.PlayerViewTest do - use Pleroma.DataCase - - alias Pleroma.Web.Metadata.PlayerView - - test "it renders audio tag" do - res = - PlayerView.render( - "player.html", - %{"mediaType" => "audio", "href" => "test-href"} - ) - |> Phoenix.HTML.safe_to_string() - - assert res == - "<audio controls><source src=\"test-href\" type=\"audio\">Your browser does not support audio playback.</audio>" - end - - test "it renders videos tag" do - res = - PlayerView.render( - "player.html", - %{"mediaType" => "video", "href" => "test-href"} - ) - |> Phoenix.HTML.safe_to_string() - - assert res == - "<video controls loop><source src=\"test-href\" type=\"video\">Your browser does not support video playback.</video>" - end -end diff --git a/test/web/metadata/rel_me_test.exs b/test/web/metadata/rel_me_test.exs @@ -1,21 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.RelMeTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Web.Metadata.Providers.RelMe - - test "it renders all links with rel='me' from user bio" 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}) - - assert RelMe.build_tags(%{user: user}) == [ - {:link, [rel: "me", href: "http://some3.com"], []}, - {:link, [rel: "me", href: "https://another-link.com"], []} - ] - end -end diff --git a/test/web/metadata/restrict_indexing_test.exs b/test/web/metadata/restrict_indexing_test.exs @@ -1,27 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.RestrictIndexingTest do - use ExUnit.Case, async: true - - describe "build_tags/1" do - test "for remote user" do - assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{ - user: %Pleroma.User{local: false} - }) == [{:meta, [name: "robots", content: "noindex, noarchive"], []}] - end - - test "for local user" do - assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{ - user: %Pleroma.User{local: true, discoverable: true} - }) == [] - end - - test "for local user when discoverable is false" do - assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{ - user: %Pleroma.User{local: true, discoverable: false} - }) == [{:meta, [name: "robots", content: "noindex, noarchive"], []}] - end - end -end diff --git a/test/web/metadata/twitter_card_test.exs b/test/web/metadata/twitter_card_test.exs @@ -1,150 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.User - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Endpoint - alias Pleroma.Web.Metadata.Providers.TwitterCard - alias Pleroma.Web.Metadata.Utils - alias Pleroma.Web.Router - - setup do: clear_config([Pleroma.Web.Metadata, :unfurl_nsfw]) - - test "it renders twitter card for user info" do - user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") - avatar_url = Utils.attachment_url(User.avatar_url(user)) - 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"], []} - ] - end - - test "it uses summary twittercard if post has no attachment" 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", - "content" => "pleroma in a nutshell" - } - }) - - 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, [property: "twitter:card", content: "summary"], []} - ] == result - end - - test "it renders avatar not attachment if post is nsfw and unfurl_nsfw is disabled" do - Pleroma.Config.put([Pleroma.Web.Metadata, :unfurl_nsfw], false) - 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", - "content" => "pleroma in a nutshell", - "sensitive" => true, - "attachment" => [ - %{ - "url" => [%{"mediaType" => "image/png", "href" => "https://pleroma.gov/tenshi.png"}] - }, - %{ - "url" => [ - %{ - "mediaType" => "application/octet-stream", - "href" => "https://pleroma.gov/fqa/badapple.sfc" - } - ] - }, - %{ - "url" => [ - %{"mediaType" => "video/webm", "href" => "https://pleroma.gov/about/juche.webm"} - ] - } - ] - } - }) - - 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, [property: "twitter:card", content: "summary"], []} - ] == result - end - - test "it renders supported types of attachments and skips unknown types" 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", - "content" => "pleroma in a nutshell", - "attachment" => [ - %{ - "url" => [%{"mediaType" => "image/png", "href" => "https://pleroma.gov/tenshi.png"}] - }, - %{ - "url" => [ - %{ - "mediaType" => "application/octet-stream", - "href" => "https://pleroma.gov/fqa/badapple.sfc" - } - ] - }, - %{ - "url" => [ - %{"mediaType" => "video/webm", "href" => "https://pleroma.gov/about/juche.webm"} - ] - } - ] - } - }) - - 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:card", content: "player"], []}, - {:meta, - [ - property: "twitter:player", - content: Router.Helpers.o_status_url(Endpoint, :notice_player, activity.id) - ], []}, - {:meta, [property: "twitter:player:width", content: "480"], []}, - {:meta, [property: "twitter:player:height", content: "480"], []} - ] == result - end -end diff --git a/test/web/metadata/utils_test.exs b/test/web/metadata/utils_test.exs @@ -1,32 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.UtilsTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Web.Metadata.Utils - - describe "scrub_html_and_truncate/1" do - test "it returns text without encode HTML" do - user = insert(:user) - - note = - insert(:note, %{ - data: %{ - "actor" => user.ap_id, - "id" => "https://pleroma.gov/objects/whatever", - "content" => "Pleroma's really cool!" - } - }) - - assert Utils.scrub_html_and_truncate(note) == "Pleroma's really cool!" - end - end - - describe "scrub_html_and_truncate/2" do - test "it returns text without encode HTML" do - assert Utils.scrub_html_and_truncate("Pleroma's really cool!") == "Pleroma's really cool!" - end - end -end diff --git a/test/web/mongooseim/mongoose_im_controller_test.exs b/test/web/mongooseim/mongoose_im_controller_test.exs @@ -1,81 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MongooseIMController do - use Pleroma.Web.ConnCase - import Pleroma.Factory - - test "/user_exists", %{conn: conn} do - _user = insert(:user, nickname: "lain") - _remote_user = insert(:user, nickname: "alice", local: false) - _deactivated_user = insert(:user, nickname: "konata", deactivated: true) - - res = - conn - |> get(mongoose_im_path(conn, :user_exists), user: "lain") - |> json_response(200) - - assert res == true - - res = - conn - |> get(mongoose_im_path(conn, :user_exists), user: "alice") - |> json_response(404) - - assert res == false - - res = - conn - |> get(mongoose_im_path(conn, :user_exists), user: "bob") - |> json_response(404) - - assert res == false - - res = - conn - |> get(mongoose_im_path(conn, :user_exists), user: "konata") - |> json_response(404) - - assert res == false - end - - test "/check_password", %{conn: conn} do - user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt("cool")) - - _deactivated_user = - insert(:user, - nickname: "konata", - deactivated: true, - password_hash: Pbkdf2.hash_pwd_salt("cool") - ) - - res = - conn - |> get(mongoose_im_path(conn, :check_password), user: user.nickname, pass: "cool") - |> json_response(200) - - assert res == true - - res = - conn - |> get(mongoose_im_path(conn, :check_password), user: user.nickname, pass: "uncool") - |> json_response(403) - - assert res == false - - res = - conn - |> get(mongoose_im_path(conn, :check_password), user: "konata", pass: "cool") - |> json_response(404) - - assert res == false - - res = - conn - |> get(mongoose_im_path(conn, :check_password), user: "nobody", pass: "cool") - |> json_response(404) - - assert res == false - end -end diff --git a/test/web/node_info_test.exs b/test/web/node_info_test.exs @@ -1,188 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.NodeInfoTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - alias Pleroma.Config - - setup do: clear_config([:mrf_simple]) - setup do: clear_config(:instance) - - test "GET /.well-known/nodeinfo", %{conn: conn} do - links = - conn - |> get("/.well-known/nodeinfo") - |> json_response(200) - |> Map.fetch!("links") - - Enum.each(links, fn link -> - href = Map.fetch!(link, "href") - - conn - |> get(href) - |> json_response(200) - end) - end - - test "nodeinfo shows staff accounts", %{conn: conn} do - moderator = insert(:user, local: true, is_moderator: true) - admin = insert(:user, local: true, is_admin: true) - - conn = - conn - |> get("/nodeinfo/2.1.json") - - assert result = json_response(conn, 200) - - assert moderator.ap_id in result["metadata"]["staffAccounts"] - assert admin.ap_id in result["metadata"]["staffAccounts"] - end - - test "nodeinfo shows restricted nicknames", %{conn: conn} do - conn = - conn - |> get("/nodeinfo/2.1.json") - - assert result = json_response(conn, 200) - - assert Config.get([Pleroma.User, :restricted_nicknames]) == - result["metadata"]["restrictedNicknames"] - end - - test "returns software.repository field in nodeinfo 2.1", %{conn: conn} do - conn - |> get("/.well-known/nodeinfo") - |> json_response(200) - - conn = - conn - |> get("/nodeinfo/2.1.json") - - assert result = json_response(conn, 200) - assert Pleroma.Application.repository() == result["software"]["repository"] - end - - test "returns fieldsLimits field", %{conn: conn} do - clear_config([:instance, :max_account_fields], 10) - clear_config([:instance, :max_remote_account_fields], 15) - clear_config([:instance, :account_field_name_length], 255) - clear_config([:instance, :account_field_value_length], 2048) - - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) - - assert response["metadata"]["fieldsLimits"]["maxFields"] == 10 - assert response["metadata"]["fieldsLimits"]["maxRemoteFields"] == 15 - assert response["metadata"]["fieldsLimits"]["nameLength"] == 255 - assert response["metadata"]["fieldsLimits"]["valueLength"] == 2048 - end - - test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do - clear_config([:instance, :safe_dm_mentions], true) - - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) - - assert "safe_dm_mentions" in response["metadata"]["features"] - - Config.put([:instance, :safe_dm_mentions], false) - - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) - - refute "safe_dm_mentions" in response["metadata"]["features"] - end - - describe "`metadata/federation/enabled`" do - setup do: clear_config([:instance, :federating]) - - test "it shows if federation is enabled/disabled", %{conn: conn} do - Config.put([:instance, :federating], true) - - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) - - assert response["metadata"]["federation"]["enabled"] == true - - Config.put([:instance, :federating], false) - - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) - - assert response["metadata"]["federation"]["enabled"] == false - end - end - - test "it shows default features flags", %{conn: conn} do - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) - - default_features = [ - "pleroma_api", - "mastodon_api", - "mastodon_api_streaming", - "polls", - "pleroma_explicit_addressing", - "shareable_emoji_packs", - "multifetch", - "pleroma_emoji_reactions", - "pleroma:api/v1/notifications:include_types_filter", - "pleroma_chat_messages" - ] - - assert MapSet.subset?( - MapSet.new(default_features), - MapSet.new(response["metadata"]["features"]) - ) - end - - test "it shows MRF transparency data if enabled", %{conn: conn} do - clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) - clear_config([:mrf, :transparency], true) - - simple_config = %{"reject" => ["example.com"]} - clear_config(:mrf_simple, simple_config) - - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) - - assert response["metadata"]["federation"]["mrf_simple"] == simple_config - end - - test "it performs exclusions from MRF transparency data if configured", %{conn: conn} do - clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) - clear_config([:mrf, :transparency], true) - clear_config([:mrf, :transparency_exclusions], ["other.site"]) - - simple_config = %{"reject" => ["example.com", "other.site"]} - clear_config(:mrf_simple, simple_config) - - expected_config = %{"reject" => ["example.com"]} - - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) - - assert response["metadata"]["federation"]["mrf_simple"] == expected_config - assert response["metadata"]["federation"]["exclusions"] == true - end -end diff --git a/test/web/oauth/app_test.exs b/test/web/oauth/app_test.exs @@ -1,44 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.AppTest do - use Pleroma.DataCase - - alias Pleroma.Web.OAuth.App - import Pleroma.Factory - - describe "get_or_make/2" do - test "gets exist app" do - attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} - app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]})) - {:ok, %App{} = exist_app} = App.get_or_make(attrs, []) - assert exist_app == app - end - - test "make app" do - attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} - {:ok, %App{} = app} = App.get_or_make(attrs, ["write"]) - assert app.scopes == ["write"] - end - - test "gets exist app and updates scopes" do - attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} - app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]})) - {:ok, %App{} = exist_app} = App.get_or_make(attrs, ["read", "write", "follow", "push"]) - assert exist_app.id == app.id - assert exist_app.scopes == ["read", "write", "follow", "push"] - end - - test "has unique client_id" do - insert(:oauth_app, client_name: "", redirect_uris: "", client_id: "boop") - - error = - catch_error(insert(:oauth_app, client_name: "", redirect_uris: "", client_id: "boop")) - - assert %Ecto.ConstraintError{} = error - assert error.constraint == "apps_client_id_index" - assert error.type == :unique - end - end -end diff --git a/test/web/oauth/authorization_test.exs b/test/web/oauth/authorization_test.exs @@ -1,77 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.AuthorizationTest do - use Pleroma.DataCase - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Authorization - import Pleroma.Factory - - setup do - {:ok, app} = - Repo.insert( - App.register_changeset(%App{}, %{ - client_name: "client", - scopes: ["read", "write"], - redirect_uris: "url" - }) - ) - - %{app: app} - end - - test "create an authorization token for a valid app", %{app: app} do - user = insert(:user) - - {:ok, auth1} = Authorization.create_authorization(app, user) - assert auth1.scopes == app.scopes - - {:ok, auth2} = Authorization.create_authorization(app, user, ["read"]) - assert auth2.scopes == ["read"] - - for auth <- [auth1, auth2] do - assert auth.user_id == user.id - assert auth.app_id == app.id - assert String.length(auth.token) > 10 - assert auth.used == false - end - end - - test "use up a token", %{app: app} do - user = insert(:user) - - {:ok, auth} = Authorization.create_authorization(app, user) - - {:ok, auth} = Authorization.use_token(auth) - - assert auth.used == true - - assert {:error, "already used"} == Authorization.use_token(auth) - - expired_auth = %Authorization{ - user_id: user.id, - app_id: app.id, - valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -10), - token: "mytoken", - used: false - } - - {:ok, expired_auth} = Repo.insert(expired_auth) - - assert {:error, "token expired"} == Authorization.use_token(expired_auth) - end - - test "delete authorizations", %{app: app} do - user = insert(:user) - - {:ok, auth} = Authorization.create_authorization(app, user) - {:ok, auth} = Authorization.use_token(auth) - - Authorization.delete_user_authorizations(user) - - {_, invalid} = Authorization.use_token(auth) - - assert auth != invalid - end -end diff --git a/test/web/oauth/ldap_authorization_test.exs b/test/web/oauth/ldap_authorization_test.exs @@ -1,135 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do - use Pleroma.Web.ConnCase - alias Pleroma.Repo - alias Pleroma.Web.OAuth.Token - import Pleroma.Factory - import Mock - - @skip if !Code.ensure_loaded?(:eldap), do: :skip - - setup_all do: clear_config([:ldap, :enabled], true) - - setup_all do: clear_config(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.LDAPAuthenticator) - - @tag @skip - test "authorizes the existing user using LDAP credentials" do - password = "testpassword" - user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) - app = insert(:oauth_app, scopes: ["read", "write"]) - - host = Pleroma.Config.get([:ldap, :host]) |> to_charlist - port = Pleroma.Config.get([:ldap, :port]) - - with_mocks [ - {:eldap, [], - [ - open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end, - simple_bind: fn _connection, _dn, ^password -> :ok end, - close: fn _connection -> - send(self(), :close_connection) - :ok - end - ]} - ] do - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert %{"access_token" => token} = json_response(conn, 200) - - token = Repo.get_by(Token, token: token) - - assert token.user_id == user.id - assert_received :close_connection - end - end - - @tag @skip - test "creates a new user after successful LDAP authorization" do - password = "testpassword" - user = build(:user) - app = insert(:oauth_app, scopes: ["read", "write"]) - - host = Pleroma.Config.get([:ldap, :host]) |> to_charlist - port = Pleroma.Config.get([:ldap, :port]) - - with_mocks [ - {:eldap, [], - [ - open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end, - simple_bind: fn _connection, _dn, ^password -> :ok end, - equalityMatch: fn _type, _value -> :ok end, - wholeSubtree: fn -> :ok end, - search: fn _connection, _options -> - {:ok, {:eldap_search_result, [{:eldap_entry, '', []}], []}} - end, - close: fn _connection -> - send(self(), :close_connection) - :ok - end - ]} - ] do - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert %{"access_token" => token} = json_response(conn, 200) - - token = Repo.get_by(Token, token: token) |> Repo.preload(:user) - - assert token.user.nickname == user.nickname - assert_received :close_connection - end - end - - @tag @skip - test "disallow authorization for wrong LDAP credentials" do - password = "testpassword" - user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) - app = insert(:oauth_app, scopes: ["read", "write"]) - - host = Pleroma.Config.get([:ldap, :host]) |> to_charlist - port = Pleroma.Config.get([:ldap, :port]) - - with_mocks [ - {:eldap, [], - [ - open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end, - simple_bind: fn _connection, _dn, ^password -> {:error, :invalidCredentials} end, - close: fn _connection -> - send(self(), :close_connection) - :ok - end - ]} - ] do - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert %{"error" => "Invalid credentials"} = json_response(conn, 400) - assert_received :close_connection - end - end -end diff --git a/test/web/oauth/mfa_controller_test.exs b/test/web/oauth/mfa_controller_test.exs @@ -1,306 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.MFAControllerTest do - use Pleroma.Web.ConnCase - import Pleroma.Factory - - alias Pleroma.MFA - alias Pleroma.MFA.BackupCodes - alias Pleroma.MFA.TOTP - alias Pleroma.Repo - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.OAuthController - - setup %{conn: conn} do - otp_secret = TOTP.generate_secret() - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - backup_codes: [Pbkdf2.hash_pwd_salt("test-code")], - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - app = insert(:oauth_app) - {:ok, conn: conn, user: user, app: app} - end - - describe "show" do - setup %{conn: conn, user: user, app: app} do - mfa_token = - insert(:mfa_token, - user: user, - authorization: build(:oauth_authorization, app: app, scopes: ["write"]) - ) - - {:ok, conn: conn, mfa_token: mfa_token} - end - - test "GET /oauth/mfa renders mfa forms", %{conn: conn, mfa_token: mfa_token} do - conn = - get( - conn, - "/oauth/mfa", - %{ - "mfa_token" => mfa_token.token, - "state" => "a_state", - "redirect_uri" => "http://localhost:8080/callback" - } - ) - - assert response = html_response(conn, 200) - assert response =~ "Two-factor authentication" - assert response =~ mfa_token.token - assert response =~ "http://localhost:8080/callback" - end - - test "GET /oauth/mfa renders mfa recovery forms", %{conn: conn, mfa_token: mfa_token} do - conn = - get( - conn, - "/oauth/mfa", - %{ - "mfa_token" => mfa_token.token, - "state" => "a_state", - "redirect_uri" => "http://localhost:8080/callback", - "challenge_type" => "recovery" - } - ) - - assert response = html_response(conn, 200) - assert response =~ "Two-factor recovery" - assert response =~ mfa_token.token - assert response =~ "http://localhost:8080/callback" - end - end - - describe "verify" do - setup %{conn: conn, user: user, app: app} do - mfa_token = - insert(:mfa_token, - user: user, - authorization: build(:oauth_authorization, app: app, scopes: ["write"]) - ) - - {:ok, conn: conn, user: user, mfa_token: mfa_token, app: app} - end - - test "POST /oauth/mfa/verify, verify totp code", %{ - conn: conn, - user: user, - mfa_token: mfa_token, - app: app - } do - otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) - - conn = - conn - |> post("/oauth/mfa/verify", %{ - "mfa" => %{ - "mfa_token" => mfa_token.token, - "challenge_type" => "totp", - "code" => otp_token, - "state" => "a_state", - "redirect_uri" => OAuthController.default_redirect_uri(app) - } - }) - - target = redirected_to(conn) - target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string() - query = URI.parse(target).query |> URI.query_decoder() |> Map.new() - assert %{"state" => "a_state", "code" => code} = query - assert target_url == OAuthController.default_redirect_uri(app) - auth = Repo.get_by(Authorization, token: code) - assert auth.scopes == ["write"] - end - - test "POST /oauth/mfa/verify, verify recovery code", %{ - conn: conn, - mfa_token: mfa_token, - app: app - } do - conn = - conn - |> post("/oauth/mfa/verify", %{ - "mfa" => %{ - "mfa_token" => mfa_token.token, - "challenge_type" => "recovery", - "code" => "test-code", - "state" => "a_state", - "redirect_uri" => OAuthController.default_redirect_uri(app) - } - }) - - target = redirected_to(conn) - target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string() - query = URI.parse(target).query |> URI.query_decoder() |> Map.new() - assert %{"state" => "a_state", "code" => code} = query - assert target_url == OAuthController.default_redirect_uri(app) - auth = Repo.get_by(Authorization, token: code) - assert auth.scopes == ["write"] - end - end - - describe "challenge/totp" do - test "returns access token with valid code", %{conn: conn, user: user, app: app} do - otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) - - mfa_token = - insert(:mfa_token, - user: user, - authorization: build(:oauth_authorization, app: app, scopes: ["write"]) - ) - - response = - conn - |> post("/oauth/mfa/challenge", %{ - "mfa_token" => mfa_token.token, - "challenge_type" => "totp", - "code" => otp_token, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(:ok) - - ap_id = user.ap_id - - assert match?( - %{ - "access_token" => _, - "expires_in" => 600, - "me" => ^ap_id, - "refresh_token" => _, - "scope" => "write", - "token_type" => "Bearer" - }, - response - ) - end - - test "returns errors when mfa token invalid", %{conn: conn, user: user, app: app} do - otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) - - response = - conn - |> post("/oauth/mfa/challenge", %{ - "mfa_token" => "XXX", - "challenge_type" => "totp", - "code" => otp_token, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(400) - - assert response == %{"error" => "Invalid code"} - end - - test "returns error when otp code is invalid", %{conn: conn, user: user, app: app} do - mfa_token = insert(:mfa_token, user: user) - - response = - conn - |> post("/oauth/mfa/challenge", %{ - "mfa_token" => mfa_token.token, - "challenge_type" => "totp", - "code" => "XXX", - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(400) - - assert response == %{"error" => "Invalid code"} - end - - test "returns error when client credentails 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) - - response = - conn - |> post("/oauth/mfa/challenge", %{ - "mfa_token" => mfa_token.token, - "challenge_type" => "totp", - "code" => otp_token, - "client_id" => "xxx", - "client_secret" => "xxx" - }) - |> json_response(400) - - assert response == %{"error" => "Invalid code"} - end - end - - describe "challenge/recovery" do - setup %{conn: conn} do - app = insert(:oauth_app) - {:ok, conn: conn, app: app} - end - - test "returns access token with valid code", %{conn: conn, app: app} do - otp_secret = TOTP.generate_secret() - - [code | _] = backup_codes = BackupCodes.generate() - - hashed_codes = - backup_codes - |> Enum.map(&Pbkdf2.hash_pwd_salt(&1)) - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - backup_codes: hashed_codes, - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - mfa_token = - insert(:mfa_token, - user: user, - authorization: build(:oauth_authorization, app: app, scopes: ["write"]) - ) - - response = - conn - |> post("/oauth/mfa/challenge", %{ - "mfa_token" => mfa_token.token, - "challenge_type" => "recovery", - "code" => code, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(:ok) - - ap_id = user.ap_id - - assert match?( - %{ - "access_token" => _, - "expires_in" => 600, - "me" => ^ap_id, - "refresh_token" => _, - "scope" => "write", - "token_type" => "Bearer" - }, - response - ) - - error_response = - conn - |> post("/oauth/mfa/challenge", %{ - "mfa_token" => mfa_token.token, - "challenge_type" => "recovery", - "code" => code, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(400) - - assert error_response == %{"error" => "Invalid code"} - end - end -end diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs @@ -1,1232 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.OAuthControllerTest do - use Pleroma.Web.ConnCase - import Pleroma.Factory - - alias Pleroma.MFA - alias Pleroma.MFA.TOTP - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.OAuthController - alias Pleroma.Web.OAuth.Token - - @session_opts [ - store: :cookie, - key: "_test", - signing_salt: "cooldude" - ] - setup do - clear_config([:instance, :account_activation_required]) - clear_config([:instance, :account_approval_required]) - end - - describe "in OAuth consumer mode, " do - setup do - [ - app: insert(:oauth_app), - conn: - build_conn() - |> Plug.Session.call(Plug.Session.init(@session_opts)) - |> fetch_session() - ] - end - - setup do: clear_config([:auth, :oauth_consumer_strategies], ~w(twitter facebook)) - - test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{ - app: app, - conn: conn - } do - conn = - get( - conn, - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "scope" => "read" - } - ) - - assert response = html_response(conn, 200) - assert response =~ "Sign in with Twitter" - assert response =~ o_auth_path(conn, :prepare_request) - end - - test "GET /oauth/prepare_request encodes parameters as `state` and redirects", %{ - app: app, - conn: conn - } do - conn = - get( - conn, - "/oauth/prepare_request", - %{ - "provider" => "twitter", - "authorization" => %{ - "scope" => "read follow", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "a_state" - } - } - ) - - assert response = html_response(conn, 302) - - redirect_query = URI.parse(redirected_to(conn)).query - assert %{"state" => state_param} = URI.decode_query(redirect_query) - assert {:ok, state_components} = Poison.decode(state_param) - - expected_client_id = app.client_id - expected_redirect_uri = app.redirect_uris - - assert %{ - "scope" => "read follow", - "client_id" => ^expected_client_id, - "redirect_uri" => ^expected_redirect_uri, - "state" => "a_state" - } = state_components - end - - test "with user-bound registration, GET /oauth/<provider>/callback redirects to `redirect_uri` with `code`", - %{app: app, conn: conn} do - registration = insert(:registration) - redirect_uri = OAuthController.default_redirect_uri(app) - - state_params = %{ - "scope" => Enum.join(app.scopes, " "), - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "" - } - - conn = - conn - |> assign(:ueberauth_auth, %{provider: registration.provider, uid: registration.uid}) - |> get( - "/oauth/twitter/callback", - %{ - "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", - "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", - "provider" => "twitter", - "state" => Poison.encode!(state_params) - } - ) - - assert response = html_response(conn, 302) - assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ - end - - test "with user-unbound registration, GET /oauth/<provider>/callback renders registration_details page", - %{app: app, conn: conn} do - user = insert(:user) - - state_params = %{ - "scope" => "read write", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "a_state" - } - - conn = - conn - |> assign(:ueberauth_auth, %{ - provider: "twitter", - uid: "171799000", - info: %{nickname: user.nickname, email: user.email, name: user.name, description: nil} - }) - |> get( - "/oauth/twitter/callback", - %{ - "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", - "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", - "provider" => "twitter", - "state" => Poison.encode!(state_params) - } - ) - - assert response = html_response(conn, 200) - assert response =~ ~r/name="op" type="submit" value="register"/ - assert response =~ ~r/name="op" type="submit" value="connect"/ - assert response =~ user.email - assert response =~ user.nickname - end - - test "on authentication error, GET /oauth/<provider>/callback redirects to `redirect_uri`", %{ - app: app, - conn: conn - } do - state_params = %{ - "scope" => Enum.join(app.scopes, " "), - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "" - } - - conn = - conn - |> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]}) - |> get( - "/oauth/twitter/callback", - %{ - "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", - "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", - "provider" => "twitter", - "state" => Poison.encode!(state_params) - } - ) - - assert response = html_response(conn, 302) - assert redirected_to(conn) == app.redirect_uris - assert get_flash(conn, :error) == "Failed to authenticate: (error description)." - end - - test "GET /oauth/registration_details renders registration details form", %{ - app: app, - conn: conn - } do - conn = - get( - conn, - "/oauth/registration_details", - %{ - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "a_state", - "nickname" => nil, - "email" => "john@doe.com" - } - } - ) - - assert response = html_response(conn, 200) - assert response =~ ~r/name="op" type="submit" value="register"/ - assert response =~ ~r/name="op" type="submit" value="connect"/ - end - - test "with valid params, POST /oauth/register?op=register redirects to `redirect_uri` with `code`", - %{ - app: app, - conn: conn - } do - registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) - redirect_uri = OAuthController.default_redirect_uri(app) - - conn = - conn - |> put_session(:registration_id, registration.id) - |> post( - "/oauth/register", - %{ - "op" => "register", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "a_state", - "nickname" => "availablenick", - "email" => "available@email.com" - } - } - ) - - assert response = html_response(conn, 302) - assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ - end - - test "with unlisted `redirect_uri`, POST /oauth/register?op=register results in HTTP 401", - %{ - app: app, - conn: conn - } do - registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) - unlisted_redirect_uri = "http://cross-site-request.com" - - conn = - conn - |> put_session(:registration_id, registration.id) - |> post( - "/oauth/register", - %{ - "op" => "register", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => unlisted_redirect_uri, - "state" => "a_state", - "nickname" => "availablenick", - "email" => "available@email.com" - } - } - ) - - assert response = html_response(conn, 401) - end - - test "with invalid params, POST /oauth/register?op=register renders registration_details page", - %{ - app: app, - conn: conn - } do - another_user = insert(:user) - registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) - - params = %{ - "op" => "register", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "a_state", - "nickname" => "availablenickname", - "email" => "available@email.com" - } - } - - for {bad_param, bad_param_value} <- - [{"nickname", another_user.nickname}, {"email", another_user.email}] do - bad_registration_attrs = %{ - "authorization" => Map.put(params["authorization"], bad_param, bad_param_value) - } - - bad_params = Map.merge(params, bad_registration_attrs) - - conn = - conn - |> put_session(:registration_id, registration.id) - |> 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." - end - end - - test "with valid params, POST /oauth/register?op=connect redirects to `redirect_uri` with `code`", - %{ - app: app, - conn: conn - } do - user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt("testpassword")) - registration = insert(:registration, user: nil) - redirect_uri = OAuthController.default_redirect_uri(app) - - conn = - conn - |> put_session(:registration_id, registration.id) - |> post( - "/oauth/register", - %{ - "op" => "connect", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "a_state", - "name" => user.nickname, - "password" => "testpassword" - } - } - ) - - assert response = html_response(conn, 302) - assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ - end - - test "with unlisted `redirect_uri`, POST /oauth/register?op=connect results in HTTP 401`", - %{ - app: app, - conn: conn - } do - user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt("testpassword")) - registration = insert(:registration, user: nil) - unlisted_redirect_uri = "http://cross-site-request.com" - - conn = - conn - |> put_session(:registration_id, registration.id) - |> post( - "/oauth/register", - %{ - "op" => "connect", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => unlisted_redirect_uri, - "state" => "a_state", - "name" => user.nickname, - "password" => "testpassword" - } - } - ) - - assert response = html_response(conn, 401) - end - - test "with invalid params, POST /oauth/register?op=connect renders registration_details page", - %{ - app: app, - conn: conn - } do - user = insert(:user) - registration = insert(:registration, user: nil) - - params = %{ - "op" => "connect", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "a_state", - "name" => user.nickname, - "password" => "wrong password" - } - } - - conn = - conn - |> put_session(:registration_id, registration.id) - |> post("/oauth/register", params) - - assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/ - assert get_flash(conn, :error) == "Invalid Username/Password" - end - end - - describe "GET /oauth/authorize" do - setup do - [ - app: insert(:oauth_app, redirect_uris: "https://redirect.url"), - conn: - build_conn() - |> Plug.Session.call(Plug.Session.init(@session_opts)) - |> fetch_session() - ] - end - - test "renders authentication page", %{app: app, conn: conn} do - conn = - get( - conn, - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "scope" => "read" - } - ) - - assert html_response(conn, 200) =~ ~s(type="submit") - end - - test "properly handles internal calls with `authorization`-wrapped params", %{ - app: app, - conn: conn - } do - conn = - get( - conn, - "/oauth/authorize", - %{ - "authorization" => %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "scope" => "read" - } - } - ) - - assert html_response(conn, 200) =~ ~s(type="submit") - end - - test "renders authentication page if user is already authenticated but `force_login` is tru-ish", - %{app: app, conn: conn} do - token = insert(:oauth_token, app: app) - - conn = - conn - |> put_session(:oauth_token, token.token) - |> get( - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "scope" => "read", - "force_login" => "true" - } - ) - - assert html_response(conn, 200) =~ ~s(type="submit") - end - - test "renders authentication page if user is already authenticated but user request with another client", - %{ - app: app, - conn: conn - } do - token = insert(:oauth_token, app: app) - - conn = - conn - |> put_session(:oauth_token, token.token) - |> get( - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => "another_client_id", - "redirect_uri" => OAuthController.default_redirect_uri(app), - "scope" => "read" - } - ) - - assert html_response(conn, 200) =~ ~s(type="submit") - end - - test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params", - %{ - app: app, - conn: conn - } do - token = insert(:oauth_token, app: app) - - conn = - conn - |> put_session(:oauth_token, token.token) - |> get( - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "specific_client_state", - "scope" => "read" - } - ) - - assert URI.decode(redirected_to(conn)) == - "https://redirect.url?access_token=#{token.token}&state=specific_client_state" - end - - test "with existing authentication and unlisted non-OOB `redirect_uri`, redirects without credentials", - %{ - app: app, - conn: conn - } do - unlisted_redirect_uri = "http://cross-site-request.com" - token = insert(:oauth_token, app: app) - - conn = - conn - |> put_session(:oauth_token, token.token) - |> get( - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => unlisted_redirect_uri, - "state" => "specific_client_state", - "scope" => "read" - } - ) - - assert redirected_to(conn) == unlisted_redirect_uri - end - - test "with existing authentication and OOB `redirect_uri`, redirects to app with `token` and `state` params", - %{ - app: app, - conn: conn - } do - token = insert(:oauth_token, app: app) - - conn = - conn - |> put_session(:oauth_token, token.token) - |> get( - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "scope" => "read" - } - ) - - assert html_response(conn, 200) =~ "Authorization exists" - end - end - - describe "POST /oauth/authorize" do - test "redirects with oauth authorization, " <> - "granting requested app-supported scopes to both admin- and non-admin users" do - app_scopes = ["read", "write", "admin", "secret_scope"] - app = insert(:oauth_app, scopes: app_scopes) - redirect_uri = OAuthController.default_redirect_uri(app) - - non_admin = insert(:user, is_admin: false) - admin = insert(:user, is_admin: true) - scopes_subset = ["read:subscope", "write", "admin"] - - # In case scope param is missing, expecting _all_ app-supported scopes to be granted - for user <- [non_admin, admin], - {requested_scopes, expected_scopes} <- - %{scopes_subset => scopes_subset, nil: app_scopes} do - conn = - post( - build_conn(), - "/oauth/authorize", - %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "test", - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "scope" => requested_scopes, - "state" => "statepassed" - } - } - ) - - target = redirected_to(conn) - assert target =~ redirect_uri - - query = URI.parse(target).query |> URI.query_decoder() |> Map.new() - - assert %{"state" => "statepassed", "code" => code} = query - auth = Repo.get_by(Authorization, token: code) - assert auth - assert auth.scopes == expected_scopes - end - end - - test "redirect to on two-factor auth page" do - otp_secret = TOTP.generate_secret() - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - app = insert(:oauth_app, scopes: ["read", "write", "follow"]) - - conn = - build_conn() - |> post("/oauth/authorize", %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "test", - "client_id" => app.client_id, - "redirect_uri" => app.redirect_uris, - "scope" => "read write", - "state" => "statepassed" - } - }) - - result = html_response(conn, 200) - - mfa_token = Repo.get_by(MFA.Token, user_id: user.id) - assert result =~ app.redirect_uris - assert result =~ "statepassed" - assert result =~ mfa_token.token - assert result =~ "Two-factor authentication" - end - - test "returns 401 for wrong credentials", %{conn: conn} do - user = insert(:user) - app = insert(:oauth_app) - redirect_uri = OAuthController.default_redirect_uri(app) - - result = - conn - |> post("/oauth/authorize", %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "wrong", - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "statepassed", - "scope" => Enum.join(app.scopes, " ") - } - }) - |> html_response(:unauthorized) - - # Keep the details - assert result =~ app.client_id - assert result =~ redirect_uri - - # Error message - assert result =~ "Invalid Username/Password" - end - - test "returns 401 for missing scopes" do - user = insert(:user, is_admin: false) - app = insert(:oauth_app, scopes: ["read", "write", "admin"]) - redirect_uri = OAuthController.default_redirect_uri(app) - - result = - build_conn() - |> post("/oauth/authorize", %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "test", - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "statepassed", - "scope" => "" - } - }) - |> html_response(:unauthorized) - - # Keep the details - assert result =~ app.client_id - assert result =~ redirect_uri - - # Error message - assert result =~ "This action is outside the authorized scopes" - end - - test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do - user = insert(:user) - app = insert(:oauth_app, scopes: ["read", "write"]) - redirect_uri = OAuthController.default_redirect_uri(app) - - result = - conn - |> post("/oauth/authorize", %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "test", - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "statepassed", - "scope" => "read write follow" - } - }) - |> html_response(:unauthorized) - - # Keep the details - assert result =~ app.client_id - assert result =~ redirect_uri - - # Error message - assert result =~ "This action is outside the authorized scopes" - end - end - - describe "POST /oauth/token" do - test "issues a token for an all-body request" do - user = insert(:user) - app = insert(:oauth_app, scopes: ["read", "write"]) - - {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) - - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "authorization_code", - "code" => auth.token, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert %{"access_token" => token, "me" => ap_id} = json_response(conn, 200) - - token = Repo.get_by(Token, token: token) - assert token - assert token.scopes == auth.scopes - assert user.ap_id == ap_id - end - - test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do - password = "testpassword" - user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) - - app = insert(:oauth_app, scopes: ["read", "write"]) - - # Note: "scope" param is intentionally omitted - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert %{"access_token" => token} = json_response(conn, 200) - - token = Repo.get_by(Token, token: token) - assert token - assert token.scopes == app.scopes - end - - test "issues a mfa token for `password` grant_type, when MFA enabled" do - password = "testpassword" - otp_secret = TOTP.generate_secret() - - user = - insert(:user, - password_hash: Pbkdf2.hash_pwd_salt(password), - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - app = insert(:oauth_app, scopes: ["read", "write"]) - - response = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(403) - - assert match?( - %{ - "supported_challenge_types" => "totp", - "mfa_token" => _, - "error" => "mfa_required" - }, - response - ) - - token = Repo.get_by(MFA.Token, token: response["mfa_token"]) - assert token.user_id == user.id - assert token.authorization_id - end - - test "issues a token for request with HTTP basic auth client credentials" do - user = insert(:user) - app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"]) - - {:ok, auth} = Authorization.create_authorization(app, user, ["scope1", "scope2"]) - assert auth.scopes == ["scope1", "scope2"] - - app_encoded = - (URI.encode_www_form(app.client_id) <> ":" <> URI.encode_www_form(app.client_secret)) - |> Base.encode64() - - conn = - build_conn() - |> put_req_header("authorization", "Basic " <> app_encoded) - |> post("/oauth/token", %{ - "grant_type" => "authorization_code", - "code" => auth.token, - "redirect_uri" => OAuthController.default_redirect_uri(app) - }) - - assert %{"access_token" => token, "scope" => scope} = json_response(conn, 200) - - assert scope == "scope1 scope2" - - token = Repo.get_by(Token, token: token) - assert token - assert token.scopes == ["scope1", "scope2"] - end - - test "issue a token for client_credentials grant type" do - app = insert(:oauth_app, scopes: ["read", "write"]) - - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "client_credentials", - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = - json_response(conn, 200) - - assert token - token_from_db = Repo.get_by(Token, token: token) - assert token_from_db - assert refresh - assert scope == "read write" - end - - test "rejects token exchange with invalid client credentials" do - user = insert(:user) - app = insert(:oauth_app) - - {:ok, auth} = Authorization.create_authorization(app, user) - - conn = - build_conn() - |> put_req_header("authorization", "Basic JTIxOiVGMCU5RiVBNCVCNwo=") - |> post("/oauth/token", %{ - "grant_type" => "authorization_code", - "code" => auth.token, - "redirect_uri" => OAuthController.default_redirect_uri(app) - }) - - assert resp = json_response(conn, 400) - assert %{"error" => _} = resp - refute Map.has_key?(resp, "access_token") - end - - test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do - Pleroma.Config.put([:instance, :account_activation_required], true) - password = "testpassword" - - {:ok, user} = - insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) - |> User.confirmation_changeset(need_confirmation: true) - |> User.update_and_set_cache() - - refute Pleroma.User.account_status(user) == :active - - app = insert(:oauth_app) - - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert resp = json_response(conn, 403) - assert %{"error" => _} = resp - refute Map.has_key?(resp, "access_token") - end - - test "rejects token exchange for valid credentials belonging to deactivated user" do - password = "testpassword" - - user = - insert(:user, - password_hash: Pbkdf2.hash_pwd_salt(password), - deactivated: true - ) - - app = insert(:oauth_app) - - resp = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(403) - - assert resp == %{ - "error" => "Your account is currently disabled", - "identifier" => "account_is_disabled" - } - end - - test "rejects token exchange for user with password_reset_pending set to true" do - password = "testpassword" - - user = - insert(:user, - password_hash: Pbkdf2.hash_pwd_salt(password), - password_reset_pending: true - ) - - app = insert(:oauth_app, scopes: ["read", "write"]) - - resp = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(403) - - assert resp == %{ - "error" => "Password reset is required", - "identifier" => "password_reset_required" - } - end - - test "rejects token exchange for user with confirmation_pending set to true" do - Pleroma.Config.put([:instance, :account_activation_required], true) - password = "testpassword" - - user = - insert(:user, - password_hash: Pbkdf2.hash_pwd_salt(password), - confirmation_pending: true - ) - - app = insert(:oauth_app, scopes: ["read", "write"]) - - resp = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(403) - - assert resp == %{ - "error" => "Your login is missing a confirmed e-mail address", - "identifier" => "missing_confirmed_email" - } - end - - test "rejects token exchange for valid credentials belonging to an unapproved user" do - password = "testpassword" - - user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password), approval_pending: true) - - refute Pleroma.User.account_status(user) == :active - - app = insert(:oauth_app) - - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert resp = json_response(conn, 403) - assert %{"error" => _} = resp - refute Map.has_key?(resp, "access_token") - end - - test "rejects an invalid authorization code" do - app = insert(:oauth_app) - - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "authorization_code", - "code" => "Imobviouslyinvalid", - "redirect_uri" => OAuthController.default_redirect_uri(app), - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert resp = json_response(conn, 400) - assert %{"error" => _} = json_response(conn, 400) - refute Map.has_key?(resp, "access_token") - end - end - - describe "POST /oauth/token - refresh token" do - setup do: clear_config([:oauth2, :issue_new_refresh_token]) - - test "issues a new access token with keep fresh token" do - Pleroma.Config.put([:oauth2, :issue_new_refresh_token], true) - user = insert(:user) - app = insert(:oauth_app, scopes: ["read", "write"]) - - {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) - {:ok, token} = Token.exchange_token(app, auth) - - response = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "refresh_token", - "refresh_token" => token.refresh_token, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(200) - - ap_id = user.ap_id - - assert match?( - %{ - "scope" => "write", - "token_type" => "Bearer", - "expires_in" => 600, - "access_token" => _, - "refresh_token" => _, - "me" => ^ap_id - }, - response - ) - - refute Repo.get_by(Token, token: token.token) - new_token = Repo.get_by(Token, token: response["access_token"]) - assert new_token.refresh_token == token.refresh_token - assert new_token.scopes == auth.scopes - assert new_token.user_id == user.id - assert new_token.app_id == app.id - end - - test "issues a new access token with new fresh token" do - Pleroma.Config.put([:oauth2, :issue_new_refresh_token], false) - user = insert(:user) - app = insert(:oauth_app, scopes: ["read", "write"]) - - {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) - {:ok, token} = Token.exchange_token(app, auth) - - response = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "refresh_token", - "refresh_token" => token.refresh_token, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(200) - - ap_id = user.ap_id - - assert match?( - %{ - "scope" => "write", - "token_type" => "Bearer", - "expires_in" => 600, - "access_token" => _, - "refresh_token" => _, - "me" => ^ap_id - }, - response - ) - - refute Repo.get_by(Token, token: token.token) - new_token = Repo.get_by(Token, token: response["access_token"]) - refute new_token.refresh_token == token.refresh_token - assert new_token.scopes == auth.scopes - assert new_token.user_id == user.id - assert new_token.app_id == app.id - end - - test "returns 400 if we try use access token" do - user = insert(:user) - app = insert(:oauth_app, scopes: ["read", "write"]) - - {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) - {:ok, token} = Token.exchange_token(app, auth) - - response = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "refresh_token", - "refresh_token" => token.token, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(400) - - assert %{"error" => "Invalid credentials"} == response - end - - test "returns 400 if refresh_token invalid" do - app = insert(:oauth_app, scopes: ["read", "write"]) - - response = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "refresh_token", - "refresh_token" => "token.refresh_token", - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(400) - - assert %{"error" => "Invalid credentials"} == response - end - - test "issues a new token if token expired" do - user = insert(:user) - app = insert(:oauth_app, scopes: ["read", "write"]) - - {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) - {:ok, token} = Token.exchange_token(app, auth) - - change = - Ecto.Changeset.change( - token, - %{valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -86_400 * 30)} - ) - - {:ok, access_token} = Repo.update(change) - - response = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "refresh_token", - "refresh_token" => access_token.refresh_token, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(200) - - ap_id = user.ap_id - - assert match?( - %{ - "scope" => "write", - "token_type" => "Bearer", - "expires_in" => 600, - "access_token" => _, - "refresh_token" => _, - "me" => ^ap_id - }, - response - ) - - refute Repo.get_by(Token, token: token.token) - token = Repo.get_by(Token, token: response["access_token"]) - assert token - assert token.scopes == auth.scopes - assert token.user_id == user.id - assert token.app_id == app.id - end - end - - describe "POST /oauth/token - bad request" do - test "returns 500" do - response = - build_conn() - |> post("/oauth/token", %{}) - |> json_response(500) - - assert %{"error" => "Bad request"} == response - end - end - - describe "POST /oauth/revoke - bad request" do - test "returns 500" do - response = - build_conn() - |> post("/oauth/revoke", %{}) - |> json_response(500) - - assert %{"error" => "Bad request"} == response - end - end -end diff --git a/test/web/oauth/token/utils_test.exs b/test/web/oauth/token/utils_test.exs @@ -1,53 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.UtilsTest do - use Pleroma.DataCase - alias Pleroma.Web.OAuth.Token.Utils - import Pleroma.Factory - - describe "fetch_app/1" do - test "returns error when credentials is invalid" do - assert {:error, :not_found} = - Utils.fetch_app(%Plug.Conn{params: %{"client_id" => 1, "client_secret" => "x"}}) - end - - test "returns App by params credentails" do - app = insert(:oauth_app) - - assert {:ok, load_app} = - Utils.fetch_app(%Plug.Conn{ - params: %{"client_id" => app.client_id, "client_secret" => app.client_secret} - }) - - assert load_app == app - end - - test "returns App by header credentails" do - app = insert(:oauth_app) - header = "Basic " <> Base.encode64("#{app.client_id}:#{app.client_secret}") - - conn = - %Plug.Conn{} - |> Plug.Conn.put_req_header("authorization", header) - - assert {:ok, load_app} = Utils.fetch_app(conn) - assert load_app == app - end - end - - describe "format_created_at/1" do - test "returns formatted created at" do - token = insert(:oauth_token) - date = Utils.format_created_at(token) - - token_date = - token.inserted_at - |> DateTime.from_naive!("Etc/UTC") - |> DateTime.to_unix() - - assert token_date == date - end - end -end diff --git a/test/web/oauth/token_test.exs b/test/web/oauth/token_test.exs @@ -1,72 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.TokenTest do - use Pleroma.DataCase - alias Pleroma.Repo - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.Token - - import Pleroma.Factory - - test "exchanges a auth token for an access token, preserving `scopes`" do - {:ok, app} = - Repo.insert( - App.register_changeset(%App{}, %{ - client_name: "client", - scopes: ["read", "write"], - redirect_uris: "url" - }) - ) - - user = insert(:user) - - {:ok, auth} = Authorization.create_authorization(app, user, ["read"]) - assert auth.scopes == ["read"] - - {:ok, token} = Token.exchange_token(app, auth) - - assert token.app_id == app.id - assert token.user_id == user.id - assert token.scopes == auth.scopes - assert String.length(token.token) > 10 - assert String.length(token.refresh_token) > 10 - - auth = Repo.get(Authorization, auth.id) - {:error, "already used"} = Token.exchange_token(app, auth) - end - - test "deletes all tokens of a user" do - {:ok, app1} = - Repo.insert( - App.register_changeset(%App{}, %{ - client_name: "client1", - scopes: ["scope"], - redirect_uris: "url" - }) - ) - - {:ok, app2} = - Repo.insert( - App.register_changeset(%App{}, %{ - client_name: "client2", - scopes: ["scope"], - redirect_uris: "url" - }) - ) - - user = insert(:user) - - {:ok, auth1} = Authorization.create_authorization(app1, user) - {:ok, auth2} = Authorization.create_authorization(app2, user) - - {:ok, _token1} = Token.exchange_token(app1, auth1) - {:ok, _token2} = Token.exchange_token(app2, auth2) - - {tokens, _} = Token.delete_user_tokens(user) - - assert tokens == 2 - end -end diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs @@ -1,338 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OStatus.OStatusControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - alias Pleroma.Config - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Endpoint - - require Pleroma.Constants - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup do: clear_config([:instance, :federating], true) - - describe "Mastodon compatibility routes" do - setup %{conn: conn} do - conn = put_req_header(conn, "accept", "text/html") - - {:ok, object} = - %{ - "type" => "Note", - "content" => "hey", - "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999", - "actor" => Endpoint.url() <> "/users/raymoo", - "to" => [Pleroma.Constants.as_public()] - } - |> Object.create() - - {:ok, activity, _} = - %{ - "id" => object.data["id"] <> "/activity", - "type" => "Create", - "object" => object.data["id"], - "actor" => object.data["actor"], - "to" => object.data["to"] - } - |> ActivityPub.persist(local: true) - - %{conn: conn, activity: activity} - end - - test "redirects to /notice/:id for html format", %{conn: conn, activity: activity} do - conn = get(conn, "/users/raymoo/statuses/999999999") - assert redirected_to(conn) == "/notice/#{activity.id}" - end - - test "redirects to /notice/:id for html format for activity", %{ - conn: conn, - activity: activity - } do - conn = get(conn, "/users/raymoo/statuses/999999999/activity") - assert redirected_to(conn) == "/notice/#{activity.id}" - end - end - - # Note: see ActivityPubControllerTest for JSON format tests - describe "GET /objects/:uuid (text/html)" do - setup %{conn: conn} do - conn = put_req_header(conn, "accept", "text/html") - %{conn: conn} - end - - test "redirects to /notice/id for html format", %{conn: conn} do - note_activity = insert(:note_activity) - object = Object.normalize(note_activity) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) - url = "/objects/#{uuid}" - - conn = get(conn, url) - assert redirected_to(conn) == "/notice/#{note_activity.id}" - end - - test "404s on private objects", %{conn: conn} do - note_activity = insert(:direct_note_activity) - object = Object.normalize(note_activity) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) - - conn - |> get("/objects/#{uuid}") - |> response(404) - end - - test "404s on non-existing objects", %{conn: conn} do - conn - |> get("/objects/123") - |> response(404) - end - end - - # Note: see ActivityPubControllerTest for JSON format tests - describe "GET /activities/:uuid (text/html)" do - setup %{conn: conn} do - conn = put_req_header(conn, "accept", "text/html") - %{conn: conn} - end - - test "redirects to /notice/id for html format", %{conn: conn} do - note_activity = insert(:note_activity) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) - - conn = get(conn, "/activities/#{uuid}") - assert redirected_to(conn) == "/notice/#{note_activity.id}" - end - - test "404s on private activities", %{conn: conn} do - note_activity = insert(:direct_note_activity) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) - - conn - |> get("/activities/#{uuid}") - |> response(404) - end - - test "404s on nonexistent activities", %{conn: conn} do - conn - |> get("/activities/123") - |> response(404) - end - end - - describe "GET notice/2" do - test "redirects to a proper object URL when json requested and the object is local", %{ - conn: conn - } do - note_activity = insert(:note_activity) - expected_redirect_url = Object.normalize(note_activity).data["id"] - - redirect_url = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/notice/#{note_activity.id}") - |> redirected_to() - - assert redirect_url == expected_redirect_url - end - - test "returns a 404 on remote notice when json requested", %{conn: conn} do - note_activity = insert(:note_activity, local: false) - - conn - |> put_req_header("accept", "application/activity+json") - |> get("/notice/#{note_activity.id}") - |> response(404) - end - - test "500s when actor not found", %{conn: conn} do - note_activity = insert(:note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - User.invalidate_cache(user) - Pleroma.Repo.delete(user) - - conn = - conn - |> get("/notice/#{note_activity.id}") - - assert response(conn, 500) == ~S({"error":"Something went wrong"}) - end - - test "render html for redirect for html format", %{conn: conn} do - note_activity = insert(:note_activity) - - resp = - conn - |> put_req_header("accept", "text/html") - |> get("/notice/#{note_activity.id}") - |> response(200) - - assert resp =~ - "<meta content=\"#{Pleroma.Web.base_url()}/notice/#{note_activity.id}\" property=\"og:url\">" - - user = insert(:user) - - {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id) - - assert like_activity.data["type"] == "Like" - - resp = - conn - |> put_req_header("accept", "text/html") - |> get("/notice/#{like_activity.id}") - |> response(200) - - assert resp =~ "<!--server-generated-meta-->" - end - - test "404s a private notice", %{conn: conn} do - note_activity = insert(:direct_note_activity) - url = "/notice/#{note_activity.id}" - - conn = - conn - |> get(url) - - assert response(conn, 404) - end - - test "404s a non-existing notice", %{conn: conn} do - url = "/notice/123" - - conn = - conn - |> get(url) - - assert response(conn, 404) - end - - test "it requires authentication if instance is NOT federating", %{ - conn: conn - } do - user = insert(:user) - note_activity = insert(:note_activity) - - conn = put_req_header(conn, "accept", "text/html") - - ensure_federating_or_authenticated(conn, "/notice/#{note_activity.id}", user) - end - end - - describe "GET /notice/:id/embed_player" do - setup do - note_activity = insert(:note_activity) - object = Pleroma.Object.normalize(note_activity) - - object_data = - Map.put(object.data, "attachment", [ - %{ - "url" => [ - %{ - "href" => - "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - } - ]) - - object - |> Ecto.Changeset.change(data: object_data) - |> Pleroma.Repo.update() - - %{note_activity: note_activity} - end - - test "renders embed player", %{conn: conn, note_activity: note_activity} do - conn = get(conn, "/notice/#{note_activity.id}/embed_player") - - assert Plug.Conn.get_resp_header(conn, "x-frame-options") == ["ALLOW"] - - assert Plug.Conn.get_resp_header( - conn, - "content-security-policy" - ) == [ - "default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;" - ] - - assert response(conn, 200) =~ - "<video controls loop><source src=\"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4\" type=\"video/mp4\">Your browser does not support video/mp4 playback.</video>" - end - - test "404s when activity isn't create", %{conn: conn} do - note_activity = insert(:note_activity, data_attrs: %{"type" => "Like"}) - - assert conn - |> get("/notice/#{note_activity.id}/embed_player") - |> response(404) - end - - test "404s when activity is direct message", %{conn: conn} do - note_activity = insert(:note_activity, data_attrs: %{"directMessage" => true}) - - assert conn - |> get("/notice/#{note_activity.id}/embed_player") - |> response(404) - end - - test "404s when attachment is empty", %{conn: conn} do - note_activity = insert(:note_activity) - object = Pleroma.Object.normalize(note_activity) - object_data = Map.put(object.data, "attachment", []) - - object - |> Ecto.Changeset.change(data: object_data) - |> Pleroma.Repo.update() - - assert conn - |> get("/notice/#{note_activity.id}/embed_player") - |> response(404) - end - - test "404s when attachment isn't audio or video", %{conn: conn} do - note_activity = insert(:note_activity) - object = Pleroma.Object.normalize(note_activity) - - object_data = - Map.put(object.data, "attachment", [ - %{ - "url" => [ - %{ - "href" => "https://peertube.moe/static/webseed/480.jpg", - "mediaType" => "image/jpg", - "type" => "Link" - } - ] - } - ]) - - object - |> Ecto.Changeset.change(data: object_data) - |> Pleroma.Repo.update() - - conn - |> get("/notice/#{note_activity.id}/embed_player") - |> response(404) - end - - test "it requires authentication if instance is NOT federating", %{ - conn: conn, - note_activity: note_activity - } do - user = insert(:user) - conn = put_req_header(conn, "accept", "text/html") - - ensure_federating_or_authenticated(conn, "/notice/#{note_activity.id}/embed_player", user) - end - end -end diff --git a/test/web/pleroma_api/controllers/account_controller_test.exs b/test/web/pleroma_api/controllers/account_controller_test.exs @@ -1,284 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Config - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - import Swoosh.TestAssertions - - describe "POST /api/v1/pleroma/accounts/confirmation_resend" do - setup do - {:ok, user} = - insert(:user) - |> User.confirmation_changeset(need_confirmation: true) - |> User.update_and_set_cache() - - assert user.confirmation_pending - - [user: user] - end - - setup do: clear_config([:instance, :account_activation_required], true) - - test "resend account confirmation email", %{conn: conn, user: user} do - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/accounts/confirmation_resend?email=#{user.email}") - |> json_response_and_validate_schema(:no_content) - - ObanHelpers.perform_all() - - email = Pleroma.Emails.UserEmail.account_confirmation_email(user) - notify_email = Config.get([:instance, :notify_email]) - instance_name = Config.get([:instance, :name]) - - assert_email_sent( - from: {instance_name, notify_email}, - to: {user.name, user.email}, - html_body: email.html_body - ) - end - - test "resend account confirmation email (with nickname)", %{conn: conn, user: user} do - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/accounts/confirmation_resend?nickname=#{user.nickname}") - |> json_response_and_validate_schema(:no_content) - - ObanHelpers.perform_all() - - email = Pleroma.Emails.UserEmail.account_confirmation_email(user) - notify_email = Config.get([:instance, :notify_email]) - instance_name = Config.get([:instance, :name]) - - assert_email_sent( - from: {instance_name, notify_email}, - to: {user.name, user.email}, - html_body: email.html_body - ) - end - end - - describe "getting favorites timeline of specified user" do - setup do - [current_user, user] = insert_pair(:user, hide_favorites: false) - %{user: current_user, conn: conn} = oauth_access(["read:favourites"], user: current_user) - [current_user: current_user, user: user, conn: conn] - end - - test "returns list of statuses favorited by specified user", %{ - conn: conn, - user: user - } do - [activity | _] = insert_pair(:note_activity) - CommonAPI.favorite(user, activity.id) - - response = - conn - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") - |> json_response_and_validate_schema(:ok) - - [like] = response - - assert length(response) == 1 - assert like["id"] == activity.id - end - - test "returns favorites for specified user_id when requester is not logged in", %{ - user: user - } do - activity = insert(:note_activity) - CommonAPI.favorite(user, activity.id) - - response = - build_conn() - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") - |> json_response_and_validate_schema(200) - - assert length(response) == 1 - end - - test "returns favorited DM only when user is logged in and he is one of recipients", %{ - current_user: current_user, - user: user - } do - {:ok, direct} = - CommonAPI.post(current_user, %{ - status: "Hi @#{user.nickname}!", - visibility: "direct" - }) - - CommonAPI.favorite(user, direct.id) - - for u <- [user, current_user] do - response = - build_conn() - |> assign(:user, u) - |> assign(:token, insert(:oauth_token, user: u, scopes: ["read:favourites"])) - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") - |> json_response_and_validate_schema(:ok) - - assert length(response) == 1 - end - - response = - build_conn() - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") - |> json_response_and_validate_schema(200) - - assert length(response) == 0 - end - - test "does not return others' favorited DM when user is not one of recipients", %{ - conn: conn, - user: user - } do - user_two = insert(:user) - - {:ok, direct} = - CommonAPI.post(user_two, %{ - status: "Hi @#{user.nickname}!", - visibility: "direct" - }) - - CommonAPI.favorite(user, direct.id) - - response = - conn - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response) - end - - test "paginates favorites using since_id and max_id", %{ - conn: conn, - user: user - } do - activities = insert_list(10, :note_activity) - - Enum.each(activities, fn activity -> - CommonAPI.favorite(user, activity.id) - end) - - third_activity = Enum.at(activities, 2) - seventh_activity = Enum.at(activities, 6) - - response = - conn - |> get( - "/api/v1/pleroma/accounts/#{user.id}/favourites?since_id=#{third_activity.id}&max_id=#{ - seventh_activity.id - }" - ) - |> json_response_and_validate_schema(:ok) - - assert length(response) == 3 - refute third_activity in response - refute seventh_activity in response - end - - test "limits favorites using limit parameter", %{ - conn: conn, - user: user - } do - 7 - |> insert_list(:note_activity) - |> Enum.each(fn activity -> - CommonAPI.favorite(user, activity.id) - end) - - response = - conn - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites?limit=3") - |> json_response_and_validate_schema(:ok) - - assert length(response) == 3 - end - - test "returns empty response when user does not have any favorited statuses", %{ - conn: conn, - user: user - } do - response = - conn - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response) - end - - test "returns 404 error when specified user is not exist", %{conn: conn} do - conn = get(conn, "/api/v1/pleroma/accounts/test/favourites") - - assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} - end - - test "returns 403 error when user has hidden own favorites", %{conn: conn} do - user = insert(:user, hide_favorites: true) - activity = insert(:note_activity) - CommonAPI.favorite(user, activity.id) - - conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites") - - assert json_response_and_validate_schema(conn, 403) == %{"error" => "Can't get favorites"} - end - - test "hides favorites for new users by default", %{conn: conn} do - user = insert(:user) - activity = insert(:note_activity) - CommonAPI.favorite(user, activity.id) - - assert user.hide_favorites - conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites") - - assert json_response_and_validate_schema(conn, 403) == %{"error" => "Can't get favorites"} - end - end - - describe "subscribing / unsubscribing" do - test "subscribing / unsubscribing to a user" do - %{user: user, conn: conn} = oauth_access(["follow"]) - subscription_target = insert(:user) - - ret_conn = - conn - |> assign(:user, user) - |> post("/api/v1/pleroma/accounts/#{subscription_target.id}/subscribe") - - assert %{"id" => _id, "subscribing" => true} = - json_response_and_validate_schema(ret_conn, 200) - - conn = post(conn, "/api/v1/pleroma/accounts/#{subscription_target.id}/unsubscribe") - - assert %{"id" => _id, "subscribing" => false} = json_response_and_validate_schema(conn, 200) - end - end - - describe "subscribing" do - test "returns 404 when subscription_target not found" do - %{conn: conn} = oauth_access(["write:follows"]) - - conn = post(conn, "/api/v1/pleroma/accounts/target_id/subscribe") - - assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404) - end - end - - describe "unsubscribing" do - test "returns 404 when subscription_target not found" do - %{conn: conn} = oauth_access(["follow"]) - - conn = post(conn, "/api/v1/pleroma/accounts/target_id/unsubscribe") - - assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404) - end - end -end diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -1,410 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Chat - alias Pleroma.Chat.MessageReference - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "POST /api/v1/pleroma/chats/:id/messages/:message_id/read" do - setup do: oauth_access(["write:chats"]) - - test "it marks one message as read", %{conn: conn, user: user} do - other_user = insert(:user) - - {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup") - {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2") - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - object = Object.normalize(create, false) - cm_ref = MessageReference.for_chat_and_object(chat, object) - - assert cm_ref.unread == true - - result = - conn - |> post("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}/read") - |> json_response_and_validate_schema(200) - - assert result["unread"] == false - - cm_ref = MessageReference.for_chat_and_object(chat, object) - - assert cm_ref.unread == false - end - end - - describe "POST /api/v1/pleroma/chats/:id/read" do - setup do: oauth_access(["write:chats"]) - - test "given a `last_read_id`, it marks everything until then as read", %{ - conn: conn, - user: user - } do - other_user = insert(:user) - - {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup") - {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2") - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - object = Object.normalize(create, false) - cm_ref = MessageReference.for_chat_and_object(chat, object) - - assert cm_ref.unread == true - - result = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/chats/#{chat.id}/read", %{"last_read_id" => cm_ref.id}) - |> json_response_and_validate_schema(200) - - assert result["unread"] == 1 - - cm_ref = MessageReference.for_chat_and_object(chat, object) - - assert cm_ref.unread == false - end - end - - describe "POST /api/v1/pleroma/chats/:id/messages" do - setup do: oauth_access(["write:chats"]) - - test "it posts a message to the chat", %{conn: conn, user: user} do - other_user = insert(:user) - - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - - result = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "Hallo!!"}) - |> json_response_and_validate_schema(200) - - assert result["content"] == "Hallo!!" - assert result["chat_id"] == chat.id |> to_string() - end - - test "it fails if there is no content", %{conn: conn, user: user} do - other_user = insert(:user) - - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - - result = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/chats/#{chat.id}/messages") - |> json_response_and_validate_schema(400) - - assert %{"error" => "no_content"} == result - end - - test "it works with an attachment", %{conn: conn, user: user} do - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) - - other_user = insert(:user) - - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - - result = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{ - "media_id" => to_string(upload.id) - }) - |> json_response_and_validate_schema(200) - - assert result["attachment"] - end - - test "gets MRF reason when rejected", %{conn: conn, user: user} do - clear_config([:mrf_keyword, :reject], ["GNO"]) - clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) - - other_user = insert(:user) - - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - - result = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "GNO/Linux"}) - |> json_response_and_validate_schema(422) - - assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} == result - end - end - - describe "DELETE /api/v1/pleroma/chats/:id/messages/:message_id" do - setup do: oauth_access(["write:chats"]) - - test "it deletes a message from the chat", %{conn: conn, user: user} do - recipient = insert(:user) - - {:ok, message} = - CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend") - - {:ok, other_message} = CommonAPI.post_chat_message(recipient, user, "nico nico ni") - - object = Object.normalize(message, false) - - chat = Chat.get(user.id, recipient.ap_id) - - cm_ref = MessageReference.for_chat_and_object(chat, object) - - # Deleting your own message removes the message and the reference - result = - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}") - |> json_response_and_validate_schema(200) - - assert result["id"] == cm_ref.id - refute MessageReference.get_by_id(cm_ref.id) - assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id) - - # Deleting other people's messages just removes the reference - object = Object.normalize(other_message, false) - cm_ref = MessageReference.for_chat_and_object(chat, object) - - result = - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}") - |> json_response_and_validate_schema(200) - - assert result["id"] == cm_ref.id - refute MessageReference.get_by_id(cm_ref.id) - assert Object.get_by_id(object.id) - end - end - - describe "GET /api/v1/pleroma/chats/:id/messages" do - setup do: oauth_access(["read:chats"]) - - test "it paginates", %{conn: conn, user: user} do - recipient = insert(:user) - - Enum.each(1..30, fn _ -> - {:ok, _} = CommonAPI.post_chat_message(user, recipient, "hey") - end) - - chat = Chat.get(user.id, recipient.ap_id) - - response = get(conn, "/api/v1/pleroma/chats/#{chat.id}/messages") - result = json_response_and_validate_schema(response, 200) - - [next, prev] = get_resp_header(response, "link") |> hd() |> String.split(", ") - api_endpoint = "/api/v1/pleroma/chats/" - - assert String.match?( - next, - ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&max_id=.*; rel=\"next\"$) - ) - - assert String.match?( - prev, - ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&min_id=.*; rel=\"prev\"$) - ) - - assert length(result) == 20 - - response = - get(conn, "/api/v1/pleroma/chats/#{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(", ") - - assert String.match?( - next, - ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&max_id=.*; rel=\"next\"$) - ) - - assert String.match?( - prev, - ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&max_id=.*&min_id=.*; rel=\"prev\"$) - ) - - assert length(result) == 10 - end - - test "it returns the messages for a given chat", %{conn: conn, user: user} do - other_user = insert(:user) - third_user = insert(:user) - - {:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey") - {:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey") - {:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?") - {:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?") - - chat = Chat.get(user.id, other_user.ap_id) - - result = - conn - |> get("/api/v1/pleroma/chats/#{chat.id}/messages") - |> json_response_and_validate_schema(200) - - result - |> Enum.each(fn message -> - assert message["chat_id"] == chat.id |> to_string() - end) - - assert length(result) == 3 - - # Trying to get the chat of a different user - conn - |> assign(:user, other_user) - |> get("/api/v1/pleroma/chats/#{chat.id}/messages") - |> json_response_and_validate_schema(404) - end - end - - describe "POST /api/v1/pleroma/chats/by-account-id/:id" do - setup do: oauth_access(["write:chats"]) - - test "it creates or returns a chat", %{conn: conn} do - other_user = insert(:user) - - result = - conn - |> post("/api/v1/pleroma/chats/by-account-id/#{other_user.id}") - |> json_response_and_validate_schema(200) - - assert result["id"] - end - end - - describe "GET /api/v1/pleroma/chats/:id" do - setup do: oauth_access(["read:chats"]) - - test "it returns a chat", %{conn: conn, user: user} do - other_user = insert(:user) - - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - - result = - conn - |> get("/api/v1/pleroma/chats/#{chat.id}") - |> json_response_and_validate_schema(200) - - assert result["id"] == to_string(chat.id) - end - end - - describe "GET /api/v1/pleroma/chats" do - setup do: oauth_access(["read:chats"]) - - test "it does not return chats with deleted users", %{conn: conn, user: user} do - recipient = insert(:user) - {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) - - Pleroma.Repo.delete(recipient) - User.invalidate_cache(recipient) - - result = - conn - |> get("/api/v1/pleroma/chats") - |> json_response_and_validate_schema(200) - - assert length(result) == 0 - end - - test "it does not return chats with users you blocked", %{conn: conn, user: user} do - recipient = insert(:user) - - {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) - - result = - conn - |> get("/api/v1/pleroma/chats") - |> json_response_and_validate_schema(200) - - assert length(result) == 1 - - User.block(user, recipient) - - result = - conn - |> get("/api/v1/pleroma/chats") - |> json_response_and_validate_schema(200) - - assert length(result) == 0 - end - - test "it returns all chats", %{conn: conn, user: user} do - Enum.each(1..30, fn _ -> - recipient = insert(:user) - {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) - end) - - result = - conn - |> get("/api/v1/pleroma/chats") - |> json_response_and_validate_schema(200) - - assert length(result) == 30 - end - - test "it return a list of chats the current user is participating in, in descending order of updates", - %{conn: conn, user: user} do - har = insert(:user) - jafnhar = insert(:user) - tridi = insert(:user) - - {:ok, chat_1} = Chat.get_or_create(user.id, har.ap_id) - :timer.sleep(1000) - {:ok, _chat_2} = Chat.get_or_create(user.id, jafnhar.ap_id) - :timer.sleep(1000) - {:ok, chat_3} = Chat.get_or_create(user.id, tridi.ap_id) - :timer.sleep(1000) - - # bump the second one - {:ok, chat_2} = Chat.bump_or_create(user.id, jafnhar.ap_id) - - result = - conn - |> get("/api/v1/pleroma/chats") - |> json_response_and_validate_schema(200) - - ids = Enum.map(result, & &1["id"]) - - assert ids == [ - chat_2.id |> to_string(), - chat_3.id |> to_string(), - chat_1.id |> to_string() - ] - end - - test "it is not affected by :restrict_unauthenticated setting (issue #1973)", %{ - conn: conn, - user: user - } do - clear_config([:restrict_unauthenticated, :profiles, :local], true) - clear_config([:restrict_unauthenticated, :profiles, :remote], true) - - user2 = insert(:user) - user3 = insert(:user, local: false) - - {:ok, _chat_12} = Chat.get_or_create(user.id, user2.ap_id) - {:ok, _chat_13} = Chat.get_or_create(user.id, user3.ap_id) - - result = - conn - |> get("/api/v1/pleroma/chats") - |> json_response_and_validate_schema(200) - - account_ids = Enum.map(result, &get_in(&1, ["account", "id"])) - assert Enum.sort(account_ids) == Enum.sort([user2.id, user3.id]) - end - end -end diff --git a/test/web/pleroma_api/controllers/conversation_controller_test.exs b/test/web/pleroma_api/controllers/conversation_controller_test.exs @@ -1,136 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.ConversationControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Conversation.Participation - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "/api/v1/pleroma/conversations/:id" do - user = insert(:user) - %{user: other_user, conn: conn} = oauth_access(["read:statuses"]) - - {:ok, _activity} = - CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"}) - - [participation] = Participation.for_user(other_user) - - result = - conn - |> get("/api/v1/pleroma/conversations/#{participation.id}") - |> json_response_and_validate_schema(200) - - assert result["id"] == participation.id |> to_string() - end - - test "/api/v1/pleroma/conversations/:id/statuses" do - user = insert(:user) - %{user: other_user, conn: conn} = oauth_access(["read:statuses"]) - third_user = insert(:user) - - {:ok, _activity} = - CommonAPI.post(user, %{status: "Hi @#{third_user.nickname}!", visibility: "direct"}) - - {:ok, activity} = - CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"}) - - [participation] = Participation.for_user(other_user) - - {:ok, activity_two} = - CommonAPI.post(other_user, %{ - status: "Hi!", - in_reply_to_status_id: activity.id, - in_reply_to_conversation_id: participation.id - }) - - result = - conn - |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses") - |> json_response_and_validate_schema(200) - - assert length(result) == 2 - - id_one = activity.id - id_two = activity_two.id - assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result - - {:ok, %{id: id_three}} = - CommonAPI.post(other_user, %{ - status: "Bye!", - in_reply_to_status_id: activity.id, - in_reply_to_conversation_id: participation.id - }) - - assert [%{"id" => ^id_two}, %{"id" => ^id_three}] = - conn - |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?limit=2") - |> json_response_and_validate_schema(:ok) - - assert [%{"id" => ^id_three}] = - conn - |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?min_id=#{id_two}") - |> json_response_and_validate_schema(:ok) - end - - test "PATCH /api/v1/pleroma/conversations/:id" do - %{user: user, conn: conn} = oauth_access(["write:conversations"]) - other_user = insert(:user) - - {:ok, _activity} = CommonAPI.post(user, %{status: "Hi", visibility: "direct"}) - - [participation] = Participation.for_user(user) - - participation = Repo.preload(participation, :recipients) - - user = User.get_cached_by_id(user.id) - assert [user] == participation.recipients - assert other_user not in participation.recipients - - query = "recipients[]=#{user.id}&recipients[]=#{other_user.id}" - - result = - conn - |> patch("/api/v1/pleroma/conversations/#{participation.id}?#{query}") - |> json_response_and_validate_schema(200) - - assert result["id"] == participation.id |> to_string - - [participation] = Participation.for_user(user) - participation = Repo.preload(participation, :recipients) - - assert user in participation.recipients - assert other_user in participation.recipients - end - - test "POST /api/v1/pleroma/conversations/read" do - user = insert(:user) - %{user: other_user, conn: conn} = oauth_access(["write:conversations"]) - - {:ok, _activity} = - CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"}) - - {:ok, _activity} = - CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"}) - - [participation2, participation1] = Participation.for_user(other_user) - assert Participation.get(participation2.id).read == false - assert Participation.get(participation1.id).read == false - assert User.get_cached_by_id(other_user.id).unread_conversation_count == 2 - - [%{"unread" => false}, %{"unread" => false}] = - conn - |> post("/api/v1/pleroma/conversations/read", %{}) - |> json_response_and_validate_schema(200) - - [participation2, participation1] = Participation.for_user(other_user) - assert Participation.get(participation2.id).read == true - assert Participation.get(participation1.id).read == true - assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0 - end -end diff --git a/test/web/pleroma_api/controllers/emoji_file_controller_test.exs b/test/web/pleroma_api/controllers/emoji_file_controller_test.exs @@ -1,357 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.EmojiFileControllerTest do - use Pleroma.Web.ConnCase - - import Tesla.Mock - import Pleroma.Factory - - @emoji_path Path.join( - Pleroma.Config.get!([:instance, :static_dir]), - "emoji" - ) - setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) - - setup do: clear_config([:instance, :public], true) - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - admin_conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - Pleroma.Emoji.reload() - {:ok, %{admin_conn: admin_conn}} - end - - describe "POST/PATCH/DELETE /api/pleroma/emoji/packs/files?name=:name" do - setup do - pack_file = "#{@emoji_path}/test_pack/pack.json" - original_content = File.read!(pack_file) - - on_exit(fn -> - File.write!(pack_file, original_content) - end) - - :ok - end - - test "upload zip file with emojies", %{admin_conn: admin_conn} do - on_exit(fn -> - [ - "128px/a_trusted_friend-128.png", - "auroraborealis.png", - "1000px/baby_in_a_box.png", - "1000px/bear.png", - "128px/bear-128.png" - ] - |> Enum.each(fn path -> File.rm_rf!("#{@emoji_path}/test_pack/#{path}") end) - end) - - resp = - admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ - file: %Plug.Upload{ - content_type: "application/zip", - filename: "emojis.zip", - path: Path.absname("test/fixtures/emojis.zip") - } - }) - |> json_response_and_validate_schema(200) - - assert resp == %{ - "a_trusted_friend-128" => "128px/a_trusted_friend-128.png", - "auroraborealis" => "auroraborealis.png", - "baby_in_a_box" => "1000px/baby_in_a_box.png", - "bear" => "1000px/bear.png", - "bear-128" => "128px/bear-128.png", - "blank" => "blank.png", - "blank2" => "blank2.png" - } - - Enum.each(Map.values(resp), fn path -> - assert File.exists?("#{@emoji_path}/test_pack/#{path}") - end) - end - - test "create shortcode exists", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(:conflict) == %{ - "error" => "An emoji with the \"blank\" shortcode already exists" - } - end - - test "don't rewrite old emoji", %{admin_conn: admin_conn} do - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir/") end) - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank3", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(200) == %{ - "blank" => "blank.png", - "blank2" => "blank2.png", - "blank3" => "dir/blank.png" - } - - assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank", - new_shortcode: "blank2", - new_filename: "dir_2/blank_3.png" - }) - |> json_response_and_validate_schema(:conflict) == %{ - "error" => - "New shortcode \"blank2\" is already used. If you want to override emoji use 'force' option" - } - end - - test "rewrite old emoji with force option", %{admin_conn: admin_conn} do - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir_2/") end) - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank3", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(200) == %{ - "blank" => "blank.png", - "blank2" => "blank2.png", - "blank3" => "dir/blank.png" - } - - assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank3", - new_shortcode: "blank4", - new_filename: "dir_2/blank_3.png", - force: true - }) - |> json_response_and_validate_schema(200) == %{ - "blank" => "blank.png", - "blank2" => "blank2.png", - "blank4" => "dir_2/blank_3.png" - } - - assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") - end - - test "with empty filename", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank2", - filename: "", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(422) == %{ - "error" => "pack name, shortcode or filename cannot be empty" - } - end - - test "add file with not loaded pack", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/files?name=not_loaded", %{ - shortcode: "blank3", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(:not_found) == %{ - "error" => "pack \"not_loaded\" is not found" - } - end - - test "remove file with not loaded pack", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/packs/files?name=not_loaded&shortcode=blank3") - |> json_response_and_validate_schema(:not_found) == %{ - "error" => "pack \"not_loaded\" is not found" - } - end - - test "remove file with empty shortcode", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/packs/files?name=not_loaded&shortcode=") - |> json_response_and_validate_schema(:not_found) == %{ - "error" => "pack \"not_loaded\" is not found" - } - end - - test "update file with not loaded pack", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/files?name=not_loaded", %{ - shortcode: "blank4", - new_shortcode: "blank3", - new_filename: "dir_2/blank_3.png" - }) - |> json_response_and_validate_schema(:not_found) == %{ - "error" => "pack \"not_loaded\" is not found" - } - end - - test "new with shortcode as file with update", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank4", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(200) == %{ - "blank" => "blank.png", - "blank4" => "dir/blank.png", - "blank2" => "blank2.png" - } - - assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank4", - new_shortcode: "blank3", - new_filename: "dir_2/blank_3.png" - }) - |> json_response_and_validate_schema(200) == %{ - "blank3" => "dir_2/blank_3.png", - "blank" => "blank.png", - "blank2" => "blank2.png" - } - - refute File.exists?("#{@emoji_path}/test_pack/dir/") - assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") - - assert admin_conn - |> delete("/api/pleroma/emoji/packs/files?name=test_pack&shortcode=blank3") - |> json_response_and_validate_schema(200) == %{ - "blank" => "blank.png", - "blank2" => "blank2.png" - } - - refute File.exists?("#{@emoji_path}/test_pack/dir_2/") - - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir") end) - end - - test "new with shortcode from url", %{admin_conn: admin_conn} do - mock(fn - %{ - method: :get, - url: "https://test-blank/blank_url.png" - } -> - text(File.read!("#{@emoji_path}/test_pack/blank.png")) - end) - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank_url", - file: "https://test-blank/blank_url.png" - }) - |> json_response_and_validate_schema(200) == %{ - "blank_url" => "blank_url.png", - "blank" => "blank.png", - "blank2" => "blank2.png" - } - - assert File.exists?("#{@emoji_path}/test_pack/blank_url.png") - - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/blank_url.png") end) - end - - test "new without shortcode", %{admin_conn: admin_conn} do - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/shortcode.png") end) - - 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_and_validate_schema(200) == %{ - "shortcode" => "shortcode.png", - "blank" => "blank.png", - "blank2" => "blank2.png" - } - end - - test "remove non existing shortcode in pack.json", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/packs/files?name=test_pack&shortcode=blank3") - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "Emoji \"blank3\" does not exist" - } - end - - test "update non existing emoji", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank3", - new_shortcode: "blank4", - new_filename: "dir_2/blank_3.png" - }) - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "Emoji \"blank3\" does not exist" - } - end - - test "update with empty shortcode", %{admin_conn: admin_conn} do - assert %{ - "error" => "Missing field: new_shortcode." - } = - 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_and_validate_schema(:bad_request) - end - end -end diff --git a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs @@ -1,604 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do - use Pleroma.Web.ConnCase - - import Tesla.Mock - import Pleroma.Factory - - @emoji_path Path.join( - Pleroma.Config.get!([:instance, :static_dir]), - "emoji" - ) - setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) - - setup do: clear_config([:instance, :public], true) - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - admin_conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - Pleroma.Emoji.reload() - {:ok, %{admin_conn: admin_conn}} - end - - test "GET /api/pleroma/emoji/packs when :public: false", %{conn: conn} do - Config.put([:instance, :public], false) - conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) - end - - test "GET /api/pleroma/emoji/packs", %{conn: conn} do - resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) - - assert resp["count"] == 4 - - assert resp["packs"] - |> Map.keys() - |> length() == 4 - - shared = resp["packs"]["test_pack"] - assert shared["files"] == %{"blank" => "blank.png", "blank2" => "blank2.png"} - assert Map.has_key?(shared["pack"], "download-sha256") - assert shared["pack"]["can-download"] - assert shared["pack"]["share-files"] - - non_shared = resp["packs"]["test_pack_nonshared"] - assert non_shared["pack"]["share-files"] == false - assert non_shared["pack"]["can-download"] == false - - resp = - conn - |> get("/api/pleroma/emoji/packs?page_size=1") - |> json_response_and_validate_schema(200) - - assert resp["count"] == 4 - - packs = Map.keys(resp["packs"]) - - assert length(packs) == 1 - - [pack1] = packs - - resp = - conn - |> get("/api/pleroma/emoji/packs?page_size=1&page=2") - |> json_response_and_validate_schema(200) - - assert resp["count"] == 4 - packs = Map.keys(resp["packs"]) - assert length(packs) == 1 - [pack2] = packs - - resp = - conn - |> get("/api/pleroma/emoji/packs?page_size=1&page=3") - |> json_response_and_validate_schema(200) - - assert resp["count"] == 4 - packs = Map.keys(resp["packs"]) - assert length(packs) == 1 - [pack3] = packs - - resp = - conn - |> get("/api/pleroma/emoji/packs?page_size=1&page=4") - |> json_response_and_validate_schema(200) - - assert resp["count"] == 4 - packs = Map.keys(resp["packs"]) - assert length(packs) == 1 - [pack4] = packs - assert [pack1, pack2, pack3, pack4] |> Enum.uniq() |> length() == 4 - end - - describe "GET /api/pleroma/emoji/packs/remote" do - test "shareable instance", %{admin_conn: admin_conn, conn: conn} do - resp = - conn - |> get("/api/pleroma/emoji/packs?page=2&page_size=1") - |> json_response_and_validate_schema(200) - - mock(fn - %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) - - %{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"} -> - json(resp) - end) - - assert admin_conn - |> get("/api/pleroma/emoji/packs/remote?url=https://example.com&page=2&page_size=1") - |> json_response_and_validate_schema(200) == resp - end - - test "non shareable instance", %{admin_conn: admin_conn} do - mock(fn - %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) - - %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> - json(%{metadata: %{features: []}}) - end) - - assert admin_conn - |> get("/api/pleroma/emoji/packs/remote?url=https://example.com") - |> json_response_and_validate_schema(500) == %{ - "error" => "The requested instance does not support sharing emoji packs" - } - end - end - - describe "GET /api/pleroma/emoji/packs/archive?name=:name" do - test "download shared pack", %{conn: conn} do - resp = - conn - |> get("/api/pleroma/emoji/packs/archive?name=test_pack") - |> response(200) - - {:ok, arch} = :zip.unzip(resp, [:memory]) - - assert Enum.find(arch, fn {n, _} -> n == 'pack.json' end) - assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end) - end - - test "non existing pack", %{conn: conn} do - assert conn - |> get("/api/pleroma/emoji/packs/archive?name=test_pack_for_import") - |> json_response_and_validate_schema(:not_found) == %{ - "error" => "Pack test_pack_for_import does not exist" - } - end - - test "non downloadable pack", %{conn: conn} do - assert conn - |> get("/api/pleroma/emoji/packs/archive?name=test_pack_nonshared") - |> json_response_and_validate_schema(:forbidden) == %{ - "error" => - "Pack test_pack_nonshared cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing" - } - end - end - - describe "POST /api/pleroma/emoji/packs/download" do - test "shared pack from remote and non shared from fallback-src", %{ - admin_conn: admin_conn, - conn: conn - } do - mock(fn - %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) - - %{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/pack?name=test_pack" - } -> - conn - |> get("/api/pleroma/emoji/pack?name=test_pack") - |> json_response_and_validate_schema(200) - |> json() - - %{ - method: :get, - url: "https://example.com/api/pleroma/emoji/packs/archive?name=test_pack" - } -> - conn - |> get("/api/pleroma/emoji/packs/archive?name=test_pack") - |> response(200) - |> text() - - %{ - method: :get, - url: "https://example.com/api/pleroma/emoji/pack?name=test_pack_nonshared" - } -> - conn - |> get("/api/pleroma/emoji/pack?name=test_pack_nonshared") - |> json_response_and_validate_schema(200) - |> json() - - %{ - method: :get, - url: "https://nonshared-pack" - } -> - text(File.read!("#{@emoji_path}/test_pack_nonshared/nonshared.zip")) - end) - - assert admin_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_and_validate_schema(200) == "ok" - - assert File.exists?("#{@emoji_path}/test_pack2/pack.json") - assert File.exists?("#{@emoji_path}/test_pack2/blank.png") - - assert admin_conn - |> delete("/api/pleroma/emoji/pack?name=test_pack2") - |> json_response_and_validate_schema(200) == "ok" - - refute File.exists?("#{@emoji_path}/test_pack2") - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post( - "/api/pleroma/emoji/packs/download", - %{ - url: "https://example.com", - name: "test_pack_nonshared", - as: "test_pack_nonshared2" - } - ) - |> json_response_and_validate_schema(200) == "ok" - - assert File.exists?("#{@emoji_path}/test_pack_nonshared2/pack.json") - assert File.exists?("#{@emoji_path}/test_pack_nonshared2/blank.png") - - assert admin_conn - |> delete("/api/pleroma/emoji/pack?name=test_pack_nonshared2") - |> json_response_and_validate_schema(200) == "ok" - - refute File.exists?("#{@emoji_path}/test_pack_nonshared2") - end - - test "nonshareable instance", %{admin_conn: admin_conn} do - mock(fn - %{method: :get, url: "https://old-instance/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://old-instance/nodeinfo/2.1.json"}]}) - - %{method: :get, url: "https://old-instance/nodeinfo/2.1.json"} -> - json(%{metadata: %{features: []}}) - end) - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post( - "/api/pleroma/emoji/packs/download", - %{ - url: "https://old-instance", - name: "test_pack", - as: "test_pack2" - } - ) - |> json_response_and_validate_schema(500) == %{ - "error" => "The requested instance does not support sharing emoji packs" - } - end - - test "checksum fail", %{admin_conn: admin_conn} do - mock(fn - %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) - - %{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/pack?name=pack_bad_sha" - } -> - {: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" - } -> - %Tesla.Env{ - status: 200, - body: File.read!("test/instance_static/emoji/pack_bad_sha/pack_bad_sha.zip") - } - end) - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/download", %{ - url: "https://example.com", - name: "pack_bad_sha", - as: "pack_bad_sha2" - }) - |> json_response_and_validate_schema(:internal_server_error) == %{ - "error" => "SHA256 for the pack doesn't match the one sent by the server" - } - end - - test "other error", %{admin_conn: admin_conn} do - mock(fn - %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) - - %{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/pack?name=test_pack" - } -> - {:ok, pack} = Pleroma.Emoji.Pack.load_pack("test_pack") - %Tesla.Env{status: 200, body: Jason.encode!(pack)} - end) - - assert admin_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_and_validate_schema(:internal_server_error) == %{ - "error" => - "The pack was not set as shared and there is no fallback src to download from" - } - end - end - - describe "PATCH /api/pleroma/emoji/pack?name=:name" do - setup do - pack_file = "#{@emoji_path}/test_pack/pack.json" - original_content = File.read!(pack_file) - - on_exit(fn -> - File.write!(pack_file, original_content) - end) - - {:ok, - pack_file: pack_file, - new_data: %{ - "license" => "Test license changed", - "homepage" => "https://pleroma.social", - "description" => "Test description", - "share-files" => false - }} - end - - test "for a pack without a fallback source", ctx do - assert ctx[:admin_conn] - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/pack?name=test_pack", %{ - "metadata" => ctx[:new_data] - }) - |> json_response_and_validate_schema(200) == ctx[:new_data] - - assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == ctx[:new_data] - end - - test "for a pack with a fallback source", ctx do - mock(fn - %{ - method: :get, - url: "https://nonshared-pack" - } -> - text(File.read!("#{@emoji_path}/test_pack_nonshared/nonshared.zip")) - end) - - new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack") - - new_data_with_sha = - Map.put( - new_data, - "fallback-src-sha256", - "1967BB4E42BCC34BCC12D57BE7811D3B7BE52F965BCE45C87BD377B9499CE11D" - ) - - assert ctx[:admin_conn] - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/pack?name=test_pack", %{metadata: new_data}) - |> json_response_and_validate_schema(200) == new_data_with_sha - - assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == new_data_with_sha - end - - test "when the fallback source doesn't have all the files", ctx do - mock(fn - %{ - method: :get, - url: "https://nonshared-pack" - } -> - {:ok, {'empty.zip', empty_arch}} = :zip.zip('empty.zip', [], [:memory]) - text(empty_arch) - end) - - new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack") - - assert ctx[:admin_conn] - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/pack?name=test_pack", %{metadata: new_data}) - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "The fallback archive does not have all files specified in pack.json" - } - end - end - - describe "POST/DELETE /api/pleroma/emoji/pack?name=:name" do - test "creating and deleting a pack", %{admin_conn: admin_conn} do - assert admin_conn - |> post("/api/pleroma/emoji/pack?name=test_created") - |> json_response_and_validate_schema(200) == "ok" - - assert File.exists?("#{@emoji_path}/test_created/pack.json") - - assert Jason.decode!(File.read!("#{@emoji_path}/test_created/pack.json")) == %{ - "pack" => %{}, - "files" => %{}, - "files_count" => 0 - } - - assert admin_conn - |> delete("/api/pleroma/emoji/pack?name=test_created") - |> json_response_and_validate_schema(200) == "ok" - - refute File.exists?("#{@emoji_path}/test_created/pack.json") - end - - test "if pack exists", %{admin_conn: admin_conn} do - path = Path.join(@emoji_path, "test_created") - File.mkdir(path) - pack_file = Jason.encode!(%{files: %{}, pack: %{}}) - File.write!(Path.join(path, "pack.json"), pack_file) - - assert admin_conn - |> post("/api/pleroma/emoji/pack?name=test_created") - |> json_response_and_validate_schema(:conflict) == %{ - "error" => "A pack named \"test_created\" already exists" - } - - on_exit(fn -> File.rm_rf(path) end) - end - - test "with empty name", %{admin_conn: admin_conn} do - assert admin_conn - |> post("/api/pleroma/emoji/pack?name= ") - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "pack name cannot be empty" - } - end - end - - test "deleting nonexisting pack", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/pack?name=non_existing") - |> json_response_and_validate_schema(:not_found) == %{ - "error" => "Pack non_existing does not exist" - } - end - - test "deleting with empty name", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/pack?name= ") - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "pack name cannot be empty" - } - end - - test "filesystem import", %{admin_conn: admin_conn, conn: conn} do - on_exit(fn -> - File.rm!("#{@emoji_path}/test_pack_for_import/emoji.txt") - File.rm!("#{@emoji_path}/test_pack_for_import/pack.json") - end) - - resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) - - refute Map.has_key?(resp["packs"], "test_pack_for_import") - - assert admin_conn - |> get("/api/pleroma/emoji/packs/import") - |> json_response_and_validate_schema(200) == ["test_pack_for_import"] - - resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) - assert resp["packs"]["test_pack_for_import"]["files"] == %{"blank" => "blank.png"} - - File.rm!("#{@emoji_path}/test_pack_for_import/pack.json") - refute File.exists?("#{@emoji_path}/test_pack_for_import/pack.json") - - emoji_txt_content = """ - blank, blank.png, Fun - blank2, blank.png - foo, /emoji/test_pack_for_import/blank.png - bar - """ - - File.write!("#{@emoji_path}/test_pack_for_import/emoji.txt", emoji_txt_content) - - assert admin_conn - |> get("/api/pleroma/emoji/packs/import") - |> json_response_and_validate_schema(200) == ["test_pack_for_import"] - - resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) - - assert resp["packs"]["test_pack_for_import"]["files"] == %{ - "blank" => "blank.png", - "blank2" => "blank.png", - "foo" => "blank.png" - } - end - - describe "GET /api/pleroma/emoji/pack?name=:name" do - test "shows pack.json", %{conn: conn} do - assert %{ - "files" => files, - "files_count" => 2, - "pack" => %{ - "can-download" => true, - "description" => "Test description", - "download-sha256" => _, - "homepage" => "https://pleroma.social", - "license" => "Test license", - "share-files" => true - } - } = - conn - |> get("/api/pleroma/emoji/pack?name=test_pack") - |> json_response_and_validate_schema(200) - - assert files == %{"blank" => "blank.png", "blank2" => "blank2.png"} - - assert %{ - "files" => files, - "files_count" => 2 - } = - conn - |> get("/api/pleroma/emoji/pack?name=test_pack&page_size=1") - |> json_response_and_validate_schema(200) - - assert files |> Map.keys() |> length() == 1 - - assert %{ - "files" => files, - "files_count" => 2 - } = - conn - |> get("/api/pleroma/emoji/pack?name=test_pack&page_size=1&page=2") - |> json_response_and_validate_schema(200) - - assert files |> Map.keys() |> length() == 1 - end - - test "for pack name with special chars", %{conn: conn} do - assert %{ - "files" => files, - "files_count" => 1, - "pack" => %{ - "can-download" => true, - "description" => "Test description", - "download-sha256" => _, - "homepage" => "https://pleroma.social", - "license" => "Test license", - "share-files" => true - } - } = - conn - |> get("/api/pleroma/emoji/pack?name=blobs.gg") - |> json_response_and_validate_schema(200) - end - - test "non existing pack", %{conn: conn} do - assert conn - |> get("/api/pleroma/emoji/pack?name=non_existing") - |> json_response_and_validate_schema(:not_found) == %{ - "error" => "Pack non_existing does not exist" - } - end - - test "error name", %{conn: conn} do - assert conn - |> get("/api/pleroma/emoji/pack?name= ") - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "pack name cannot be empty" - } - end - end -end diff --git a/test/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/web/pleroma_api/controllers/emoji_reaction_controller_test.exs @@ -1,149 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do - use Oban.Testing, repo: Pleroma.Repo - use Pleroma.Web.ConnCase - - alias Pleroma.Object - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - - 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/☕") - |> 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} - ] - - # Reacting with a non-emoji - assert conn - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) - |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/x") - |> json_response_and_validate_schema(400) - end - - test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") - - ObanHelpers.perform_all() - - 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/☕") - - 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 - end - - test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - doomed_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - - result = - conn - |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") - |> json_response_and_validate_schema(200) - - assert result == [] - - {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") - {:ok, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅") - - User.perform(:delete, doomed_user) - - result = - conn - |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") - |> json_response_and_validate_schema(200) - - [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result - - assert represented_user["id"] == other_user.id - - result = - conn - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:statuses"])) - |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") - |> json_response_and_validate_schema(200) - - assert [%{"name" => "🎅", "count" => 1, "accounts" => [_represented_user], "me" => true}] = - result - end - - test "GET /api/v1/pleroma/statuses/:id/reactions with :show_reactions disabled", %{conn: conn} do - clear_config([:instance, :show_reactions], false) - - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") - - result = - conn - |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") - |> json_response_and_validate_schema(200) - - assert result == [] - end - - test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - - result = - conn - |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅") - |> json_response_and_validate_schema(200) - - assert result == [] - - {: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}] = - conn - |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅") - |> json_response_and_validate_schema(200) - - assert represented_user["id"] == other_user.id - end -end diff --git a/test/web/pleroma_api/controllers/mascot_controller_test.exs b/test/web/pleroma_api/controllers/mascot_controller_test.exs @@ -1,73 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.MascotControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.User - - test "mascot upload" do - %{conn: conn} = oauth_access(["write:accounts"]) - - non_image_file = %Plug.Upload{ - content_type: "audio/mpeg", - path: Path.absname("test/fixtures/sound.mp3"), - filename: "sound.mp3" - } - - ret_conn = - conn - |> put_req_header("content-type", "multipart/form-data") - |> put("/api/v1/pleroma/mascot", %{"file" => non_image_file}) - - assert json_response_and_validate_schema(ret_conn, 415) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - conn = - conn - |> put_req_header("content-type", "multipart/form-data") - |> put("/api/v1/pleroma/mascot", %{"file" => file}) - - assert %{"id" => _, "type" => image} = json_response_and_validate_schema(conn, 200) - end - - test "mascot retrieving" do - %{user: user, conn: conn} = oauth_access(["read:accounts", "write:accounts"]) - - # When user hasn't set a mascot, we should just get pleroma tan back - ret_conn = get(conn, "/api/v1/pleroma/mascot") - - assert %{"url" => url} = json_response_and_validate_schema(ret_conn, 200) - assert url =~ "pleroma-fox-tan-smol" - - # When a user sets their mascot, we should get that back - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - ret_conn = - conn - |> put_req_header("content-type", "multipart/form-data") - |> put("/api/v1/pleroma/mascot", %{"file" => file}) - - assert json_response_and_validate_schema(ret_conn, 200) - - user = User.get_cached_by_id(user.id) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/pleroma/mascot") - - assert %{"url" => url, "type" => "image"} = json_response_and_validate_schema(conn, 200) - assert url =~ "an_image" - end -end diff --git a/test/web/pleroma_api/controllers/notification_controller_test.exs b/test/web/pleroma_api/controllers/notification_controller_test.exs @@ -1,68 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.NotificationControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Notification - alias Pleroma.Repo - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "POST /api/v1/pleroma/notifications/read" do - setup do: oauth_access(["write:notifications"]) - - test "it marks a single notification as read", %{user: user1, conn: conn} do - user2 = insert(:user) - {:ok, activity1} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"}) - {:ok, activity2} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"}) - {: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) - - assert %{"pleroma" => %{"is_seen" => true}} = response - assert Repo.get(Notification, notification1.id).seen - refute Repo.get(Notification, notification2.id).seen - end - - test "it marks multiple notifications as read", %{user: user1, conn: conn} do - user2 = insert(:user) - {:ok, _activity1} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"}) - {:ok, _activity2} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"}) - {:ok, _activity3} = CommonAPI.post(user2, %{status: "HIE @#{user1.nickname}"}) - - [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) - - 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 - end - - test "it returns error when notification not found", %{conn: conn} do - response = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/notifications/read", %{ - id: 22_222_222_222_222 - }) - |> json_response_and_validate_schema(:bad_request) - - assert response == %{"error" => "Cannot get notification"} - end - end -end diff --git a/test/web/pleroma_api/controllers/scrobble_controller_test.exs b/test/web/pleroma_api/controllers/scrobble_controller_test.exs @@ -1,60 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Web.CommonAPI - - describe "POST /api/v1/pleroma/scrobble" do - test "works correctly" do - %{conn: conn} = oauth_access(["write"]) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/scrobble", %{ - "title" => "lain radio episode 1", - "artist" => "lain", - "album" => "lain radio", - "length" => "180000" - }) - - assert %{"title" => "lain radio episode 1"} = json_response_and_validate_schema(conn, 200) - end - end - - describe "GET /api/v1/pleroma/accounts/:id/scrobbles" do - test "works correctly" do - %{user: user, conn: conn} = oauth_access(["read"]) - - {:ok, _activity} = - CommonAPI.listen(user, %{ - title: "lain radio episode 1", - artist: "lain", - album: "lain radio" - }) - - {:ok, _activity} = - CommonAPI.listen(user, %{ - title: "lain radio episode 2", - artist: "lain", - album: "lain radio" - }) - - {:ok, _activity} = - CommonAPI.listen(user, %{ - title: "lain radio episode 3", - artist: "lain", - album: "lain radio" - }) - - conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/scrobbles") - - result = json_response_and_validate_schema(conn, 200) - - assert length(result) == 3 - end - end -end diff --git a/test/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs b/test/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs @@ -1,264 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - alias Pleroma.MFA.Settings - alias Pleroma.MFA.TOTP - - describe "GET /api/pleroma/accounts/mfa/settings" do - test "returns user mfa settings for new user", %{conn: conn} do - token = insert(:oauth_token, scopes: ["read", "follow"]) - token2 = insert(:oauth_token, scopes: ["write"]) - - assert conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> get("/api/pleroma/accounts/mfa") - |> json_response(:ok) == %{ - "settings" => %{"enabled" => false, "totp" => false} - } - - assert conn - |> put_req_header("authorization", "Bearer #{token2.token}") - |> get("/api/pleroma/accounts/mfa") - |> json_response(403) == %{ - "error" => "Insufficient permissions: read:security." - } - end - - test "returns user mfa settings with enabled totp", %{conn: conn} do - user = - insert(:user, - multi_factor_authentication_settings: %Settings{ - enabled: true, - totp: %Settings.TOTP{secret: "XXX", delivery_type: "app", confirmed: true} - } - ) - - token = insert(:oauth_token, scopes: ["read", "follow"], user: user) - - assert conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> get("/api/pleroma/accounts/mfa") - |> json_response(:ok) == %{ - "settings" => %{"enabled" => true, "totp" => true} - } - end - end - - describe "GET /api/pleroma/accounts/mfa/backup_codes" do - test "returns backup codes", %{conn: conn} do - user = - insert(:user, - multi_factor_authentication_settings: %Settings{ - backup_codes: ["1", "2", "3"], - totp: %Settings.TOTP{secret: "secret"} - } - ) - - token = insert(:oauth_token, scopes: ["write", "follow"], user: user) - token2 = insert(:oauth_token, scopes: ["read"]) - - response = - conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> get("/api/pleroma/accounts/mfa/backup_codes") - |> json_response(:ok) - - assert [<<_::bytes-size(6)>>, <<_::bytes-size(6)>>] = response["codes"] - user = refresh_record(user) - mfa_settings = user.multi_factor_authentication_settings - assert mfa_settings.totp.secret == "secret" - refute mfa_settings.backup_codes == ["1", "2", "3"] - refute mfa_settings.backup_codes == [] - - assert conn - |> put_req_header("authorization", "Bearer #{token2.token}") - |> get("/api/pleroma/accounts/mfa/backup_codes") - |> json_response(403) == %{ - "error" => "Insufficient permissions: write:security." - } - end - end - - describe "GET /api/pleroma/accounts/mfa/setup/totp" do - test "return errors when method is invalid", %{conn: conn} do - user = insert(:user) - token = insert(:oauth_token, scopes: ["write", "follow"], user: user) - - response = - conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> get("/api/pleroma/accounts/mfa/setup/torf") - |> json_response(400) - - assert response == %{"error" => "undefined method"} - end - - test "returns key and provisioning_uri", %{conn: conn} do - user = - insert(:user, - multi_factor_authentication_settings: %Settings{backup_codes: ["1", "2", "3"]} - ) - - token = insert(:oauth_token, scopes: ["write", "follow"], user: user) - token2 = insert(:oauth_token, scopes: ["read"]) - - response = - conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> get("/api/pleroma/accounts/mfa/setup/totp") - |> json_response(:ok) - - user = refresh_record(user) - mfa_settings = user.multi_factor_authentication_settings - secret = mfa_settings.totp.secret - refute mfa_settings.enabled - assert mfa_settings.backup_codes == ["1", "2", "3"] - - assert response == %{ - "key" => secret, - "provisioning_uri" => TOTP.provisioning_uri(secret, "#{user.email}") - } - - assert conn - |> put_req_header("authorization", "Bearer #{token2.token}") - |> get("/api/pleroma/accounts/mfa/setup/totp") - |> json_response(403) == %{ - "error" => "Insufficient permissions: write:security." - } - end - end - - describe "GET /api/pleroma/accounts/mfa/confirm/totp" do - test "returns success result", %{conn: conn} do - secret = TOTP.generate_secret() - code = TOTP.generate_token(secret) - - user = - insert(:user, - multi_factor_authentication_settings: %Settings{ - backup_codes: ["1", "2", "3"], - totp: %Settings.TOTP{secret: secret} - } - ) - - token = insert(:oauth_token, scopes: ["write", "follow"], user: user) - token2 = insert(:oauth_token, scopes: ["read"]) - - assert conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: code}) - |> json_response(:ok) - - settings = refresh_record(user).multi_factor_authentication_settings - assert settings.enabled - assert settings.totp.secret == secret - assert settings.totp.confirmed - assert settings.backup_codes == ["1", "2", "3"] - - assert conn - |> put_req_header("authorization", "Bearer #{token2.token}") - |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: code}) - |> json_response(403) == %{ - "error" => "Insufficient permissions: write:security." - } - end - - test "returns error if password incorrect", %{conn: conn} do - secret = TOTP.generate_secret() - code = TOTP.generate_token(secret) - - user = - insert(:user, - multi_factor_authentication_settings: %Settings{ - backup_codes: ["1", "2", "3"], - totp: %Settings.TOTP{secret: secret} - } - ) - - token = insert(:oauth_token, scopes: ["write", "follow"], user: user) - - response = - conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "xxx", code: code}) - |> json_response(422) - - settings = refresh_record(user).multi_factor_authentication_settings - refute settings.enabled - refute settings.totp.confirmed - assert settings.backup_codes == ["1", "2", "3"] - assert response == %{"error" => "Invalid password."} - end - - test "returns error if code incorrect", %{conn: conn} do - secret = TOTP.generate_secret() - - user = - insert(:user, - multi_factor_authentication_settings: %Settings{ - backup_codes: ["1", "2", "3"], - totp: %Settings.TOTP{secret: secret} - } - ) - - token = insert(:oauth_token, scopes: ["write", "follow"], user: user) - token2 = insert(:oauth_token, scopes: ["read"]) - - response = - conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: "code"}) - |> json_response(422) - - settings = refresh_record(user).multi_factor_authentication_settings - refute settings.enabled - refute settings.totp.confirmed - assert settings.backup_codes == ["1", "2", "3"] - assert response == %{"error" => "invalid_token"} - - assert conn - |> put_req_header("authorization", "Bearer #{token2.token}") - |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: "code"}) - |> json_response(403) == %{ - "error" => "Insufficient permissions: write:security." - } - end - end - - describe "DELETE /api/pleroma/accounts/mfa/totp" do - test "returns success result", %{conn: conn} do - user = - insert(:user, - multi_factor_authentication_settings: %Settings{ - backup_codes: ["1", "2", "3"], - totp: %Settings.TOTP{secret: "secret"} - } - ) - - token = insert(:oauth_token, scopes: ["write", "follow"], user: user) - token2 = insert(:oauth_token, scopes: ["read"]) - - assert conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> delete("/api/pleroma/accounts/mfa/totp", %{password: "test"}) - |> json_response(:ok) - - settings = refresh_record(user).multi_factor_authentication_settings - refute settings.enabled - assert settings.totp.secret == nil - refute settings.totp.confirmed - - assert conn - |> put_req_header("authorization", "Bearer #{token2.token}") - |> delete("/api/pleroma/accounts/mfa/totp", %{password: "test"}) - |> json_response(403) == %{ - "error" => "Insufficient permissions: write:security." - } - end - end -end diff --git a/test/web/pleroma_api/controllers/user_import_controller_test.exs b/test/web/pleroma_api/controllers/user_import_controller_test.exs @@ -1,235 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do - use Pleroma.Web.ConnCase - use Oban.Testing, repo: Pleroma.Repo - - alias Pleroma.Config - alias Pleroma.Tests.ObanHelpers - - import Pleroma.Factory - import Mock - - setup do - Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe "POST /api/pleroma/follow_import" do - setup do: oauth_access(["follow"]) - - test "it returns HTTP 200", %{conn: conn} do - user2 = insert(:user) - - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/follow_import", %{"list" => "#{user2.ap_id}"}) - |> json_response_and_validate_schema(200) - end - - test "it imports follow lists from file", %{conn: conn} do - user2 = insert(:user) - - with_mocks([ - {File, [], - read!: fn "follow_list.txt" -> - "Account address,Show boosts\n#{user2.ap_id},true" - end} - ]) do - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/follow_import", %{ - "list" => %Plug.Upload{path: "follow_list.txt"} - }) - |> json_response_and_validate_schema(200) - - assert [{:ok, job_result}] = ObanHelpers.perform_all() - assert job_result == [user2] - end - end - - test "it imports new-style mastodon follow lists", %{conn: conn} do - user2 = insert(:user) - - response = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/follow_import", %{ - "list" => "Account address,Show boosts\n#{user2.ap_id},true" - }) - |> json_response_and_validate_schema(200) - - assert response == "job started" - end - - test "requires 'follow' or 'write:follows' permissions" do - token1 = insert(:oauth_token, scopes: ["read", "write"]) - token2 = insert(:oauth_token, scopes: ["follow"]) - token3 = insert(:oauth_token, scopes: ["something"]) - another_user = insert(:user) - - for token <- [token1, token2, token3] do - conn = - build_conn() - |> put_req_header("authorization", "Bearer #{token.token}") - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/follow_import", %{"list" => "#{another_user.ap_id}"}) - - if token == token3 do - assert %{"error" => "Insufficient permissions: follow | write:follows."} == - json_response(conn, 403) - else - assert json_response(conn, 200) - end - end - end - - test "it imports follows with different nickname variations", %{conn: conn} do - users = [user2, user3, user4, user5, user6] = insert_list(5, :user) - - identifiers = - [ - user2.ap_id, - user3.nickname, - " ", - "@" <> user4.nickname, - user5.nickname <> "@localhost", - "@" <> user6.nickname <> "@localhost" - ] - |> Enum.join("\n") - - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/follow_import", %{"list" => identifiers}) - |> json_response_and_validate_schema(200) - - assert [{:ok, job_result}] = ObanHelpers.perform_all() - assert job_result == users - end - end - - describe "POST /api/pleroma/blocks_import" do - # Note: "follow" or "write:blocks" permission is required - setup do: oauth_access(["write:blocks"]) - - test "it returns HTTP 200", %{conn: conn} do - user2 = insert(:user) - - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/blocks_import", %{"list" => "#{user2.ap_id}"}) - |> json_response_and_validate_schema(200) - end - - test "it imports blocks users from file", %{conn: conn} do - users = [user2, user3] = insert_list(2, :user) - - with_mocks([ - {File, [], read!: fn "blocks_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end} - ]) do - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/blocks_import", %{ - "list" => %Plug.Upload{path: "blocks_list.txt"} - }) - |> json_response_and_validate_schema(200) - - assert [{:ok, job_result}] = ObanHelpers.perform_all() - assert job_result == users - end - end - - test "it imports blocks with different nickname variations", %{conn: conn} do - users = [user2, user3, user4, user5, user6] = insert_list(5, :user) - - identifiers = - [ - user2.ap_id, - user3.nickname, - "@" <> user4.nickname, - user5.nickname <> "@localhost", - "@" <> user6.nickname <> "@localhost" - ] - |> Enum.join(" ") - - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/blocks_import", %{"list" => identifiers}) - |> json_response_and_validate_schema(200) - - assert [{:ok, job_result}] = ObanHelpers.perform_all() - assert job_result == users - end - end - - describe "POST /api/pleroma/mutes_import" do - # Note: "follow" or "write:mutes" permission is required - setup do: oauth_access(["write:mutes"]) - - test "it returns HTTP 200", %{user: user, conn: conn} do - user2 = insert(:user) - - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/mutes_import", %{"list" => "#{user2.ap_id}"}) - |> json_response_and_validate_schema(200) - - assert [{:ok, job_result}] = ObanHelpers.perform_all() - assert job_result == [user2] - assert Pleroma.User.mutes?(user, user2) - end - - test "it imports mutes users from file", %{user: user, conn: conn} do - users = [user2, user3] = insert_list(2, :user) - - with_mocks([ - {File, [], read!: fn "mutes_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end} - ]) do - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/mutes_import", %{ - "list" => %Plug.Upload{path: "mutes_list.txt"} - }) - |> json_response_and_validate_schema(200) - - assert [{:ok, job_result}] = ObanHelpers.perform_all() - assert job_result == users - assert Enum.all?(users, &Pleroma.User.mutes?(user, &1)) - end - end - - test "it imports mutes with different nickname variations", %{user: user, conn: conn} do - users = [user2, user3, user4, user5, user6] = insert_list(5, :user) - - identifiers = - [ - user2.ap_id, - user3.nickname, - "@" <> user4.nickname, - user5.nickname <> "@localhost", - "@" <> user6.nickname <> "@localhost" - ] - |> Enum.join(" ") - - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/mutes_import", %{"list" => identifiers}) - |> json_response_and_validate_schema(200) - - assert [{:ok, job_result}] = ObanHelpers.perform_all() - assert job_result == users - assert Enum.all?(users, &Pleroma.User.mutes?(user, &1)) - end - end -end diff --git a/test/web/pleroma_api/views/chat/message_reference_view_test.exs b/test/web/pleroma_api/views/chat/message_reference_view_test.exs @@ -1,72 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceViewTest do - use Pleroma.DataCase - - alias Pleroma.Chat - alias Pleroma.Chat.MessageReference - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView - - import Pleroma.Factory - - test "it displays a chat message" do - user = insert(:user) - recipient = insert(:user) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) - {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis :firefox:") - - chat = Chat.get(user.id, recipient.ap_id) - - object = Object.normalize(activity) - - cm_ref = MessageReference.for_chat_and_object(chat, object) - - chat_message = MessageReferenceView.render("show.json", chat_message_reference: cm_ref) - - assert chat_message[:id] == cm_ref.id - assert chat_message[:content] == "kippis :firefox:" - assert chat_message[:account_id] == user.id - assert chat_message[:chat_id] - assert chat_message[:created_at] - assert chat_message[:unread] == false - assert match?([%{shortcode: "firefox"}], chat_message[:emojis]) - - clear_config([:rich_media, :enabled], true) - - Tesla.Mock.mock(fn - %{url: "https://example.com/ogp"} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")} - end) - - {:ok, activity} = - CommonAPI.post_chat_message(recipient, user, "gkgkgk https://example.com/ogp", - media_id: upload.id - ) - - object = Object.normalize(activity) - - cm_ref = MessageReference.for_chat_and_object(chat, object) - - chat_message_two = MessageReferenceView.render("show.json", chat_message_reference: cm_ref) - - assert chat_message_two[:id] == cm_ref.id - assert chat_message_two[:content] == object.data["content"] - assert chat_message_two[:account_id] == recipient.id - assert chat_message_two[:chat_id] == chat_message[:chat_id] - assert chat_message_two[:attachment] - assert chat_message_two[:unread] == true - assert chat_message_two[:card] - end -end diff --git a/test/web/pleroma_api/views/chat_view_test.exs b/test/web/pleroma_api/views/chat_view_test.exs @@ -1,49 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.ChatViewTest do - use Pleroma.DataCase - - alias Pleroma.Chat - alias Pleroma.Chat.MessageReference - alias Pleroma.Object - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.CommonAPI.Utils - alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView - alias Pleroma.Web.PleromaAPI.ChatView - - import Pleroma.Factory - - test "it represents a chat" do - user = insert(:user) - recipient = insert(:user) - - {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) - - represented_chat = ChatView.render("show.json", chat: chat) - - assert represented_chat == %{ - id: "#{chat.id}", - account: - AccountView.render("show.json", user: recipient, skip_visibility_check: true), - unread: 0, - last_message: nil, - updated_at: Utils.to_masto_date(chat.updated_at) - } - - {:ok, chat_message_creation} = CommonAPI.post_chat_message(user, recipient, "hello") - - chat_message = Object.normalize(chat_message_creation, false) - - {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) - - represented_chat = ChatView.render("show.json", chat: chat) - - cm_ref = MessageReference.for_chat_and_object(chat, chat_message) - - assert represented_chat[:last_message] == - MessageReferenceView.render("show.json", chat_message_reference: cm_ref) - end -end diff --git a/test/web/pleroma_api/views/scrobble_view_test.exs b/test/web/pleroma_api/views/scrobble_view_test.exs @@ -1,20 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.StatusViewTest do - use Pleroma.DataCase - - alias Pleroma.Web.PleromaAPI.ScrobbleView - - import Pleroma.Factory - - test "successfully renders a Listen activity (pleroma extension)" do - listen_activity = insert(:listen) - - status = ScrobbleView.render("show.json", activity: listen_activity) - - assert status.length == listen_activity.data["object"]["length"] - assert status.title == listen_activity.data["object"]["title"] - end -end diff --git a/test/web/plugs/federating_plug_test.exs b/test/web/plugs/federating_plug_test.exs @@ -1,31 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.FederatingPlugTest do - use Pleroma.Web.ConnCase - - setup do: clear_config([:instance, :federating]) - - test "returns and halt the conn when federating is disabled" do - Pleroma.Config.put([:instance, :federating], false) - - conn = - build_conn() - |> Pleroma.Web.FederatingPlug.call(%{}) - - assert conn.status == 404 - assert conn.halted - end - - test "does nothing when federating is enabled" do - Pleroma.Config.put([:instance, :federating], true) - - conn = - build_conn() - |> Pleroma.Web.FederatingPlug.call(%{}) - - refute conn.status - refute conn.halted - end -end diff --git a/test/web/plugs/plug_test.exs b/test/web/plugs/plug_test.exs @@ -1,91 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PlugTest do - @moduledoc "Tests for the functionality added via `use Pleroma.Web, :plug`" - - alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug - alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug - alias Pleroma.Plugs.PlugHelper - - import Mock - - use Pleroma.Web.ConnCase - - describe "when plug is skipped, " do - setup_with_mocks( - [ - {ExpectPublicOrAuthenticatedCheckPlug, [:passthrough], []} - ], - %{conn: conn} - ) do - conn = ExpectPublicOrAuthenticatedCheckPlug.skip_plug(conn) - %{conn: conn} - end - - test "it neither adds plug to called plugs list nor calls `perform/2`, " <> - "regardless of :if_func / :unless_func options", - %{conn: conn} do - for opts <- [%{}, %{if_func: fn _ -> true end}, %{unless_func: fn _ -> false end}] do - ret_conn = ExpectPublicOrAuthenticatedCheckPlug.call(conn, opts) - - refute called(ExpectPublicOrAuthenticatedCheckPlug.perform(:_, :_)) - refute PlugHelper.plug_called?(ret_conn, ExpectPublicOrAuthenticatedCheckPlug) - end - end - end - - describe "when plug is NOT skipped, " do - setup_with_mocks([{ExpectAuthenticatedCheckPlug, [:passthrough], []}]) do - :ok - end - - test "with no pre-run checks, adds plug to called plugs list and calls `perform/2`", %{ - conn: conn - } do - ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{}) - - assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) - assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) - end - - test "when :if_func option is given, calls the plug only if provided function evals tru-ish", - %{conn: conn} do - ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{if_func: fn _ -> false end}) - - refute called(ExpectAuthenticatedCheckPlug.perform(:_, :_)) - refute PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) - - ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{if_func: fn _ -> true end}) - - assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) - assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) - end - - test "if :unless_func option is given, calls the plug only if provided function evals falsy", - %{conn: conn} do - ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{unless_func: fn _ -> true end}) - - refute called(ExpectAuthenticatedCheckPlug.perform(:_, :_)) - refute PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) - - ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{unless_func: fn _ -> false end}) - - assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) - assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) - end - - test "allows a plug to be called multiple times (even if it's in called plugs list)", %{ - conn: conn - } do - conn = ExpectAuthenticatedCheckPlug.call(conn, %{an_option: :value1}) - assert called(ExpectAuthenticatedCheckPlug.perform(conn, %{an_option: :value1})) - - assert PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) - - conn = ExpectAuthenticatedCheckPlug.call(conn, %{an_option: :value2}) - assert called(ExpectAuthenticatedCheckPlug.perform(conn, %{an_option: :value2})) - end - end -end diff --git a/test/web/preload/instance_test.exs b/test/web/preload/instance_test.exs @@ -1,56 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Preload.Providers.InstanceTest do - use Pleroma.DataCase - alias Pleroma.Web.Preload.Providers.Instance - - setup do: {:ok, Instance.generate_terms(nil)} - - test "it renders the info", %{"/api/v1/instance" => info} do - assert %{ - description: description, - email: "admin@example.com", - registrations: true - } = info - - assert String.equivalent?(description, "Pleroma: An efficient and flexible fediverse server") - end - - test "it renders the panel", %{"/instance/panel.html" => panel} do - assert String.contains?( - panel, - "<p>Welcome to <a href=\"https://pleroma.social\" target=\"_blank\">Pleroma!</a></p>" - ) - end - - test "it works with overrides" do - clear_config([:instance, :static_dir], "test/fixtures/preload_static") - - %{"/instance/panel.html" => panel} = Instance.generate_terms(nil) - - assert String.contains?( - panel, - "HEY!" - ) - end - - test "it renders the node_info", %{"/nodeinfo/2.0.json" => nodeinfo} do - %{ - metadata: metadata, - version: "2.0" - } = nodeinfo - - assert metadata.private == false - assert metadata.suggestions == %{enabled: false} - end - - test "it renders the frontend configurations", %{ - "/api/pleroma/frontend_configurations" => fe_configs - } do - assert %{ - pleroma_fe: %{background: "/images/city.jpg", logo: "/static/logo.png"} - } = fe_configs - end -end diff --git a/test/web/preload/timeline_test.exs b/test/web/preload/timeline_test.exs @@ -1,56 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Preload.Providers.TimelineTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Preload.Providers.Timelines - - @public_url "/api/v1/timelines/public" - - describe "unauthenticated timeliness when restricted" do - setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true) - setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true) - - test "return nothing" do - tl_data = Timelines.generate_terms(%{}) - - refute Map.has_key?(tl_data, "/api/v1/timelines/public") - end - end - - describe "unauthenticated timeliness when unrestricted" do - setup do: clear_config([:restrict_unauthenticated, :timelines, :local], false) - setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], false) - - setup do: {:ok, user: insert(:user)} - - test "returns the timeline when not restricted" do - assert Timelines.generate_terms(%{}) - |> Map.has_key?(@public_url) - end - - test "returns public items", %{user: user} do - {:ok, _} = CommonAPI.post(user, %{status: "it's post 1!"}) - {:ok, _} = CommonAPI.post(user, %{status: "it's post 2!"}) - {:ok, _} = CommonAPI.post(user, %{status: "it's post 3!"}) - - assert Timelines.generate_terms(%{}) - |> Map.fetch!(@public_url) - |> Enum.count() == 3 - end - - test "does not return non-public items", %{user: user} do - {:ok, _} = CommonAPI.post(user, %{status: "it's post 1!", visibility: "unlisted"}) - {:ok, _} = CommonAPI.post(user, %{status: "it's post 2!", visibility: "direct"}) - {:ok, _} = CommonAPI.post(user, %{status: "it's post 3!"}) - - assert Timelines.generate_terms(%{}) - |> Map.fetch!(@public_url) - |> Enum.count() == 1 - end - end -end diff --git a/test/web/preload/user_test.exs b/test/web/preload/user_test.exs @@ -1,33 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Preload.Providers.UserTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Web.Preload.Providers.User - - describe "returns empty when user doesn't exist" do - test "nil user specified" do - assert User.generate_terms(%{user: nil}) == %{} - end - - test "missing user specified" do - assert User.generate_terms(%{user: :not_a_user}) == %{} - end - end - - describe "specified user exists" do - setup do - user = insert(:user) - - terms = User.generate_terms(%{user: user}) - %{terms: terms, user: user} - end - - test "account is rendered", %{terms: terms, user: user} do - account = terms["/api/v1/accounts/#{user.id}"] - assert %{acct: user, username: user} = account - end - end -end diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs @@ -1,344 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Push.ImplTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Notification - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Push.Impl - alias Pleroma.Web.Push.Subscription - - setup do - Tesla.Mock.mock(fn - %{method: :post, url: "https://example.com/example/1234"} -> - %Tesla.Env{status: 200} - - %{method: :post, url: "https://example.com/example/not_found"} -> - %Tesla.Env{status: 400} - - %{method: :post, url: "https://example.com/example/bad"} -> - %Tesla.Env{status: 100} - end) - - :ok - end - - @sub %{ - endpoint: "https://example.com/example/1234", - keys: %{ - auth: "8eDyX_uCN0XRhSbY5hs7Hg==", - p256dh: - "BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=" - } - } - @api_key "BASgACIHpN1GYgzSRp" - @message "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." - - test "performs sending notifications" do - user = insert(:user) - user2 = insert(:user) - insert(:push_subscription, user: user, data: %{alerts: %{"mention" => true}}) - insert(:push_subscription, user: user2, data: %{alerts: %{"mention" => true}}) - - insert(:push_subscription, - user: user, - data: %{alerts: %{"follow" => true, "mention" => true}} - ) - - insert(:push_subscription, - user: user, - data: %{alerts: %{"follow" => true, "mention" => false}} - ) - - {:ok, activity} = CommonAPI.post(user, %{status: "<Lorem ipsum dolor sit amet."}) - - notif = - insert(:notification, - user: user, - activity: activity, - type: "mention" - ) - - assert Impl.perform(notif) == {:ok, [:ok, :ok]} - end - - @tag capture_log: true - test "returns error if notif does not match " do - assert Impl.perform(%{}) == {:error, :unknown_type} - end - - test "successful message sending" do - assert Impl.push_message(@message, @sub, @api_key, %Subscription{}) == :ok - end - - @tag capture_log: true - test "fail message sending" do - assert Impl.push_message( - @message, - Map.merge(@sub, %{endpoint: "https://example.com/example/bad"}), - @api_key, - %Subscription{} - ) == :error - end - - test "delete subscription if result send message between 400..500" do - subscription = insert(:push_subscription) - - assert Impl.push_message( - @message, - Map.merge(@sub, %{endpoint: "https://example.com/example/not_found"}), - @api_key, - subscription - ) == :ok - - refute Pleroma.Repo.get(Subscription, subscription.id) - end - - test "deletes subscription when token has been deleted" do - subscription = insert(:push_subscription) - - Pleroma.Repo.delete(subscription.token) - - refute Pleroma.Repo.get(Subscription, subscription.id) - end - - test "renders title and body for create activity" do - user = insert(:user, nickname: "Bob") - - {:ok, activity} = - CommonAPI.post(user, %{ - status: - "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." - }) - - object = Object.normalize(activity) - - assert Impl.format_body( - %{ - activity: activity - }, - user, - object - ) == - "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." - - assert Impl.format_title(%{activity: activity, type: "mention"}) == - "New Mention" - end - - test "renders title and body for follow activity" do - user = insert(:user, nickname: "Bob") - other_user = insert(:user) - {:ok, _, _, activity} = CommonAPI.follow(user, other_user) - object = Object.normalize(activity, false) - - assert Impl.format_body(%{activity: activity, type: "follow"}, user, object) == - "@Bob has followed you" - - assert Impl.format_title(%{activity: activity, type: "follow"}) == - "New Follower" - end - - test "renders title and body for announce activity" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: - "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." - }) - - {:ok, announce_activity} = CommonAPI.repeat(activity.id, user) - object = Object.normalize(activity) - - assert Impl.format_body(%{activity: announce_activity}, user, object) == - "@#{user.nickname} repeated: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." - - assert Impl.format_title(%{activity: announce_activity, type: "reblog"}) == - "New Repeat" - end - - test "renders title and body for like activity" do - user = insert(:user, nickname: "Bob") - - {:ok, activity} = - CommonAPI.post(user, %{ - status: - "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." - }) - - {:ok, activity} = CommonAPI.favorite(user, activity.id) - object = Object.normalize(activity) - - assert Impl.format_body(%{activity: activity, type: "favourite"}, user, object) == - "@Bob has favorited your post" - - assert Impl.format_title(%{activity: activity, type: "favourite"}) == - "New Favorite" - end - - test "renders title for create activity with direct visibility" do - user = insert(:user, nickname: "Bob") - - {:ok, activity} = - CommonAPI.post(user, %{ - visibility: "direct", - status: "This is just between you and me, pal" - }) - - assert Impl.format_title(%{activity: activity}) == - "New Direct Message" - end - - describe "build_content/3" do - test "builds content for chat messages" do - user = insert(:user) - recipient = insert(:user) - - {:ok, chat} = CommonAPI.post_chat_message(user, recipient, "hey") - object = Object.normalize(chat, false) - [notification] = Notification.for_user(recipient) - - res = Impl.build_content(notification, user, object) - - assert res == %{ - body: "@#{user.nickname}: hey", - title: "New Chat Message" - } - end - - test "builds content for chat messages with no content" do - user = insert(:user) - recipient = insert(:user) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) - - {:ok, chat} = CommonAPI.post_chat_message(user, recipient, nil, media_id: upload.id) - object = Object.normalize(chat, false) - [notification] = Notification.for_user(recipient) - - res = Impl.build_content(notification, user, object) - - assert res == %{ - body: "@#{user.nickname}: (Attachment)", - title: "New Chat Message" - } - end - - test "hides contents of notifications when option enabled" do - user = insert(:user, nickname: "Bob") - - user2 = - insert(:user, nickname: "Rob", notification_settings: %{hide_notification_contents: true}) - - {:ok, activity} = - CommonAPI.post(user, %{ - visibility: "direct", - status: "<Lorem ipsum dolor sit amet." - }) - - notif = insert(:notification, user: user2, activity: activity) - - actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) - object = Object.normalize(activity) - - assert Impl.build_content(notif, actor, object) == %{ - body: "New Direct Message" - } - - {:ok, activity} = - CommonAPI.post(user, %{ - visibility: "public", - status: "<Lorem ipsum dolor sit amet." - }) - - notif = insert(:notification, user: user2, activity: activity, type: "mention") - - actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) - object = Object.normalize(activity) - - assert Impl.build_content(notif, actor, object) == %{ - body: "New Mention" - } - - {:ok, activity} = CommonAPI.favorite(user, activity.id) - - notif = insert(:notification, user: user2, activity: activity, type: "favourite") - - actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) - object = Object.normalize(activity) - - assert Impl.build_content(notif, actor, object) == %{ - body: "New Favorite" - } - end - - test "returns regular content when hiding contents option disabled" do - user = insert(:user, nickname: "Bob") - - user2 = - insert(:user, nickname: "Rob", notification_settings: %{hide_notification_contents: false}) - - {:ok, activity} = - CommonAPI.post(user, %{ - visibility: "direct", - status: - "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." - }) - - notif = insert(:notification, user: user2, activity: activity) - - actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) - object = Object.normalize(activity) - - assert Impl.build_content(notif, actor, object) == %{ - body: - "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...", - title: "New Direct Message" - } - - {:ok, activity} = - CommonAPI.post(user, %{ - visibility: "public", - status: - "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." - }) - - notif = insert(:notification, user: user2, activity: activity, type: "mention") - - actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) - object = Object.normalize(activity) - - assert Impl.build_content(notif, actor, object) == %{ - body: - "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...", - title: "New Mention" - } - - {:ok, activity} = CommonAPI.favorite(user, activity.id) - - notif = insert(:notification, user: user2, activity: activity, type: "favourite") - - actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) - object = Object.normalize(activity) - - assert Impl.build_content(notif, actor, object) == %{ - body: "@Bob has favorited your post", - title: "New Favorite" - } - end - end -end diff --git a/test/web/rel_me_test.exs b/test/web/rel_me_test.exs @@ -1,48 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RelMeTest do - use ExUnit.Case - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "parse/1" do - hrefs = ["https://social.example.org/users/lain"] - - assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/null") == {:ok, []} - - assert {:ok, %Tesla.Env{status: 404}} = - Pleroma.Web.RelMe.parse("http://example.com/rel_me/error") - - assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/link") == {:ok, hrefs} - assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/anchor") == {:ok, hrefs} - assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/anchor_nofollow") == {:ok, hrefs} - end - - test "maybe_put_rel_me/2" do - profile_urls = ["https://social.example.org/users/lain"] - attr = "me" - fallback = nil - - assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/null", profile_urls) == - fallback - - assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/error", profile_urls) == - fallback - - assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/anchor", profile_urls) == - attr - - assert Pleroma.Web.RelMe.maybe_put_rel_me( - "http://example.com/rel_me/anchor_nofollow", - profile_urls - ) == attr - - assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/link", profile_urls) == - attr - end -end diff --git a/test/web/rich_media/aws_signed_url_test.exs b/test/web/rich_media/aws_signed_url_test.exs @@ -1,82 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.TTL.AwsSignedUrlTest do - use ExUnit.Case, async: true - - test "s3 signed url is parsed correct for expiration time" do - url = "https://pleroma.social/amz" - - {:ok, timestamp} = - Timex.now() - |> DateTime.truncate(:second) - |> Timex.format("{ISO:Basic:Z}") - - # in seconds - valid_till = 30 - - metadata = construct_metadata(timestamp, valid_till, url) - - 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) - end - - test "s3 signed url is parsed and correct ttl is set for rich media" do - url = "https://pleroma.social/amz" - - {:ok, timestamp} = - Timex.now() - |> DateTime.truncate(:second) - |> Timex.format("{ISO:Basic:Z}") - - # in seconds - valid_till = 30 - - metadata = construct_metadata(timestamp, valid_till, url) - - body = """ - <meta name="twitter:card" content="Pleroma" /> - <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)}" /> - """ - - Tesla.Mock.mock(fn - %{ - method: :get, - url: "https://pleroma.social/amz" - } -> - %Tesla.Env{status: 200, body: body} - end) - - Cachex.put(:rich_media_cache, url, metadata) - - Pleroma.Web.RichMedia.Parser.set_ttl_based_on_image(metadata, url) - - {:ok, cache_ttl} = Cachex.ttl(:rich_media_cache, url) - - # 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) - end - - defp construct_s3_url(timestamp, valid_till) do - "https://pleroma.s3.ap-southeast-1.amazonaws.com/sachin%20%281%29%20_a%20-%25%2Aasdasd%20BNN%20bnnn%20.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIBLWWK6RGDQXDLJQ%2F20190716%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=#{ - timestamp - }&X-Amz-Expires=#{valid_till}&X-Amz-Signature=04ffd6b98634f4b1bbabc62e0fac4879093cd54a6eed24fe8eb38e8369526bbf&X-Amz-SignedHeaders=host" - end - - defp construct_metadata(timestamp, valid_till, url) do - %{ - image: construct_s3_url(timestamp, valid_till), - site: "Pleroma", - title: "Pleroma", - description: "Pleroma", - url: url - } - end -end diff --git a/test/web/rich_media/helpers_test.exs b/test/web/rich_media/helpers_test.exs @@ -1,86 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.HelpersTest do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.Object - 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" - }) - - Config.put([: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" - }) - - Config.put([: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" - }) - - Config.put([: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"}) - - Config.put([: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/web/rich_media/parser_test.exs b/test/web/rich_media/parser_test.exs @@ -1,176 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.ParserTest do - use ExUnit.Case, async: true - - 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) - - :ok - end - - test "returns error when no metadata present" do - assert {:error, _} = Parser.parse("http://example.com/empty") - end - - test "doesn't just add a title" do - assert {:error, {:invalid_metadata, _}} = Parser.parse("http://example.com/non-ogp") - end - - test "parses ogp" do - assert Parser.parse("http://example.com/ogp") == - {:ok, - %{ - "image" => "http://ia.media-imdb.com/images/rock.jpg", - "title" => "The Rock", - "description" => - "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", - "type" => "video.movie", - "url" => "http://example.com/ogp" - }} - end - - test "falls back to <title> when ogp:title is missing" do - assert Parser.parse("http://example.com/ogp-missing-title") == - {:ok, - %{ - "image" => "http://ia.media-imdb.com/images/rock.jpg", - "title" => "The Rock (1996)", - "description" => - "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", - "type" => "video.movie", - "url" => "http://example.com/ogp-missing-title" - }} - end - - test "parses twitter card" do - assert Parser.parse("http://example.com/twitter-card") == - {:ok, - %{ - "card" => "summary", - "site" => "@flickr", - "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" - }} - end - - test "parses OEmbed" do - assert Parser.parse("http://example.com/oembed") == - {:ok, - %{ - "author_name" => "‮‭‬bees‬", - "author_url" => "https://www.flickr.com/photos/bees/", - "cache_age" => 3600, - "flickr_type" => "photo", - "height" => "768", - "html" => - "<a data-flickr-embed=\"true\" href=\"https://www.flickr.com/photos/bees/2362225867/\" title=\"Bacon Lollys by ‮‭‬bees‬, 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>", - "license" => "All Rights Reserved", - "license_id" => 0, - "provider_name" => "Flickr", - "provider_url" => "https://www.flickr.com/", - "thumbnail_height" => 150, - "thumbnail_url" => - "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg", - "thumbnail_width" => 150, - "title" => "Bacon Lollys", - "type" => "photo", - "url" => "http://example.com/oembed", - "version" => "1.0", - "web_page" => "https://www.flickr.com/photos/bees/2362225867/", - "web_page_short_url" => "https://flic.kr/p/4AK2sc", - "width" => "1024" - }} - end - - test "rejects invalid OGP data" do - assert {:error, _} = Parser.parse("http://example.com/malformed") - end - - test "returns error if getting page was not successful" do - assert {:error, :overload} = Parser.parse("http://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") - 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") - end -end diff --git a/test/web/rich_media/parsers/twitter_card_test.exs b/test/web/rich_media/parsers/twitter_card_test.exs @@ -1,127 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do - use ExUnit.Case, async: true - alias Pleroma.Web.RichMedia.Parsers.TwitterCard - - test "returns error when html not contains twitter card" do - assert TwitterCard.parse([{"html", [], [{"head", [], []}, {"body", [], []}]}], %{}) == %{} - end - - test "parses twitter card with only name attributes" do - html = - File.read!("test/fixtures/nypd-facial-recognition-children-teenagers3.html") - |> Floki.parse_document!() - - assert TwitterCard.parse(html, %{}) == - %{ - "app:id:googleplay" => "com.nytimes.android", - "app:name:googleplay" => "NYTimes", - "app:url:googleplay" => "nytimes://reader/id/100000006583622", - "site" => nil, - "description" => - "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", - "image" => - "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg", - "type" => "article", - "url" => - "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", - "title" => - "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database." - } - end - - test "parses twitter card with only property attributes" do - html = - File.read!("test/fixtures/nypd-facial-recognition-children-teenagers2.html") - |> Floki.parse_document!() - - assert TwitterCard.parse(html, %{}) == - %{ - "card" => "summary_large_image", - "description" => - "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", - "image" => - "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", - "image:alt" => "", - "title" => - "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", - "url" => - "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", - "type" => "article" - } - end - - test "parses twitter card with name & property attributes" do - html = - File.read!("test/fixtures/nypd-facial-recognition-children-teenagers.html") - |> Floki.parse_document!() - - assert TwitterCard.parse(html, %{}) == - %{ - "app:id:googleplay" => "com.nytimes.android", - "app:name:googleplay" => "NYTimes", - "app:url:googleplay" => "nytimes://reader/id/100000006583622", - "card" => "summary_large_image", - "description" => - "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", - "image" => - "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", - "image:alt" => "", - "site" => nil, - "title" => - "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", - "url" => - "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", - "type" => "article" - } - end - - test "respect only first title tag on the page" do - image_path = - "https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzLzkwYzgyMzI4LThlMDUtNGRiNS05MDg3LTUzMGUxZTM5N2RmMmVkOTM5ZDM4MGM4OTIx" <> - "YTQ5MF9EQVIgZXhodW1hdGlvbiBvZiBNYXJnYXJldCBDb3JiaW4gZ3JhdmUgMTkyNi5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9" <> - "yaWVudCJdLFsicCIsInRodW1iIiwiNjAweD4iXV0/DAR%20exhumation%20of%20Margaret%20Corbin%20grave%201926.jpg" - - html = - File.read!("test/fixtures/margaret-corbin-grave-west-point.html") |> Floki.parse_document!() - - assert TwitterCard.parse(html, %{}) == - %{ - "site" => "@atlasobscura", - "title" => "The Missing Grave of Margaret Corbin, Revolutionary War Veteran", - "card" => "summary_large_image", - "image" => image_path, - "description" => - "She's the only woman veteran honored with a monument at West Point. But where was she buried?", - "site_name" => "Atlas Obscura", - "type" => "article", - "url" => "http://www.atlasobscura.com/articles/margaret-corbin-grave-west-point" - } - end - - test "takes first founded title in html head if there is html markup error" do - html = - File.read!("test/fixtures/nypd-facial-recognition-children-teenagers4.html") - |> Floki.parse_document!() - - assert TwitterCard.parse(html, %{}) == - %{ - "site" => nil, - "title" => - "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", - "app:id:googleplay" => "com.nytimes.android", - "app:name:googleplay" => "NYTimes", - "app:url:googleplay" => "nytimes://reader/id/100000006583622", - "description" => - "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", - "image" => - "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg", - "type" => "article", - "url" => - "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" - } - end -end diff --git a/test/web/static_fe/static_fe_controller_test.exs b/test/web/static_fe/static_fe_controller_test.exs @@ -1,196 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Activity - alias Pleroma.Config - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - setup_all do: clear_config([:static_fe, :enabled], true) - setup do: clear_config([:instance, :federating], true) - - setup %{conn: conn} do - conn = put_req_header(conn, "accept", "text/html") - user = insert(:user) - - %{conn: conn, user: user} - end - - describe "user profile html" do - test "just the profile as HTML", %{conn: conn, user: user} do - conn = get(conn, "/users/#{user.nickname}") - - assert html_response(conn, 200) =~ user.nickname - end - - test "404 when user not found", %{conn: conn} do - conn = get(conn, "/users/limpopo") - - assert html_response(conn, 404) =~ "not found" - end - - test "profile does not include private messages", %{conn: conn, user: user} do - CommonAPI.post(user, %{status: "public"}) - CommonAPI.post(user, %{status: "private", visibility: "private"}) - - conn = get(conn, "/users/#{user.nickname}") - - html = html_response(conn, 200) - - assert html =~ ">public<" - refute html =~ ">private<" - end - - test "pagination", %{conn: conn, user: user} do - Enum.map(1..30, fn i -> CommonAPI.post(user, %{status: "test#{i}"}) end) - - conn = get(conn, "/users/#{user.nickname}") - - html = html_response(conn, 200) - - assert html =~ ">test30<" - assert html =~ ">test11<" - refute html =~ ">test10<" - refute html =~ ">test1<" - end - - test "pagination, page 2", %{conn: conn, user: user} do - activities = Enum.map(1..30, fn i -> CommonAPI.post(user, %{status: "test#{i}"}) end) - {:ok, a11} = Enum.at(activities, 11) - - conn = get(conn, "/users/#{user.nickname}?max_id=#{a11.id}") - - html = html_response(conn, 200) - - assert html =~ ">test1<" - assert html =~ ">test10<" - refute html =~ ">test20<" - refute html =~ ">test29<" - end - - test "it requires authentication if instance is NOT federating", %{conn: conn, user: user} do - ensure_federating_or_authenticated(conn, "/users/#{user.nickname}", user) - end - end - - describe "notice html" do - test "single notice page", %{conn: conn, user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"}) - - conn = get(conn, "/notice/#{activity.id}") - - html = html_response(conn, 200) - assert html =~ "<header>" - assert html =~ user.nickname - assert html =~ "testing a thing!" - end - - test "redirects to json if requested", %{conn: conn, user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"}) - - conn = - conn - |> put_req_header( - "accept", - "Accept: application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\", text/html" - ) - |> get("/notice/#{activity.id}") - - assert redirected_to(conn, 302) =~ activity.data["object"] - end - - test "filters HTML tags", %{conn: conn} do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "<script>alert('xss')</script>"}) - - conn = - conn - |> put_req_header("accept", "text/html") - |> get("/notice/#{activity.id}") - - html = html_response(conn, 200) - assert html =~ ~s[&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;] - end - - test "shows the whole thread", %{conn: conn, user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "space: the final frontier"}) - - CommonAPI.post(user, %{ - status: "these are the voyages or something", - in_reply_to_status_id: activity.id - }) - - conn = get(conn, "/notice/#{activity.id}") - - html = html_response(conn, 200) - assert html =~ "the final frontier" - assert html =~ "voyages" - end - - test "redirect by AP object ID", %{conn: conn, user: user} do - {:ok, %Activity{data: %{"object" => object_url}}} = - CommonAPI.post(user, %{status: "beam me up"}) - - conn = get(conn, URI.parse(object_url).path) - - assert html_response(conn, 302) =~ "redirected" - end - - test "redirect by activity ID", %{conn: conn, user: user} do - {:ok, %Activity{data: %{"id" => id}}} = - CommonAPI.post(user, %{status: "I'm a doctor, not a devops!"}) - - conn = get(conn, URI.parse(id).path) - - assert html_response(conn, 302) =~ "redirected" - end - - test "404 when notice not found", %{conn: conn} do - conn = get(conn, "/notice/88c9c317") - - assert html_response(conn, 404) =~ "not found" - end - - test "404 for private status", %{conn: conn, user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "don't show me!", visibility: "private"}) - - conn = get(conn, "/notice/#{activity.id}") - - assert html_response(conn, 404) =~ "not found" - end - - test "302 for remote cached status", %{conn: conn, user: user} do - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "to" => user.follower_address, - "cc" => "https://www.w3.org/ns/activitystreams#Public", - "type" => "Create", - "object" => %{ - "content" => "blah blah blah", - "type" => "Note", - "attributedTo" => user.ap_id, - "inReplyTo" => nil - }, - "actor" => user.ap_id - } - - assert {:ok, activity} = Transmogrifier.handle_incoming(message) - - conn = get(conn, "/notice/#{activity.id}") - - assert html_response(conn, 302) =~ "redirected" - end - - test "it requires authentication if instance is NOT federating", %{conn: conn, user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"}) - - ensure_federating_or_authenticated(conn, "/notice/#{activity.id}", user) - end - end -end diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs @@ -1,767 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.StreamerTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Chat - alias Pleroma.Chat.MessageReference - alias Pleroma.Conversation.Participation - alias Pleroma.List - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Streamer - alias Pleroma.Web.StreamerView - - @moduletag needs_streamer: true, capture_log: true - - setup do: clear_config([:instance, :skip_thread_containment]) - - describe "get_topic/_ (unauthenticated)" do - test "allows public" do - assert {:ok, "public"} = Streamer.get_topic("public", nil, nil) - assert {:ok, "public:local"} = Streamer.get_topic("public:local", nil, nil) - assert {:ok, "public:media"} = Streamer.get_topic("public:media", nil, nil) - assert {:ok, "public:local:media"} = Streamer.get_topic("public:local:media", nil, nil) - end - - test "allows hashtag streams" do - assert {:ok, "hashtag:cofe"} = Streamer.get_topic("hashtag", nil, nil, %{"tag" => "cofe"}) - end - - test "disallows user streams" do - assert {:error, _} = Streamer.get_topic("user", nil, nil) - assert {:error, _} = Streamer.get_topic("user:notification", nil, nil) - assert {:error, _} = Streamer.get_topic("direct", nil, nil) - end - - test "disallows list streams" do - assert {:error, _} = Streamer.get_topic("list", nil, nil, %{"list" => 42}) - end - end - - describe "get_topic/_ (authenticated)" do - setup do: oauth_access(["read"]) - - test "allows public streams (regardless of OAuth token scopes)", %{ - user: user, - token: read_oauth_token - } do - with oauth_token <- [nil, read_oauth_token] do - assert {:ok, "public"} = Streamer.get_topic("public", user, oauth_token) - assert {:ok, "public:local"} = Streamer.get_topic("public:local", user, oauth_token) - assert {:ok, "public:media"} = Streamer.get_topic("public:media", user, oauth_token) - - assert {:ok, "public:local:media"} = - Streamer.get_topic("public:local:media", user, oauth_token) - end - end - - test "allows user streams (with proper OAuth token scopes)", %{ - user: user, - token: read_oauth_token - } do - %{token: read_notifications_token} = oauth_access(["read:notifications"], user: user) - %{token: read_statuses_token} = oauth_access(["read:statuses"], user: user) - %{token: badly_scoped_token} = oauth_access(["irrelevant:scope"], user: user) - - expected_user_topic = "user:#{user.id}" - expected_notification_topic = "user:notification:#{user.id}" - expected_direct_topic = "direct:#{user.id}" - expected_pleroma_chat_topic = "user:pleroma_chat:#{user.id}" - - for valid_user_token <- [read_oauth_token, read_statuses_token] do - assert {:ok, ^expected_user_topic} = Streamer.get_topic("user", user, valid_user_token) - - assert {:ok, ^expected_direct_topic} = - Streamer.get_topic("direct", user, valid_user_token) - - assert {:ok, ^expected_pleroma_chat_topic} = - Streamer.get_topic("user:pleroma_chat", user, valid_user_token) - end - - for invalid_user_token <- [read_notifications_token, badly_scoped_token], - user_topic <- ["user", "direct", "user:pleroma_chat"] do - assert {:error, :unauthorized} = Streamer.get_topic(user_topic, user, invalid_user_token) - end - - for valid_notification_token <- [read_oauth_token, read_notifications_token] do - assert {:ok, ^expected_notification_topic} = - Streamer.get_topic("user:notification", user, valid_notification_token) - end - - for invalid_notification_token <- [read_statuses_token, badly_scoped_token] do - assert {:error, :unauthorized} = - Streamer.get_topic("user:notification", user, invalid_notification_token) - end - end - - test "allows hashtag streams (regardless of OAuth token scopes)", %{ - user: user, - token: read_oauth_token - } do - for oauth_token <- [nil, read_oauth_token] do - assert {:ok, "hashtag:cofe"} = - Streamer.get_topic("hashtag", user, oauth_token, %{"tag" => "cofe"}) - end - end - - test "disallows registering to another user's stream", %{user: user, token: read_oauth_token} do - another_user = insert(:user) - assert {:error, _} = Streamer.get_topic("user:#{another_user.id}", user, read_oauth_token) - - assert {:error, _} = - Streamer.get_topic("user:notification:#{another_user.id}", user, read_oauth_token) - - assert {:error, _} = Streamer.get_topic("direct:#{another_user.id}", user, read_oauth_token) - end - - test "allows list stream that are owned by the user (with `read` or `read:lists` scopes)", %{ - user: user, - token: read_oauth_token - } do - %{token: read_lists_token} = oauth_access(["read:lists"], user: user) - %{token: invalid_token} = oauth_access(["irrelevant:scope"], user: user) - {:ok, list} = List.create("Test", user) - - assert {:error, _} = Streamer.get_topic("list:#{list.id}", user, read_oauth_token) - - for valid_token <- [read_oauth_token, read_lists_token] do - assert {:ok, _} = Streamer.get_topic("list", user, valid_token, %{"list" => list.id}) - end - - assert {:error, _} = Streamer.get_topic("list", user, invalid_token, %{"list" => list.id}) - end - - test "disallows list stream that are not owned by the user", %{user: user, token: oauth_token} do - another_user = insert(:user) - {:ok, list} = List.create("Test", another_user) - - assert {:error, _} = Streamer.get_topic("list:#{list.id}", user, oauth_token) - assert {:error, _} = Streamer.get_topic("list", user, oauth_token, %{"list" => list.id}) - end - end - - describe "user streams" do - setup do - %{user: user, token: token} = oauth_access(["read"]) - notify = insert(:notification, user: user, activity: build(:note_activity)) - {:ok, %{user: user, notify: notify, token: token}} - end - - test "it streams the user's post in the 'user' stream", %{user: user, token: oauth_token} do - Streamer.get_topic_and_add_socket("user", user, oauth_token) - {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - - assert_receive {:render_with_user, _, _, ^activity} - refute Streamer.filtered_by_user?(user, activity) - end - - test "it streams boosts of the user in the 'user' stream", %{user: user, token: oauth_token} do - Streamer.get_topic_and_add_socket("user", user, oauth_token) - - other_user = insert(:user) - {: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} - refute Streamer.filtered_by_user?(user, announce) - end - - test "it does not stream announces of the user's own posts in the 'user' stream", %{ - user: user, - token: oauth_token - } do - Streamer.get_topic_and_add_socket("user", user, oauth_token) - - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, announce} = CommonAPI.repeat(activity.id, other_user) - - assert Streamer.filtered_by_user?(user, announce) - end - - test "it does stream notifications announces of the user's own posts in the 'user' stream", %{ - user: user, - token: oauth_token - } do - Streamer.get_topic_and_add_socket("user", user, oauth_token) - - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, announce} = CommonAPI.repeat(activity.id, other_user) - - notification = - Pleroma.Notification - |> Repo.get_by(%{user_id: user.id, activity_id: announce.id}) - |> Repo.preload(:activity) - - refute Streamer.filtered_by_user?(user, notification) - end - - test "it streams boosts of mastodon user in the 'user' stream", %{ - user: user, - token: oauth_token - } do - Streamer.get_topic_and_add_socket("user", user, oauth_token) - - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) - - data = - File.read!("test/fixtures/mastodon-announce.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - |> Map.put("actor", user.ap_id) - - {: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} - refute Streamer.filtered_by_user?(user, announce) - end - - test "it sends notify to in the 'user' stream", %{ - user: user, - token: oauth_token, - notify: notify - } do - Streamer.get_topic_and_add_socket("user", user, oauth_token) - Streamer.stream("user", notify) - - assert_receive {:render_with_user, _, _, ^notify} - refute Streamer.filtered_by_user?(user, notify) - end - - test "it sends notify to in the 'user:notification' stream", %{ - user: user, - token: oauth_token, - notify: notify - } do - Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) - Streamer.stream("user:notification", notify) - - assert_receive {:render_with_user, _, _, ^notify} - refute Streamer.filtered_by_user?(user, notify) - end - - test "it sends chat messages to the 'user:pleroma_chat' stream", %{ - user: user, - token: oauth_token - } do - other_user = insert(:user) - - {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno") - object = Object.normalize(create_activity, false) - chat = Chat.get(user.id, other_user.ap_id) - cm_ref = MessageReference.for_chat_and_object(chat, object) - cm_ref = %{cm_ref | chat: chat, object: object} - - 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}) - - assert text =~ "hey cirno" - assert_receive {:text, ^text} - end - - test "it sends chat messages to the 'user' stream", %{user: user, token: oauth_token} do - other_user = insert(:user) - - {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno") - object = Object.normalize(create_activity, false) - chat = Chat.get(user.id, other_user.ap_id) - cm_ref = MessageReference.for_chat_and_object(chat, object) - cm_ref = %{cm_ref | chat: chat, object: object} - - 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}) - - assert text =~ "hey cirno" - assert_receive {:text, ^text} - end - - test "it sends chat message notifications to the 'user:notification' stream", %{ - user: user, - token: oauth_token - } do - other_user = insert(:user) - - {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey") - - notify = - Repo.get_by(Pleroma.Notification, user_id: user.id, activity_id: create_activity.id) - |> Repo.preload(:activity) - - Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) - Streamer.stream("user:notification", notify) - - assert_receive {:render_with_user, _, _, ^notify} - refute Streamer.filtered_by_user?(user, notify) - end - - test "it doesn't send notify to the 'user:notification' stream when a user is blocked", %{ - user: user, - token: oauth_token - } do - blocked = insert(:user) - {:ok, _user_relationship} = User.block(user, blocked) - - Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) - - {:ok, activity} = CommonAPI.post(user, %{status: ":("}) - {:ok, _} = CommonAPI.favorite(blocked, activity.id) - - refute_receive _ - end - - test "it doesn't send notify to the 'user:notification' stream when a thread is muted", %{ - user: user, - token: oauth_token - } do - user2 = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) - {:ok, _} = CommonAPI.add_mute(user, activity) - - Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) - - {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) - - refute_receive _ - assert Streamer.filtered_by_user?(user, favorite_activity) - end - - test "it sends favorite to 'user:notification' stream'", %{ - user: user, - token: oauth_token - } do - user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"}) - - {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) - 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 notif.activity.id == favorite_activity.id - refute Streamer.filtered_by_user?(user, notif) - end - - test "it doesn't send the 'user:notification' stream' when a domain is blocked", %{ - user: user, - token: oauth_token - } do - user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"}) - - {:ok, user} = User.block_domain(user, "hecking-lewd-place.com") - {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) - Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) - {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) - - refute_receive _ - assert Streamer.filtered_by_user?(user, favorite_activity) - end - - test "it sends follow activities to the 'user:notification' stream", %{ - user: user, - token: oauth_token - } do - user_url = user.ap_id - user2 = insert(:user) - - body = - File.read!("test/fixtures/users_mock/localhost.json") - |> String.replace("{{nickname}}", user.nickname) - |> Jason.encode!() - - Tesla.Mock.mock_global(fn - %{method: :get, url: ^user_url} -> - %Tesla.Env{status: 200, body: body} - end) - - 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 notif.activity.id == follow_activity.id - refute Streamer.filtered_by_user?(user, notif) - end - end - - describe "public streams" do - test "it sends to public (authenticated)" do - %{user: user, token: oauth_token} = oauth_access(["read"]) - other_user = insert(:user) - - Streamer.get_topic_and_add_socket("public", user, oauth_token) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "Test"}) - assert_receive {:render_with_user, _, _, ^activity} - refute Streamer.filtered_by_user?(other_user, activity) - end - - test "it sends to public (unauthenticated)" do - user = insert(:user) - - Streamer.get_topic_and_add_socket("public", nil, nil) - - {:ok, activity} = CommonAPI.post(user, %{status: "Test"}) - activity_id = activity.id - assert_receive {:text, event} - assert %{"event" => "update", "payload" => payload} = Jason.decode!(event) - assert %{"id" => ^activity_id} = Jason.decode!(payload) - - {:ok, _} = CommonAPI.delete(activity.id, user) - assert_receive {:text, event} - assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event) - end - - test "handles deletions" do - %{user: user, token: oauth_token} = oauth_access(["read"]) - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(other_user, %{status: "Test"}) - - Streamer.get_topic_and_add_socket("public", user, oauth_token) - - {:ok, _} = CommonAPI.delete(activity.id, other_user) - activity_id = activity.id - assert_receive {:text, event} - assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event) - end - end - - describe "thread_containment/2" do - test "it filters to user if recipients invalid and thread containment is enabled" do - Pleroma.Config.put([:instance, :skip_thread_containment], false) - author = insert(:user) - %{user: user, token: oauth_token} = oauth_access(["read"]) - User.follow(user, author, :follow_accept) - - activity = - insert(:note_activity, - note: - insert(:note, - user: author, - data: %{"to" => ["TEST-FFF"]} - ) - ) - - Streamer.get_topic_and_add_socket("public", user, oauth_token) - Streamer.stream("public", activity) - assert_receive {:render_with_user, _, _, ^activity} - assert Streamer.filtered_by_user?(user, activity) - end - - test "it sends message if recipients invalid and thread containment is disabled" do - Pleroma.Config.put([:instance, :skip_thread_containment], true) - author = insert(:user) - %{user: user, token: oauth_token} = oauth_access(["read"]) - User.follow(user, author, :follow_accept) - - activity = - insert(:note_activity, - note: - insert(:note, - user: author, - data: %{"to" => ["TEST-FFF"]} - ) - ) - - Streamer.get_topic_and_add_socket("public", user, oauth_token) - Streamer.stream("public", activity) - - assert_receive {:render_with_user, _, _, ^activity} - refute Streamer.filtered_by_user?(user, activity) - end - - test "it sends message if recipients invalid and thread containment is enabled but user's thread containment is disabled" do - Pleroma.Config.put([:instance, :skip_thread_containment], false) - author = insert(:user) - user = insert(:user, skip_thread_containment: true) - %{token: oauth_token} = oauth_access(["read"], user: user) - User.follow(user, author, :follow_accept) - - activity = - insert(:note_activity, - note: - insert(:note, - user: author, - data: %{"to" => ["TEST-FFF"]} - ) - ) - - Streamer.get_topic_and_add_socket("public", user, oauth_token) - Streamer.stream("public", activity) - - assert_receive {:render_with_user, _, _, ^activity} - refute Streamer.filtered_by_user?(user, activity) - end - end - - describe "blocks" do - setup do: oauth_access(["read"]) - - test "it filters messages involving blocked users", %{user: user, token: oauth_token} do - blocked_user = insert(:user) - {:ok, _user_relationship} = User.block(user, blocked_user) - - 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 Streamer.filtered_by_user?(user, activity) - end - - test "it filters messages transitively involving blocked users", %{ - user: blocker, - token: blocker_token - } do - blockee = insert(:user) - friend = insert(:user) - - Streamer.get_topic_and_add_socket("public", blocker, blocker_token) - - {:ok, _user_relationship} = User.block(blocker, blockee) - - {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey! @#{blockee.nickname}"}) - - 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 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 Streamer.filtered_by_user?(blocker, activity_three) - end - end - - describe "lists" do - setup do: oauth_access(["read"]) - - test "it doesn't send unwanted DMs to list", %{user: user_a, token: user_a_token} do - user_b = insert(:user) - user_c = insert(:user) - - {:ok, user_a} = User.follow(user_a, user_b) - - {:ok, list} = List.create("Test", user_a) - {:ok, list} = List.follow(list, user_b) - - Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) - - {:ok, _activity} = - CommonAPI.post(user_b, %{ - status: "@#{user_c.nickname} Test", - visibility: "direct" - }) - - refute_receive _ - end - - test "it doesn't send unwanted private posts to list", %{user: user_a, token: user_a_token} do - user_b = insert(:user) - - {:ok, list} = List.create("Test", user_a) - {:ok, list} = List.follow(list, user_b) - - Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) - - {:ok, _activity} = - CommonAPI.post(user_b, %{ - status: "Test", - visibility: "private" - }) - - refute_receive _ - end - - test "it sends wanted private posts to list", %{user: user_a, token: user_a_token} do - user_b = insert(:user) - - {:ok, user_a} = User.follow(user_a, user_b) - - {:ok, list} = List.create("Test", user_a) - {:ok, list} = List.follow(list, user_b) - - Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) - - {:ok, activity} = - CommonAPI.post(user_b, %{ - status: "Test", - visibility: "private" - }) - - assert_receive {:render_with_user, _, _, ^activity} - refute Streamer.filtered_by_user?(user_a, activity) - end - end - - describe "muted reblogs" do - setup do: oauth_access(["read"]) - - test "it filters muted reblogs", %{user: user1, token: user1_token} do - user2 = insert(:user) - user3 = insert(:user) - CommonAPI.follow(user1, user2) - CommonAPI.hide_reblogs(user1, user2) - - {:ok, create_activity} = CommonAPI.post(user3, %{status: "I'm kawen"}) - - 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 Streamer.filtered_by_user?(user1, announce_activity) - end - - test "it filters reblog notification for reblog-muted actors", %{ - user: user1, - token: user1_token - } do - user2 = insert(:user) - CommonAPI.follow(user1, user2) - CommonAPI.hide_reblogs(user1, user2) - - {:ok, create_activity} = CommonAPI.post(user1, %{status: "I'm kawen"}) - 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 Streamer.filtered_by_user?(user1, notif) - end - - test "it send non-reblog notification for reblog-muted actors", %{ - user: user1, - token: user1_token - } do - user2 = insert(:user) - CommonAPI.follow(user1, user2) - CommonAPI.hide_reblogs(user1, user2) - - {:ok, create_activity} = CommonAPI.post(user1, %{status: "I'm kawen"}) - 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} - refute Streamer.filtered_by_user?(user1, notif) - end - end - - describe "muted threads" do - test "it filters posts from muted threads" do - user = insert(:user) - %{user: user2, token: user2_token} = oauth_access(["read"]) - Streamer.get_topic_and_add_socket("user", user2, user2_token) - - {:ok, user2, user, _activity} = CommonAPI.follow(user2, user) - {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) - {:ok, _} = CommonAPI.add_mute(user2, activity) - - assert_receive {:render_with_user, _, _, ^activity} - assert Streamer.filtered_by_user?(user2, activity) - end - end - - describe "direct streams" do - setup do: oauth_access(["read"]) - - test "it sends conversation update to the 'direct' stream", %{user: user, token: oauth_token} do - another_user = insert(:user) - - Streamer.get_topic_and_add_socket("direct", user, oauth_token) - - {:ok, _create_activity} = - CommonAPI.post(another_user, %{ - status: "hey @#{user.nickname}", - visibility: "direct" - }) - - assert_receive {:text, received_event} - - assert %{"event" => "conversation", "payload" => received_payload} = - Jason.decode!(received_event) - - assert %{"last_status" => last_status} = Jason.decode!(received_payload) - [participation] = Participation.for_user(user) - assert last_status["pleroma"]["direct_conversation_id"] == participation.id - end - - test "it doesn't send conversation update to the 'direct' stream when the last message in the conversation is deleted", - %{user: user, token: oauth_token} do - another_user = insert(:user) - - Streamer.get_topic_and_add_socket("direct", user, oauth_token) - - {:ok, create_activity} = - CommonAPI.post(another_user, %{ - status: "hi @#{user.nickname}", - visibility: "direct" - }) - - create_activity_id = create_activity.id - assert_receive {:render_with_user, _, _, ^create_activity} - assert_receive {:text, received_conversation1} - assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) - - {:ok, _} = CommonAPI.delete(create_activity_id, another_user) - - assert_receive {:text, received_event} - - assert %{"event" => "delete", "payload" => ^create_activity_id} = - Jason.decode!(received_event) - - refute_receive _ - end - - test "it sends conversation update to the 'direct' stream when a message is deleted", %{ - user: user, - token: oauth_token - } do - another_user = insert(:user) - Streamer.get_topic_and_add_socket("direct", user, oauth_token) - - {:ok, create_activity} = - CommonAPI.post(another_user, %{ - status: "hi @#{user.nickname}", - visibility: "direct" - }) - - {:ok, create_activity2} = - CommonAPI.post(another_user, %{ - status: "hi @#{user.nickname} 2", - in_reply_to_status_id: create_activity.id, - visibility: "direct" - }) - - 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} - assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) - - {:ok, _} = CommonAPI.delete(create_activity2.id, another_user) - - assert_receive {:text, received_event} - assert %{"event" => "delete", "payload" => _} = Jason.decode!(received_event) - - assert_receive {:text, received_event} - - assert %{"event" => "conversation", "payload" => received_payload} = - Jason.decode!(received_event) - - assert %{"last_status" => last_status} = Jason.decode!(received_payload) - assert last_status["id"] == to_string(create_activity.id) - end - end -end diff --git a/test/web/twitter_api/password_controller_test.exs b/test/web/twitter_api/password_controller_test.exs @@ -1,81 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.PasswordResetToken - alias Pleroma.User - alias Pleroma.Web.OAuth.Token - import Pleroma.Factory - - describe "GET /api/pleroma/password_reset/token" do - test "it returns error when token invalid", %{conn: conn} do - response = - conn - |> get("/api/pleroma/password_reset/token") - |> html_response(:ok) - - assert response =~ "<h2>Invalid Token</h2>" - end - - test "it shows password reset form", %{conn: conn} do - user = insert(:user) - {:ok, token} = PasswordResetToken.create_token(user) - - response = - conn - |> get("/api/pleroma/password_reset/#{token.token}") - |> html_response(:ok) - - assert response =~ "<h2>Password Reset for #{user.nickname}</h2>" - end - end - - describe "POST /api/pleroma/password_reset" do - test "it returns HTTP 200", %{conn: conn} do - user = insert(:user) - {:ok, token} = PasswordResetToken.create_token(user) - {:ok, _access_token} = Token.create(insert(:oauth_app), user, %{}) - - params = %{ - "password" => "test", - password_confirmation: "test", - token: token.token - } - - response = - conn - |> assign(:user, user) - |> post("/api/pleroma/password_reset", %{data: params}) - |> html_response(:ok) - - assert response =~ "<h2>Password changed!</h2>" - - user = refresh_record(user) - assert Pbkdf2.verify_pass("test", user.password_hash) - assert Enum.empty?(Token.get_user_tokens(user)) - end - - test "it sets password_reset_pending to false", %{conn: conn} do - user = insert(:user, password_reset_pending: true) - - {:ok, token} = PasswordResetToken.create_token(user) - {:ok, _access_token} = Token.create(insert(:oauth_app), user, %{}) - - params = %{ - "password" => "test", - password_confirmation: "test", - token: token.token - } - - conn - |> assign(:user, user) - |> post("/api/pleroma/password_reset", %{data: params}) - |> html_response(:ok) - - assert User.get_by_id(user.id).password_reset_pending == false - end - end -end diff --git a/test/web/twitter_api/remote_follow_controller_test.exs b/test/web/twitter_api/remote_follow_controller_test.exs @@ -1,350 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Config - alias Pleroma.MFA - alias Pleroma.MFA.TOTP - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import ExUnit.CaptureLog - import Pleroma.Factory - import Ecto.Query - - setup do - Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup_all do: clear_config([:instance, :federating], true) - setup do: clear_config([:instance]) - setup do: clear_config([:frontend_configurations, :pleroma_fe]) - setup do: clear_config([:user, :deny_follow_blocked]) - - describe "GET /ostatus_subscribe - remote_follow/2" do - test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} do - assert conn - |> get( - remote_follow_path(conn, :follow, %{ - acct: "https://mastodon.social/users/emelie/statuses/101849165031453009" - }) - ) - |> redirected_to() =~ "/notice/" - end - - test "show follow account page if the `acct` is a account link", %{conn: conn} do - response = - conn - |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) - |> html_response(200) - - assert response =~ "Log in to follow" - end - - test "show follow page if the `acct` is a account link", %{conn: conn} do - user = insert(:user) - - response = - conn - |> assign(:user, user) - |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) - |> html_response(200) - - assert response =~ "Remote follow" - end - - test "show follow page with error when user cannot fecth by `acct` link", %{conn: conn} do - user = insert(:user) - - assert capture_log(fn -> - response = - conn - |> assign(:user, user) - |> get( - remote_follow_path(conn, :follow, %{ - acct: "https://mastodon.social/users/not_found" - }) - ) - |> html_response(200) - - assert response =~ "Error fetching user" - end) =~ "Object has been deleted" - end - end - - describe "POST /ostatus_subscribe - do_follow/2 with assigned user " do - test "required `follow | write:follows` scope", %{conn: conn} do - user = insert(:user) - user2 = insert(:user) - read_token = insert(:oauth_token, user: user, scopes: ["read"]) - - assert capture_log(fn -> - response = - conn - |> assign(:user, user) - |> assign(:token, read_token) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) - |> response(200) - - assert response =~ "Error following account" - end) =~ "Insufficient permissions: follow | write:follows." - end - - test "follows user", %{conn: conn} do - user = insert(:user) - user2 = insert(:user) - - conn = - conn - |> assign(:user, user) - |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"])) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) - - assert redirected_to(conn) == "/users/#{user2.id}" - end - - test "returns error when user is deactivated", %{conn: conn} do - user = insert(:user, deactivated: true) - user2 = insert(:user) - - response = - conn - |> assign(:user, user) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) - |> response(200) - - assert response =~ "Error following account" - end - - test "returns error when user is blocked", %{conn: conn} do - Pleroma.Config.put([:user, :deny_follow_blocked], true) - user = insert(:user) - user2 = insert(:user) - - {:ok, _user_block} = Pleroma.User.block(user2, user) - - response = - conn - |> assign(:user, user) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) - |> response(200) - - assert response =~ "Error following account" - end - - test "returns error when followee not found", %{conn: conn} do - user = insert(:user) - - response = - conn - |> assign(:user, user) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => "jimm"}}) - |> response(200) - - assert response =~ "Error following account" - end - - test "returns success result when user already in followers", %{conn: conn} do - user = insert(:user) - user2 = insert(:user) - {:ok, _, _, _} = CommonAPI.follow(user, user2) - - conn = - conn - |> assign(:user, refresh_record(user)) - |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"])) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) - - assert redirected_to(conn) == "/users/#{user2.id}" - end - end - - describe "POST /ostatus_subscribe - follow/2 with enabled Two-Factor Auth " do - test "render the MFA login form", %{conn: conn} do - otp_secret = TOTP.generate_secret() - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - user2 = insert(:user) - - response = - conn - |> post(remote_follow_path(conn, :do_follow), %{ - "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} - }) - |> response(200) - - mfa_token = Pleroma.Repo.one(from(q in Pleroma.MFA.Token, where: q.user_id == ^user.id)) - - assert response =~ "Two-factor authentication" - assert response =~ "Authentication code" - assert response =~ mfa_token.token - refute user2.follower_address in User.following(user) - end - - test "returns error when password is incorrect", %{conn: conn} do - otp_secret = TOTP.generate_secret() - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - user2 = insert(:user) - - response = - conn - |> post(remote_follow_path(conn, :do_follow), %{ - "authorization" => %{"name" => user.nickname, "password" => "test1", "id" => user2.id} - }) - |> response(200) - - assert response =~ "Wrong username or password" - refute user2.follower_address in User.following(user) - end - - test "follows", %{conn: conn} do - otp_secret = TOTP.generate_secret() - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - {:ok, %{token: token}} = MFA.Token.create(user) - - user2 = insert(:user) - otp_token = TOTP.generate_token(otp_secret) - - conn = - conn - |> post( - remote_follow_path(conn, :do_follow), - %{ - "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id} - } - ) - - assert redirected_to(conn) == "/users/#{user2.id}" - assert user2.follower_address in User.following(user) - end - - test "returns error when auth code is incorrect", %{conn: conn} do - otp_secret = TOTP.generate_secret() - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - {:ok, %{token: token}} = MFA.Token.create(user) - - user2 = insert(:user) - otp_token = TOTP.generate_token(TOTP.generate_secret()) - - response = - conn - |> post( - remote_follow_path(conn, :do_follow), - %{ - "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id} - } - ) - |> response(200) - - assert response =~ "Wrong authentication code" - refute user2.follower_address in User.following(user) - end - end - - describe "POST /ostatus_subscribe - follow/2 without assigned user " do - test "follows", %{conn: conn} do - user = insert(:user) - user2 = insert(:user) - - conn = - conn - |> post(remote_follow_path(conn, :do_follow), %{ - "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} - }) - - assert redirected_to(conn) == "/users/#{user2.id}" - assert user2.follower_address in User.following(user) - end - - test "returns error when followee not found", %{conn: conn} do - user = insert(:user) - - response = - conn - |> post(remote_follow_path(conn, :do_follow), %{ - "authorization" => %{"name" => user.nickname, "password" => "test", "id" => "jimm"} - }) - |> response(200) - - assert response =~ "Error following account" - end - - test "returns error when login invalid", %{conn: conn} do - user = insert(:user) - - response = - conn - |> post(remote_follow_path(conn, :do_follow), %{ - "authorization" => %{"name" => "jimm", "password" => "test", "id" => user.id} - }) - |> response(200) - - assert response =~ "Wrong username or password" - end - - test "returns error when password invalid", %{conn: conn} do - user = insert(:user) - user2 = insert(:user) - - response = - conn - |> post(remote_follow_path(conn, :do_follow), %{ - "authorization" => %{"name" => user.nickname, "password" => "42", "id" => user2.id} - }) - |> response(200) - - assert response =~ "Wrong username or password" - end - - test "returns error when user is blocked", %{conn: conn} do - Pleroma.Config.put([:user, :deny_follow_blocked], true) - user = insert(:user) - user2 = insert(:user) - {:ok, _user_block} = Pleroma.User.block(user2, user) - - response = - conn - |> post(remote_follow_path(conn, :do_follow), %{ - "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} - }) - |> response(200) - - assert response =~ "Error following account" - end - end -end diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs @@ -1,138 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.TwitterAPI.ControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Builders.ActivityBuilder - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.OAuth.Token - - import Pleroma.Factory - - describe "POST /api/qvitter/statuses/notifications/read" do - test "without valid credentials", %{conn: conn} do - conn = post(conn, "/api/qvitter/statuses/notifications/read", %{"latest_id" => 1_234_567}) - assert json_response(conn, 403) == %{"error" => "Invalid credentials."} - end - - test "with credentials, without any params" do - %{conn: conn} = oauth_access(["write:notifications"]) - - conn = post(conn, "/api/qvitter/statuses/notifications/read") - - assert json_response(conn, 400) == %{ - "error" => "You need to specify latest_id", - "request" => "/api/qvitter/statuses/notifications/read" - } - end - - test "with credentials, with params" do - %{user: current_user, conn: conn} = - oauth_access(["read:notifications", "write:notifications"]) - - other_user = insert(:user) - - {:ok, _activity} = - ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: other_user}) - - response_conn = - conn - |> assign(:user, current_user) - |> get("/api/v1/notifications") - - [notification] = response = json_response(response_conn, 200) - - assert length(response) == 1 - - assert notification["pleroma"]["is_seen"] == false - - response_conn = - conn - |> assign(:user, current_user) - |> post("/api/qvitter/statuses/notifications/read", %{"latest_id" => notification["id"]}) - - [notification] = response = json_response(response_conn, 200) - - assert length(response) == 1 - - assert notification["pleroma"]["is_seen"] == true - end - end - - describe "GET /api/account/confirm_email/:id/:token" do - setup do - {:ok, user} = - insert(:user) - |> User.confirmation_changeset(need_confirmation: true) - |> Repo.update() - - assert user.confirmation_pending - - [user: user] - end - - test "it redirects to root url", %{conn: conn, user: user} do - conn = get(conn, "/api/account/confirm_email/#{user.id}/#{user.confirmation_token}") - - assert 302 == conn.status - end - - test "it confirms the user account", %{conn: conn, user: user} do - get(conn, "/api/account/confirm_email/#{user.id}/#{user.confirmation_token}") - - user = User.get_cached_by_id(user.id) - - refute user.confirmation_pending - refute user.confirmation_token - end - - test "it returns 500 if user cannot be found by id", %{conn: conn, user: user} do - conn = get(conn, "/api/account/confirm_email/0/#{user.confirmation_token}") - - assert 500 == conn.status - end - - test "it returns 500 if token is invalid", %{conn: conn, user: user} do - conn = get(conn, "/api/account/confirm_email/#{user.id}/wrong_token") - - assert 500 == conn.status - end - end - - describe "GET /api/oauth_tokens" do - setup do - token = insert(:oauth_token) |> Repo.preload(:user) - - %{token: token} - end - - test "renders list", %{token: token} do - response = - build_conn() - |> assign(:user, token.user) - |> get("/api/oauth_tokens") - - keys = - json_response(response, 200) - |> hd() - |> Map.keys() - - assert keys -- ["id", "app_name", "valid_until"] == [] - end - - test "revoke token", %{token: token} do - response = - build_conn() - |> assign(:user, token.user) - |> delete("/api/oauth_tokens/#{token.id}") - - tokens = Token.get_user_tokens(token.user) - - assert tokens == [] - assert response.status == 201 - end - end -end diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs @@ -1,432 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.UserInviteToken - alias Pleroma.Web.TwitterAPI.TwitterAPI - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "it registers a new user and returns the user." do - data = %{ - :username => "lain", - :email => "lain@wired.jp", - :fullname => "lain iwakura", - :password => "bear", - :confirm => "bear" - } - - {:ok, user} = TwitterAPI.register_user(data) - - assert user == User.get_cached_by_nickname("lain") - end - - test "it registers a new user with empty string in bio and returns the user" do - data = %{ - :username => "lain", - :email => "lain@wired.jp", - :fullname => "lain iwakura", - :bio => "", - :password => "bear", - :confirm => "bear" - } - - {:ok, user} = TwitterAPI.register_user(data) - - assert user == User.get_cached_by_nickname("lain") - end - - test "it sends confirmation email if :account_activation_required is specified in instance config" do - setting = Pleroma.Config.get([:instance, :account_activation_required]) - - unless setting do - Pleroma.Config.put([:instance, :account_activation_required], true) - on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end) - end - - data = %{ - :username => "lain", - :email => "lain@wired.jp", - :fullname => "lain iwakura", - :bio => "", - :password => "bear", - :confirm => "bear" - } - - {:ok, user} = TwitterAPI.register_user(data) - ObanHelpers.perform_all() - - assert user.confirmation_pending - - email = Pleroma.Emails.UserEmail.account_confirmation_email(user) - - notify_email = Pleroma.Config.get([:instance, :notify_email]) - instance_name = Pleroma.Config.get([:instance, :name]) - - Swoosh.TestAssertions.assert_email_sent( - from: {instance_name, notify_email}, - to: {user.name, user.email}, - html_body: email.html_body - ) - end - - test "it sends an admin email if :account_approval_required is specified in instance config" do - admin = insert(:user, is_admin: true) - setting = Pleroma.Config.get([:instance, :account_approval_required]) - - unless setting do - Pleroma.Config.put([:instance, :account_approval_required], true) - on_exit(fn -> Pleroma.Config.put([:instance, :account_approval_required], setting) end) - end - - data = %{ - :username => "lain", - :email => "lain@wired.jp", - :fullname => "lain iwakura", - :bio => "", - :password => "bear", - :confirm => "bear", - :reason => "I love anime" - } - - {:ok, user} = TwitterAPI.register_user(data) - ObanHelpers.perform_all() - - assert user.approval_pending - - email = Pleroma.Emails.AdminEmail.new_unapproved_registration(admin, user) - - notify_email = Pleroma.Config.get([:instance, :notify_email]) - instance_name = Pleroma.Config.get([:instance, :name]) - - Swoosh.TestAssertions.assert_email_sent( - from: {instance_name, notify_email}, - to: {admin.name, admin.email}, - html_body: email.html_body - ) - end - - test "it registers a new user and parses mentions in the bio" do - data1 = %{ - :username => "john", - :email => "john@gmail.com", - :fullname => "John Doe", - :bio => "test", - :password => "bear", - :confirm => "bear" - } - - {:ok, user1} = TwitterAPI.register_user(data1) - - data2 = %{ - :username => "lain", - :email => "lain@wired.jp", - :fullname => "lain iwakura", - :bio => "@john test", - :password => "bear", - :confirm => "bear" - } - - {:ok, user2} = TwitterAPI.register_user(data2) - - expected_text = - ~s(<span class="h-card"><a class="u-url mention" data-user="#{user1.id}" href="#{ - user1.ap_id - }" rel="ugc">@<span>john</span></a></span> test) - - assert user2.bio == expected_text - end - - describe "register with one time token" do - setup do: clear_config([:instance, :registrations_open], false) - - test "returns user on success" do - {:ok, invite} = UserInviteToken.create_invite() - - data = %{ - :username => "vinny", - :email => "pasta@pizza.vs", - :fullname => "Vinny Vinesauce", - :bio => "streamer", - :password => "hiptofbees", - :confirm => "hiptofbees", - :token => invite.token - } - - {:ok, user} = TwitterAPI.register_user(data) - - assert user == User.get_cached_by_nickname("vinny") - - invite = Repo.get_by(UserInviteToken, token: invite.token) - assert invite.used == true - end - - test "returns error on invalid token" do - data = %{ - :username => "GrimReaper", - :email => "death@reapers.afterlife", - :fullname => "Reaper Grim", - :bio => "Your time has come", - :password => "scythe", - :confirm => "scythe", - :token => "DudeLetMeInImAFairy" - } - - {:error, msg} = TwitterAPI.register_user(data) - - assert msg == "Invalid token" - refute User.get_cached_by_nickname("GrimReaper") - end - - test "returns error on expired token" do - {:ok, invite} = UserInviteToken.create_invite() - UserInviteToken.update_invite!(invite, used: true) - - data = %{ - :username => "GrimReaper", - :email => "death@reapers.afterlife", - :fullname => "Reaper Grim", - :bio => "Your time has come", - :password => "scythe", - :confirm => "scythe", - :token => invite.token - } - - {:error, msg} = TwitterAPI.register_user(data) - - assert msg == "Expired token" - refute User.get_cached_by_nickname("GrimReaper") - end - end - - describe "registers with date limited token" do - setup do: clear_config([:instance, :registrations_open], false) - - setup do - data = %{ - :username => "vinny", - :email => "pasta@pizza.vs", - :fullname => "Vinny Vinesauce", - :bio => "streamer", - :password => "hiptofbees", - :confirm => "hiptofbees" - } - - check_fn = fn invite -> - data = Map.put(data, :token, invite.token) - {:ok, user} = TwitterAPI.register_user(data) - - assert user == User.get_cached_by_nickname("vinny") - end - - {:ok, data: data, check_fn: check_fn} - end - - test "returns user on success", %{check_fn: check_fn} do - {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today()}) - - check_fn.(invite) - - invite = Repo.get_by(UserInviteToken, token: invite.token) - - refute invite.used - end - - test "returns user on token which expired tomorrow", %{check_fn: check_fn} do - {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), 1)}) - - check_fn.(invite) - - invite = Repo.get_by(UserInviteToken, token: invite.token) - - refute invite.used - end - - test "returns an error on overdue date", %{data: data} do - {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1)}) - - data = Map.put(data, "token", invite.token) - - {:error, msg} = TwitterAPI.register_user(data) - - assert msg == "Expired token" - refute User.get_cached_by_nickname("vinny") - invite = Repo.get_by(UserInviteToken, token: invite.token) - - refute invite.used - end - end - - describe "registers with reusable token" do - setup do: clear_config([:instance, :registrations_open], false) - - test "returns user on success, after him registration fails" do - {:ok, invite} = UserInviteToken.create_invite(%{max_use: 100}) - - UserInviteToken.update_invite!(invite, uses: 99) - - data = %{ - :username => "vinny", - :email => "pasta@pizza.vs", - :fullname => "Vinny Vinesauce", - :bio => "streamer", - :password => "hiptofbees", - :confirm => "hiptofbees", - :token => invite.token - } - - {:ok, user} = TwitterAPI.register_user(data) - assert user == User.get_cached_by_nickname("vinny") - - invite = Repo.get_by(UserInviteToken, token: invite.token) - assert invite.used == true - - data = %{ - :username => "GrimReaper", - :email => "death@reapers.afterlife", - :fullname => "Reaper Grim", - :bio => "Your time has come", - :password => "scythe", - :confirm => "scythe", - :token => invite.token - } - - {:error, msg} = TwitterAPI.register_user(data) - - assert msg == "Expired token" - refute User.get_cached_by_nickname("GrimReaper") - end - end - - describe "registers with reusable date limited token" do - setup do: clear_config([:instance, :registrations_open], false) - - test "returns user on success" do - {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100}) - - data = %{ - :username => "vinny", - :email => "pasta@pizza.vs", - :fullname => "Vinny Vinesauce", - :bio => "streamer", - :password => "hiptofbees", - :confirm => "hiptofbees", - :token => invite.token - } - - {:ok, user} = TwitterAPI.register_user(data) - assert user == User.get_cached_by_nickname("vinny") - - invite = Repo.get_by(UserInviteToken, token: invite.token) - refute invite.used - end - - test "error after max uses" do - {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100}) - - UserInviteToken.update_invite!(invite, uses: 99) - - data = %{ - :username => "vinny", - :email => "pasta@pizza.vs", - :fullname => "Vinny Vinesauce", - :bio => "streamer", - :password => "hiptofbees", - :confirm => "hiptofbees", - :token => invite.token - } - - {:ok, user} = TwitterAPI.register_user(data) - assert user == User.get_cached_by_nickname("vinny") - - invite = Repo.get_by(UserInviteToken, token: invite.token) - assert invite.used == true - - data = %{ - :username => "GrimReaper", - :email => "death@reapers.afterlife", - :fullname => "Reaper Grim", - :bio => "Your time has come", - :password => "scythe", - :confirm => "scythe", - :token => invite.token - } - - {:error, msg} = TwitterAPI.register_user(data) - - assert msg == "Expired token" - refute User.get_cached_by_nickname("GrimReaper") - end - - test "returns error on overdue date" do - {:ok, invite} = - UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100}) - - data = %{ - :username => "GrimReaper", - :email => "death@reapers.afterlife", - :fullname => "Reaper Grim", - :bio => "Your time has come", - :password => "scythe", - :confirm => "scythe", - :token => invite.token - } - - {:error, msg} = TwitterAPI.register_user(data) - - assert msg == "Expired token" - refute User.get_cached_by_nickname("GrimReaper") - end - - test "returns error on with overdue date and after max" do - {:ok, invite} = - UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100}) - - UserInviteToken.update_invite!(invite, uses: 100) - - data = %{ - :username => "GrimReaper", - :email => "death@reapers.afterlife", - :fullname => "Reaper Grim", - :bio => "Your time has come", - :password => "scythe", - :confirm => "scythe", - :token => invite.token - } - - {:error, msg} = TwitterAPI.register_user(data) - - assert msg == "Expired token" - refute User.get_cached_by_nickname("GrimReaper") - end - end - - test "it returns the error on registration problems" do - data = %{ - :username => "lain", - :email => "lain@wired.jp", - :fullname => "lain iwakura", - :bio => "close the world." - } - - {:error, error} = TwitterAPI.register_user(data) - - assert is_binary(error) - refute User.get_cached_by_nickname("lain") - end - - setup do - Supervisor.terminate_child(Pleroma.Supervisor, Cachex) - Supervisor.restart_child(Pleroma.Supervisor, Cachex) - :ok - end -end diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs @@ -1,437 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do - use Pleroma.Web.ConnCase - use Oban.Testing, repo: Pleroma.Repo - - alias Pleroma.Config - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - - import Pleroma.Factory - import Mock - - setup do - Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup do: clear_config([:instance]) - setup do: clear_config([:frontend_configurations, :pleroma_fe]) - - describe "PUT /api/pleroma/notification_settings" do - setup do: oauth_access(["write:accounts"]) - - test "it updates notification settings", %{user: user, conn: conn} do - conn - |> put("/api/pleroma/notification_settings", %{ - "block_from_strangers" => true, - "bar" => 1 - }) - |> json_response(:ok) - - user = refresh_record(user) - - assert %Pleroma.User.NotificationSetting{ - block_from_strangers: true, - hide_notification_contents: false - } == user.notification_settings - end - - test "it updates notification settings to enable hiding contents", %{user: user, conn: conn} do - conn - |> put("/api/pleroma/notification_settings", %{"hide_notification_contents" => "1"}) - |> json_response(:ok) - - user = refresh_record(user) - - assert %Pleroma.User.NotificationSetting{ - block_from_strangers: false, - hide_notification_contents: true - } == user.notification_settings - end - end - - describe "GET /api/pleroma/frontend_configurations" do - test "returns everything in :pleroma, :frontend_configurations", %{conn: conn} do - config = [ - frontend_a: %{ - x: 1, - y: 2 - }, - frontend_b: %{ - z: 3 - } - ] - - Config.put(:frontend_configurations, config) - - response = - conn - |> get("/api/pleroma/frontend_configurations") - |> json_response(:ok) - - assert response == Jason.encode!(config |> Enum.into(%{})) |> Jason.decode!() - end - end - - describe "/api/pleroma/emoji" do - test "returns json with custom emoji with tags", %{conn: conn} do - emoji = - conn - |> get("/api/pleroma/emoji") - |> json_response(200) - - assert Enum.all?(emoji, fn - {_key, - %{ - "image_url" => url, - "tags" => tags - }} -> - is_binary(url) and is_list(tags) - end) - end - end - - describe "GET /api/pleroma/healthcheck" do - setup do: clear_config([:instance, :healthcheck]) - - test "returns 503 when healthcheck disabled", %{conn: conn} do - Config.put([:instance, :healthcheck], false) - - response = - conn - |> get("/api/pleroma/healthcheck") - |> json_response(503) - - assert response == %{} - end - - test "returns 200 when healthcheck enabled and all ok", %{conn: conn} do - Config.put([:instance, :healthcheck], true) - - with_mock Pleroma.Healthcheck, - system_info: fn -> %Pleroma.Healthcheck{healthy: true} end do - response = - conn - |> get("/api/pleroma/healthcheck") - |> json_response(200) - - assert %{ - "active" => _, - "healthy" => true, - "idle" => _, - "memory_used" => _, - "pool_size" => _ - } = response - end - end - - test "returns 503 when healthcheck enabled and health is false", %{conn: conn} do - Config.put([:instance, :healthcheck], true) - - with_mock Pleroma.Healthcheck, - system_info: fn -> %Pleroma.Healthcheck{healthy: false} end do - response = - conn - |> get("/api/pleroma/healthcheck") - |> json_response(503) - - assert %{ - "active" => _, - "healthy" => false, - "idle" => _, - "memory_used" => _, - "pool_size" => _ - } = response - end - end - end - - describe "POST /api/pleroma/disable_account" do - setup do: oauth_access(["write:accounts"]) - - test "with valid permissions and password, it disables the account", %{conn: conn, user: user} do - response = - conn - |> post("/api/pleroma/disable_account", %{"password" => "test"}) - |> json_response(:ok) - - assert response == %{"status" => "success"} - ObanHelpers.perform_all() - - user = User.get_cached_by_id(user.id) - - assert user.deactivated == true - end - - test "with valid permissions and invalid password, it returns an error", %{conn: conn} do - user = insert(:user) - - response = - conn - |> post("/api/pleroma/disable_account", %{"password" => "test1"}) - |> json_response(:ok) - - assert response == %{"error" => "Invalid password."} - user = User.get_cached_by_id(user.id) - - refute user.deactivated - end - end - - describe "POST /main/ostatus - remote_subscribe/2" do - setup do: clear_config([:instance, :federating], true) - - test "renders subscribe form", %{conn: conn} do - user = insert(:user) - - response = - conn - |> post("/main/ostatus", %{"nickname" => user.nickname, "profile" => ""}) - |> response(:ok) - - refute response =~ "Could not find user" - assert response =~ "Remotely follow #{user.nickname}" - end - - test "renders subscribe form with error when user not found", %{conn: conn} do - response = - conn - |> post("/main/ostatus", %{"nickname" => "nickname", "profile" => ""}) - |> response(:ok) - - assert response =~ "Could not find user" - refute response =~ "Remotely follow" - end - - test "it redirect to webfinger url", %{conn: conn} do - user = insert(:user) - user2 = insert(:user, ap_id: "shp@social.heldscal.la") - - conn = - conn - |> post("/main/ostatus", %{ - "user" => %{"nickname" => user.nickname, "profile" => user2.ap_id} - }) - - assert redirected_to(conn) == - "https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}" - end - - test "it renders form with error when user not found", %{conn: conn} do - user2 = insert(:user, ap_id: "shp@social.heldscal.la") - - response = - conn - |> post("/main/ostatus", %{"user" => %{"nickname" => "jimm", "profile" => user2.ap_id}}) - |> response(:ok) - - assert response =~ "Something went wrong." - end - end - - test "it returns new captcha", %{conn: conn} do - with_mock Pleroma.Captcha, - new: fn -> "test_captcha" end do - resp = - conn - |> get("/api/pleroma/captcha") - |> response(200) - - assert resp == "\"test_captcha\"" - assert called(Pleroma.Captcha.new()) - end - end - - describe "POST /api/pleroma/change_email" do - setup do: oauth_access(["write:accounts"]) - - test "without permissions", %{conn: conn} do - conn = - conn - |> assign(:token, nil) - |> post("/api/pleroma/change_email") - - assert json_response(conn, 403) == %{"error" => "Insufficient permissions: write:accounts."} - end - - test "with proper permissions and invalid password", %{conn: conn} do - conn = - post(conn, "/api/pleroma/change_email", %{ - "password" => "hi", - "email" => "test@test.com" - }) - - assert json_response(conn, 200) == %{"error" => "Invalid password."} - end - - test "with proper permissions, valid password and invalid email", %{ - conn: conn - } do - conn = - post(conn, "/api/pleroma/change_email", %{ - "password" => "test", - "email" => "foobar" - }) - - assert json_response(conn, 200) == %{"error" => "Email has invalid format."} - end - - test "with proper permissions, valid password and no email", %{ - conn: conn - } do - conn = - post(conn, "/api/pleroma/change_email", %{ - "password" => "test" - }) - - assert json_response(conn, 200) == %{"error" => "Email can't be blank."} - end - - test "with proper permissions, valid password and blank email", %{ - conn: conn - } do - conn = - post(conn, "/api/pleroma/change_email", %{ - "password" => "test", - "email" => "" - }) - - assert json_response(conn, 200) == %{"error" => "Email can't be blank."} - end - - test "with proper permissions, valid password and non unique email", %{ - conn: conn - } do - user = insert(:user) - - conn = - post(conn, "/api/pleroma/change_email", %{ - "password" => "test", - "email" => user.email - }) - - assert json_response(conn, 200) == %{"error" => "Email has already been taken."} - end - - test "with proper permissions, valid password and valid email", %{ - conn: conn - } do - conn = - post(conn, "/api/pleroma/change_email", %{ - "password" => "test", - "email" => "cofe@foobar.com" - }) - - assert json_response(conn, 200) == %{"status" => "success"} - end - end - - describe "POST /api/pleroma/change_password" do - setup do: oauth_access(["write:accounts"]) - - test "without permissions", %{conn: conn} do - conn = - conn - |> assign(:token, nil) - |> post("/api/pleroma/change_password") - - assert json_response(conn, 403) == %{"error" => "Insufficient permissions: write:accounts."} - end - - test "with proper permissions and invalid password", %{conn: conn} do - conn = - post(conn, "/api/pleroma/change_password", %{ - "password" => "hi", - "new_password" => "newpass", - "new_password_confirmation" => "newpass" - }) - - assert json_response(conn, 200) == %{"error" => "Invalid password."} - end - - test "with proper permissions, valid password and new password and confirmation not matching", - %{ - conn: conn - } do - conn = - post(conn, "/api/pleroma/change_password", %{ - "password" => "test", - "new_password" => "newpass", - "new_password_confirmation" => "notnewpass" - }) - - assert json_response(conn, 200) == %{ - "error" => "New password does not match confirmation." - } - end - - test "with proper permissions, valid password and invalid new password", %{ - conn: conn - } do - conn = - post(conn, "/api/pleroma/change_password", %{ - "password" => "test", - "new_password" => "", - "new_password_confirmation" => "" - }) - - assert json_response(conn, 200) == %{ - "error" => "New password can't be blank." - } - end - - test "with proper permissions, valid password and matching new password and confirmation", %{ - conn: conn, - user: user - } do - conn = - post(conn, "/api/pleroma/change_password", %{ - "password" => "test", - "new_password" => "newpass", - "new_password_confirmation" => "newpass" - }) - - assert json_response(conn, 200) == %{"status" => "success"} - fetched_user = User.get_cached_by_id(user.id) - assert Pbkdf2.verify_pass("newpass", fetched_user.password_hash) == true - end - end - - describe "POST /api/pleroma/delete_account" do - setup do: oauth_access(["write:accounts"]) - - test "without permissions", %{conn: conn} do - conn = - conn - |> assign(:token, nil) - |> post("/api/pleroma/delete_account") - - assert json_response(conn, 403) == - %{"error" => "Insufficient permissions: write:accounts."} - end - - test "with proper permissions and wrong or missing password", %{conn: conn} do - for params <- [%{"password" => "hi"}, %{}] do - ret_conn = post(conn, "/api/pleroma/delete_account", params) - - assert json_response(ret_conn, 200) == %{"error" => "Invalid password."} - end - end - - test "with proper permissions and valid password", %{conn: conn, user: user} do - conn = post(conn, "/api/pleroma/delete_account", %{"password" => "test"}) - ObanHelpers.perform_all() - assert json_response(conn, 200) == %{"status" => "success"} - - user = User.get_by_id(user.id) - assert user.deactivated == true - assert user.name == nil - assert user.bio == "" - assert user.password_hash == nil - end - end -end diff --git a/test/web/uploader_controller_test.exs b/test/web/uploader_controller_test.exs @@ -1,43 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.UploaderControllerTest do - use Pleroma.Web.ConnCase - alias Pleroma.Uploaders.Uploader - - describe "callback/2" do - test "it returns 400 response when process callback isn't alive", %{conn: conn} do - res = - conn - |> post(uploader_path(conn, :callback, "test-path")) - - assert res.status == 400 - assert res.resp_body == "{\"error\":\"bad request\"}" - end - - test "it returns success result", %{conn: conn} do - task = - Task.async(fn -> - receive do - {Uploader, pid, conn, _params} -> - conn = - conn - |> put_status(:ok) - |> Phoenix.Controller.json(%{upload_path: "test-path"}) - - send(pid, {Uploader, conn}) - end - end) - - :global.register_name({Uploader, "test-path"}, task.pid) - - res = - conn - |> post(uploader_path(conn, :callback, "test-path")) - |> json_response(200) - - assert res == %{"upload_path" => "test-path"} - end - end -end diff --git a/test/web/views/error_view_test.exs b/test/web/views/error_view_test.exs @@ -1,36 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ErrorViewTest do - use Pleroma.Web.ConnCase, async: true - import ExUnit.CaptureLog - - # Bring render/3 and render_to_string/3 for testing custom views - import Phoenix.View - - test "renders 404.json" do - assert render(Pleroma.Web.ErrorView, "404.json", []) == %{errors: %{detail: "Page not found"}} - end - - test "render 500.json" do - assert capture_log(fn -> - assert render(Pleroma.Web.ErrorView, "500.json", []) == - %{errors: %{detail: "Internal server error", reason: "nil"}} - end) =~ "[error] Internal server error: nil" - end - - test "render any other" do - assert capture_log(fn -> - assert render(Pleroma.Web.ErrorView, "505.json", []) == - %{errors: %{detail: "Internal server error", reason: "nil"}} - end) =~ "[error] Internal server error: nil" - end - - test "render 500.json with reason" do - assert capture_log(fn -> - assert render(Pleroma.Web.ErrorView, "500.json", reason: "test reason") == - %{errors: %{detail: "Internal server error", reason: "\"test reason\""}} - end) =~ "[error] Internal server error: \"test reason\"" - end -end diff --git a/test/web/web_finger/web_finger_controller_test.exs b/test/web/web_finger/web_finger_controller_test.exs @@ -1,94 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do - use Pleroma.Web.ConnCase - - import ExUnit.CaptureLog - import Pleroma.Factory - import Tesla.Mock - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup_all do: clear_config([:instance, :federating], true) - - test "GET host-meta" do - response = - build_conn() - |> get("/.well-known/host-meta") - - 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.base_url() - }/.well-known/webfinger?resource={uri}" type="application/xrd+xml" /></XRD>) - end - - test "Webfinger JRD" do - user = insert(:user) - - response = - build_conn() - |> put_req_header("accept", "application/jrd+json") - |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") - - assert json_response(response, 200)["subject"] == "acct:#{user.nickname}@localhost" - end - - test "it returns 404 when user isn't found (JSON)" do - result = - build_conn() - |> put_req_header("accept", "application/jrd+json") - |> get("/.well-known/webfinger?resource=acct:jimm@localhost") - |> json_response(404) - - assert result == "Couldn't find user" - end - - test "Webfinger XML" do - user = insert(:user) - - response = - build_conn() - |> put_req_header("accept", "application/xrd+xml") - |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") - - assert response(response, 200) - end - - test "it returns 404 when user isn't found (XML)" do - result = - build_conn() - |> put_req_header("accept", "application/xrd+xml") - |> get("/.well-known/webfinger?resource=acct:jimm@localhost") - |> response(404) - - assert result == "Couldn't find user" - end - - test "Sends a 404 when invalid format" do - user = insert(:user) - - assert capture_log(fn -> - assert_raise Phoenix.NotAcceptableError, fn -> - build_conn() - |> put_req_header("accept", "text/html") - |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") - end - end) =~ "no supported media type in accept header" - end - - test "Sends a 400 when resource param is missing" do - response = - build_conn() - |> put_req_header("accept", "application/xrd+xml,application/jrd+json") - |> get("/.well-known/webfinger") - - assert response(response, 400) - end -end diff --git a/test/web/web_finger/web_finger_test.exs b/test/web/web_finger/web_finger_test.exs @@ -1,116 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.WebFingerTest do - use Pleroma.DataCase - alias Pleroma.Web.WebFinger - import Pleroma.Factory - import Tesla.Mock - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe "host meta" do - test "returns a link to the xml lrdd" do - host_info = WebFinger.host_meta() - - assert String.contains?(host_info, Pleroma.Web.base_url()) - end - end - - describe "incoming webfinger request" do - test "works for fqns" do - user = insert(:user) - - {:ok, result} = - WebFinger.webfinger("#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", "XML") - - assert is_binary(result) - end - - test "works for ap_ids" do - user = insert(:user) - - {:ok, result} = WebFinger.webfinger(user.ap_id, "XML") - assert is_binary(result) - end - end - - describe "fingering" do - test "returns error for nonsensical input" do - assert {:error, _} = WebFinger.finger("bliblablu") - assert {:error, _} = WebFinger.finger("pleroma.social") - end - - test "returns error when fails parse xml or json" do - user = "invalid_content@social.heldscal.la" - assert {:error, %Jason.DecodeError{}} = WebFinger.finger(user) - end - - test "returns the ActivityPub actor URI for an ActivityPub user" do - user = "framasoft@framatube.org" - - {:ok, _data} = WebFinger.finger(user) - end - - test "returns the ActivityPub actor URI 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" - end - - test "it work for AP-only user" do - user = "kpherox@mstdn.jp" - - {:ok, data} = WebFinger.finger(user) - - assert data["magic_key"] == nil - assert data["salmon"] == nil - - assert data["topic"] == nil - assert data["subject"] == "acct:kPherox@mstdn.jp" - assert data["ap_id"] == "https://mstdn.jp/users/kPherox" - 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") - - assert template == "https://social.heldscal.la/.well-known/webfinger?resource={uri}" - end - - test "it gets the xrd endpoint for hubzilla" do - {:ok, template} = WebFinger.find_lrdd_template("macgirvin.com") - - assert template == "https://macgirvin.com/xrd/?uri={uri}" - end - - 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}" - end - - test "it works with idna domains as nickname" do - nickname = "lain@" <> to_string(:idna.encode("zetsubou.みんな")) - - {:ok, _data} = WebFinger.finger(nickname) - end - - test "it works with idna domains as link" do - ap_id = "https://" <> to_string(:idna.encode("zetsubou.みんな")) <> "/users/lain" - {:ok, _data} = WebFinger.finger(ap_id) - end - end -end diff --git a/test/workers/cron/digest_emails_worker_test.exs b/test/workers/cron/digest_emails_worker_test.exs @@ -1,54 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.Cron.DigestEmailsWorkerTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - setup do: clear_config([:email_notifications, :digest]) - - setup do - Pleroma.Config.put([:email_notifications, :digest], %{ - active: true, - inactivity_threshold: 7, - interval: 7 - }) - - user = insert(:user) - - date = - Timex.now() - |> Timex.shift(days: -10) - |> Timex.to_naive_datetime() - - user2 = insert(:user, last_digest_emailed_at: date) - {:ok, _} = User.switch_email_notifications(user2, "digest", true) - CommonAPI.post(user, %{status: "hey @#{user2.nickname}!"}) - - {:ok, user2: user2} - end - - test "it sends digest emails", %{user2: user2} do - Pleroma.Workers.Cron.DigestEmailsWorker.perform(%Oban.Job{}) - # Performing job(s) enqueued at previous step - ObanHelpers.perform_all() - - assert_received {:email, email} - assert email.to == [{user2.name, user2.email}] - assert email.subject == "Your digest from #{Pleroma.Config.get(:instance)[:name]}" - end - - test "it doesn't fail when a user has no email", %{user2: user2} do - {:ok, _} = user2 |> Ecto.Changeset.change(%{email: nil}) |> Pleroma.Repo.update() - - Pleroma.Workers.Cron.DigestEmailsWorker.perform(%Oban.Job{}) - # Performing job(s) enqueued at previous step - ObanHelpers.perform_all() - end -end diff --git a/test/workers/cron/new_users_digest_worker_test.exs b/test/workers/cron/new_users_digest_worker_test.exs @@ -1,45 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.Cron.NewUsersDigestWorkerTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Tests.ObanHelpers - alias Pleroma.Web.CommonAPI - alias Pleroma.Workers.Cron.NewUsersDigestWorker - - test "it sends new users digest emails" do - yesterday = NaiveDateTime.utc_now() |> Timex.shift(days: -1) - admin = insert(:user, %{is_admin: true}) - user = insert(:user, %{inserted_at: yesterday}) - user2 = insert(:user, %{inserted_at: yesterday}) - CommonAPI.post(user, %{status: "cofe"}) - - NewUsersDigestWorker.perform(%Oban.Job{}) - ObanHelpers.perform_all() - - assert_received {:email, email} - assert email.to == [{admin.name, admin.email}] - assert email.subject == "#{Pleroma.Config.get([:instance, :name])} New Users" - - refute email.html_body =~ admin.nickname - assert email.html_body =~ user.nickname - assert email.html_body =~ user2.nickname - assert email.html_body =~ "cofe" - assert email.html_body =~ "#{Pleroma.Web.Endpoint.url()}/static/logo.png" - end - - test "it doesn't fail when admin has no email" do - yesterday = NaiveDateTime.utc_now() |> Timex.shift(days: -1) - insert(:user, %{is_admin: true, email: nil}) - insert(:user, %{inserted_at: yesterday}) - user = insert(:user, %{inserted_at: yesterday}) - - CommonAPI.post(user, %{status: "cofe"}) - - NewUsersDigestWorker.perform(%Oban.Job{}) - ObanHelpers.perform_all() - end -end diff --git a/test/workers/purge_expired_activity_test.exs b/test/workers/purge_expired_activity_test.exs @@ -1,59 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.PurgeExpiredActivityTest do - use Pleroma.DataCase, async: true - use Oban.Testing, repo: Pleroma.Repo - - import Pleroma.Factory - - alias Pleroma.Workers.PurgeExpiredActivity - - test "enqueue job" do - activity = insert(:note_activity) - - assert {:ok, _} = - PurgeExpiredActivity.enqueue(%{ - activity_id: activity.id, - expires_at: DateTime.add(DateTime.utc_now(), 3601) - }) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: activity.id} - ) - - assert {:ok, _} = - perform_job(Pleroma.Workers.PurgeExpiredActivity, %{activity_id: activity.id}) - - assert %Oban.Job{} = Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) - end - - test "error if user was not found" do - activity = insert(:note_activity) - - assert {:ok, _} = - PurgeExpiredActivity.enqueue(%{ - activity_id: activity.id, - expires_at: DateTime.add(DateTime.utc_now(), 3601) - }) - - user = Pleroma.User.get_by_ap_id(activity.actor) - Pleroma.Repo.delete(user) - - assert {:error, :user_not_found} = - perform_job(Pleroma.Workers.PurgeExpiredActivity, %{activity_id: activity.id}) - end - - test "error if actiivity was not found" do - assert {:ok, _} = - PurgeExpiredActivity.enqueue(%{ - activity_id: "some_id", - expires_at: DateTime.add(DateTime.utc_now(), 3601) - }) - - assert {:error, :activity_not_found} = - perform_job(Pleroma.Workers.PurgeExpiredActivity, %{activity_id: "some_if"}) - end -end diff --git a/test/workers/purge_expired_token_test.exs b/test/workers/purge_expired_token_test.exs @@ -1,51 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.PurgeExpiredTokenTest do - use Pleroma.DataCase, async: true - use Oban.Testing, repo: Pleroma.Repo - - import Pleroma.Factory - - setup do: clear_config([:oauth2, :clean_expired_tokens], true) - - test "purges expired oauth token" do - user = insert(:user) - app = insert(:oauth_app) - - {:ok, %{id: id}} = Pleroma.Web.OAuth.Token.create(app, user) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredToken, - args: %{token_id: id, mod: Pleroma.Web.OAuth.Token} - ) - - assert {:ok, %{id: ^id}} = - perform_job(Pleroma.Workers.PurgeExpiredToken, %{ - token_id: id, - mod: Pleroma.Web.OAuth.Token - }) - - assert Repo.aggregate(Pleroma.Web.OAuth.Token, :count, :id) == 0 - end - - test "purges expired mfa token" do - authorization = insert(:oauth_authorization) - - {:ok, %{id: id}} = Pleroma.MFA.Token.create(authorization.user, authorization) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredToken, - args: %{token_id: id, mod: Pleroma.MFA.Token} - ) - - assert {:ok, %{id: ^id}} = - perform_job(Pleroma.Workers.PurgeExpiredToken, %{ - token_id: id, - mod: Pleroma.MFA.Token - }) - - assert Repo.aggregate(Pleroma.MFA.Token, :count, :id) == 0 - end -end diff --git a/test/workers/scheduled_activity_worker_test.exs b/test/workers/scheduled_activity_worker_test.exs @@ -1,49 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.ScheduledActivityWorkerTest do - use Pleroma.DataCase - - alias Pleroma.ScheduledActivity - alias Pleroma.Workers.ScheduledActivityWorker - - import Pleroma.Factory - import ExUnit.CaptureLog - - setup do: clear_config([ScheduledActivity, :enabled]) - - test "creates a status from the scheduled activity" do - Pleroma.Config.put([ScheduledActivity, :enabled], true) - user = insert(:user) - - naive_datetime = - NaiveDateTime.add( - NaiveDateTime.utc_now(), - -:timer.minutes(2), - :millisecond - ) - - scheduled_activity = - insert( - :scheduled_activity, - scheduled_at: naive_datetime, - user: user, - params: %{status: "hi"} - ) - - ScheduledActivityWorker.perform(%Oban.Job{args: %{"activity_id" => scheduled_activity.id}}) - - refute Repo.get(ScheduledActivity, scheduled_activity.id) - activity = Repo.all(Pleroma.Activity) |> Enum.find(&(&1.actor == user.ap_id)) - assert Pleroma.Object.normalize(activity).data["content"] == "hi" - end - - test "adds log message if ScheduledActivity isn't find" do - Pleroma.Config.put([ScheduledActivity, :enabled], true) - - assert capture_log([level: :error], fn -> - ScheduledActivityWorker.perform(%Oban.Job{args: %{"activity_id" => 42}}) - end) =~ "Couldn't find scheduled activity" - end -end diff --git a/test/xml_builder_test.exs b/test/xml_builder_test.exs @@ -1,65 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.XmlBuilderTest do - use Pleroma.DataCase - alias Pleroma.XmlBuilder - - test "Build a basic xml string from a tuple" do - data = {:feed, %{xmlns: "http://www.w3.org/2005/Atom"}, "Some content"} - - expected_xml = "<feed xmlns=\"http://www.w3.org/2005/Atom\">Some content</feed>" - - assert XmlBuilder.to_xml(data) == expected_xml - end - - test "returns a complete document" do - data = {:feed, %{xmlns: "http://www.w3.org/2005/Atom"}, "Some content"} - - expected_xml = - "<?xml version=\"1.0\" encoding=\"UTF-8\"?><feed xmlns=\"http://www.w3.org/2005/Atom\">Some content</feed>" - - assert XmlBuilder.to_doc(data) == expected_xml - end - - test "Works without attributes" do - data = { - :feed, - "Some content" - } - - expected_xml = "<feed>Some content</feed>" - - assert XmlBuilder.to_xml(data) == expected_xml - end - - test "It works with nested tuples" do - data = { - :feed, - [ - {:guy, "brush"}, - {:lament, %{configuration: "puzzle"}, "pinhead"} - ] - } - - expected_xml = - ~s[<feed><guy>brush</guy><lament configuration="puzzle">pinhead</lament></feed>] - - assert XmlBuilder.to_xml(data) == expected_xml - end - - test "Represents NaiveDateTime as iso8601" do - assert XmlBuilder.to_xml(~N[2000-01-01 13:13:33]) == "2000-01-01T13:13:33" - end - - test "Uses self-closing tags when no content is giving" do - data = { - :link, - %{rel: "self"} - } - - expected_xml = ~s[<link rel="self" />] - assert XmlBuilder.to_xml(data) == expected_xml - end -end