aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/activity/ir/topics_test.exs8
-rw-r--r--test/activity_expiration_test.exs27
-rw-r--r--test/activity_test.exs7
-rw-r--r--test/bbs/handler_test.exs2
-rw-r--r--test/bookmark_test.exs2
-rw-r--r--test/captcha_test.exs92
-rw-r--r--test/config/config_db_test.exs713
-rw-r--r--test/config/holder_test.exs34
-rw-r--r--test/config/loader_test.exs29
-rw-r--r--test/config/transfer_task_test.exs172
-rw-r--r--test/config_test.exs2
-rw-r--r--test/conversation/participation_test.exs30
-rw-r--r--test/conversation_test.exs2
-rw-r--r--test/daemons/activity_expiration_daemon_test.exs17
-rw-r--r--test/daemons/scheduled_activity_daemon_test.exs19
-rw-r--r--test/docs/generator_test.exs230
-rw-r--r--test/earmark_renderer_test.exs79
-rw-r--r--test/emails/admin_email_test.exs6
-rw-r--r--test/emails/mailer_test.exs2
-rw-r--r--test/emails/user_email_test.exs2
-rw-r--r--test/emoji/formatter_test.exs2
-rw-r--r--test/emoji/loader_test.exs2
-rw-r--r--test/emoji_test.exs2
-rw-r--r--test/federation/federation_test.exs2
-rw-r--r--test/filter_test.exs2
-rw-r--r--test/fixtures/config/temp.secret.exs9
-rw-r--r--test/fixtures/emoji-reaction-no-emoji.json30
-rw-r--r--test/fixtures/emoji-reaction-too-long.json30
-rw-r--r--test/fixtures/emoji-reaction.json2
-rw-r--r--test/fixtures/margaret-corbin-grave-west-point.html2895
-rw-r--r--test/fixtures/mastodon-post-activity.json13
-rw-r--r--test/fixtures/modules/runtime_module.ex9
-rw-r--r--test/fixtures/nypd-facial-recognition-children-teenagers4.html228
-rw-r--r--test/fixtures/relay/accept-follow.json15
-rw-r--r--test/fixtures/relay/relay.json20
-rw-r--r--test/fixtures/tesla_mock/mobilizon.org-event.json1
-rw-r--r--test/fixtures/tesla_mock/mobilizon.org-user.json1
-rw-r--r--test/following_relationship_test.exs2
-rw-r--r--test/formatter_test.exs17
-rw-r--r--test/healthcheck_test.exs2
-rw-r--r--test/html_test.exs2
-rw-r--r--test/http/request_builder_test.exs3
-rw-r--r--test/http_test.exs2
-rw-r--r--test/integration/mastodon_websocket_test.exs2
-rw-r--r--test/job_queue_monitor_test.exs2
-rw-r--r--test/keys_test.exs2
-rw-r--r--test/list_test.exs2
-rw-r--r--test/marker_test.exs2
-rw-r--r--test/moderation_log_test.exs6
-rw-r--r--test/notification_test.exs68
-rw-r--r--test/object/containment_test.exs2
-rw-r--r--test/object/fetcher_test.exs36
-rw-r--r--test/object_test.exs186
-rw-r--r--test/pagination_test.exs2
-rw-r--r--test/plugs/admin_secret_authentication_plug_test.exs4
-rw-r--r--test/plugs/authentication_plug_test.exs2
-rw-r--r--test/plugs/basic_auth_decoder_plug_test.exs2
-rw-r--r--test/plugs/cache_control_test.exs4
-rw-r--r--test/plugs/cache_test.exs2
-rw-r--r--test/plugs/ensure_authenticated_plug_test.exs68
-rw-r--r--test/plugs/ensure_public_or_authenticated_plug_test.exs2
-rw-r--r--test/plugs/ensure_user_key_plug_test.exs2
-rw-r--r--test/plugs/http_security_plug_test.exs3
-rw-r--r--test/plugs/http_signature_plug_test.exs62
-rw-r--r--test/plugs/idempotency_plug_test.exs2
-rw-r--r--test/plugs/instance_static_test.exs2
-rw-r--r--test/plugs/legacy_authentication_plug_test.exs2
-rw-r--r--test/plugs/mapped_identity_to_signature_plug_test.exs2
-rw-r--r--test/plugs/oauth_plug_test.exs4
-rw-r--r--test/plugs/oauth_scopes_plug_test.exs209
-rw-r--r--test/plugs/rate_limiter_test.exs229
-rw-r--r--test/plugs/remote_ip_test.exs6
-rw-r--r--test/plugs/session_authentication_plug_test.exs2
-rw-r--r--test/plugs/set_format_plug_test.exs2
-rw-r--r--test/plugs/set_locale_plug_test.exs2
-rw-r--r--test/plugs/set_user_session_id_plug_test.exs2
-rw-r--r--test/plugs/uploaded_media_plug_test.exs2
-rw-r--r--test/plugs/user_enabled_plug_test.exs7
-rw-r--r--test/plugs/user_fetcher_plug_test.exs2
-rw-r--r--test/plugs/user_is_admin_plug_test.exs126
-rw-r--r--test/registration_test.exs2
-rw-r--r--test/repo_test.exs37
-rw-r--r--test/reverse_proxy_test.exs15
-rw-r--r--test/runtime_test.exs11
-rw-r--r--test/scheduled_activity_test.exs43
-rw-r--r--test/signature_test.exs2
-rw-r--r--test/stat_test.exs70
-rw-r--r--test/support/builders/user_builder.ex3
-rw-r--r--test/support/captcha_mock.ex15
-rw-r--r--test/support/channel_case.ex3
-rw-r--r--test/support/conn_case.ex46
-rw-r--r--test/support/data_case.ex2
-rw-r--r--test/support/factory.ex61
-rw-r--r--test/support/helpers.ex46
-rw-r--r--test/support/http_request_mock.ex30
-rw-r--r--test/support/mrf_module_mock.ex2
-rw-r--r--test/support/oban_helpers.ex6
-rw-r--r--test/support/web_push_http_client_mock.ex2
-rw-r--r--test/support/websocket_client.ex2
-rw-r--r--test/tasks/config_test.exs198
-rw-r--r--test/tasks/count_statuses_test.exs2
-rw-r--r--test/tasks/database_test.exs2
-rw-r--r--test/tasks/ecto/ecto_test.exs2
-rw-r--r--test/tasks/ecto/migrate_test.exs2
-rw-r--r--test/tasks/ecto/rollback_test.exs2
-rw-r--r--test/tasks/email_test.exs52
-rw-r--r--test/tasks/instance_test.exs8
-rw-r--r--test/tasks/pleroma_test.exs2
-rw-r--r--test/tasks/refresh_counter_cache_test.exs43
-rw-r--r--test/tasks/relay_test.exs5
-rw-r--r--test/tasks/robots_txt_test.exs2
-rw-r--r--test/tasks/uploads_test.exs2
-rw-r--r--test/tasks/user_test.exs2
-rw-r--r--test/test_helper.exs2
-rw-r--r--test/upload/filter/anonymize_filename_test.exs2
-rw-r--r--test/upload/filter/dedupe_test.exs2
-rw-r--r--test/upload/filter/mogrifun_test.exs2
-rw-r--r--test/upload/filter/mogrify_test.exs2
-rw-r--r--test/upload/filter_test.exs2
-rw-r--r--test/upload_test.exs2
-rw-r--r--test/uploaders/local_test.exs23
-rw-r--r--test/uploaders/mdii_test.exs50
-rw-r--r--test/uploaders/s3_test.exs9
-rw-r--r--test/user/notification_setting_test.exs21
-rw-r--r--test/user_invite_token_test.exs2
-rw-r--r--test/user_relationship_test.exs130
-rw-r--r--test/user_search_test.exs9
-rw-r--r--test/user_test.exs213
-rw-r--r--test/web/activity_pub/activity_pub_controller_test.exs301
-rw-r--r--test/web/activity_pub/activity_pub_test.exs409
-rw-r--r--test/web/activity_pub/mrf/hellthread_policy_test.exs4
-rw-r--r--test/web/activity_pub/mrf/keyword_policy_test.exs4
-rw-r--r--test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs2
-rw-r--r--test/web/activity_pub/mrf/mention_policy_test.exs4
-rw-r--r--test/web/activity_pub/mrf/subchain_policy_test.exs4
-rw-r--r--test/web/activity_pub/mrf/tag_policy_test.exs2
-rw-r--r--test/web/activity_pub/mrf/user_allowlist_policy_test.exs2
-rw-r--r--test/web/activity_pub/mrf/vocabulary_policy_test.exs2
-rw-r--r--test/web/activity_pub/publisher_test.exs27
-rw-r--r--test/web/activity_pub/relay_test.exs2
-rw-r--r--test/web/activity_pub/transmogrifier/follow_handling_test.exs8
-rw-r--r--test/web/activity_pub/transmogrifier_test.exs179
-rw-r--r--test/web/activity_pub/utils_test.exs113
-rw-r--r--test/web/activity_pub/views/object_view_test.exs22
-rw-r--r--test/web/activity_pub/views/user_view_test.exs4
-rw-r--r--test/web/activity_pub/visibilty_test.exs2
-rw-r--r--test/web/admin_api/admin_api_controller_test.exs1799
-rw-r--r--test/web/admin_api/config_test.exs497
-rw-r--r--test/web/admin_api/search_test.exs2
-rw-r--r--test/web/admin_api/views/report_view_test.exs4
-rw-r--r--test/web/auth/authenticator_test.exs2
-rw-r--r--test/web/chat_channel_test.exs37
-rw-r--r--test/web/common_api/common_api_test.exs92
-rw-r--r--test/web/common_api/common_api_utils_test.exs44
-rw-r--r--test/web/fallback_test.exs2
-rw-r--r--test/web/federator_test.exs2
-rw-r--r--test/web/feed/feed_controller_test.exs251
-rw-r--r--test/web/feed/tag_controller_test.exs154
-rw-r--r--test/web/feed/user_controller_test.exs137
-rw-r--r--test/web/instances/instance_test.exs2
-rw-r--r--test/web/instances/instances_test.exs2
-rw-r--r--test/web/masto_fe_controller_test.exs7
-rw-r--r--test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs222
-rw-r--r--test/web/mastodon_api/controllers/account_controller_test.exs506
-rw-r--r--test/web/mastodon_api/controllers/app_controller_test.exs2
-rw-r--r--test/web/mastodon_api/controllers/auth_controller_test.exs33
-rw-r--r--test/web/mastodon_api/controllers/conversation_controller_test.exs47
-rw-r--r--test/web/mastodon_api/controllers/custom_emoji_controller_test.exs2
-rw-r--r--test/web/mastodon_api/controllers/domain_block_controller_test.exs24
-rw-r--r--test/web/mastodon_api/controllers/filter_controller_test.exs44
-rw-r--r--test/web/mastodon_api/controllers/follow_request_controller_test.exs30
-rw-r--r--test/web/mastodon_api/controllers/instance_controller_test.exs2
-rw-r--r--test/web/mastodon_api/controllers/list_controller_test.exs90
-rw-r--r--test/web/mastodon_api/controllers/marker_controller_test.exs2
-rw-r--r--test/web/mastodon_api/controllers/media_controller_test.exs24
-rw-r--r--test/web/mastodon_api/controllers/notification_controller_test.exs350
-rw-r--r--test/web/mastodon_api/controllers/poll_controller_test.exs61
-rw-r--r--test/web/mastodon_api/controllers/report_controller_test.exs31
-rw-r--r--test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs100
-rw-r--r--test/web/mastodon_api/controllers/search_controller_test.exs68
-rw-r--r--test/web/mastodon_api/controllers/status_controller_test.exs502
-rw-r--r--test/web/mastodon_api/controllers/subscription_controller_test.exs2
-rw-r--r--test/web/mastodon_api/controllers/suggestion_controller_test.exs58
-rw-r--r--test/web/mastodon_api/controllers/timeline_controller_test.exs96
-rw-r--r--test/web/mastodon_api/mastodon_api_controller_test.exs76
-rw-r--r--test/web/mastodon_api/mastodon_api_test.exs2
-rw-r--r--test/web/mastodon_api/views/account_view_test.exs37
-rw-r--r--test/web/mastodon_api/views/conversation_view_test.exs2
-rw-r--r--test/web/mastodon_api/views/list_view_test.exs2
-rw-r--r--test/web/mastodon_api/views/marker_view_test.exs2
-rw-r--r--test/web/mastodon_api/views/notification_view_test.exs35
-rw-r--r--test/web/mastodon_api/views/poll_view_test.exs2
-rw-r--r--test/web/mastodon_api/views/push_subscription_view_test.exs2
-rw-r--r--test/web/mastodon_api/views/scheduled_activity_view_test.exs2
-rw-r--r--test/web/mastodon_api/views/status_view_test.exs53
-rw-r--r--test/web/media_proxy/media_proxy_controller_test.exs12
-rw-r--r--test/web/media_proxy/media_proxy_test.exs6
-rw-r--r--test/web/metadata/feed_test.exs2
-rw-r--r--test/web/metadata/opengraph_test.exs4
-rw-r--r--test/web/metadata/player_view_test.exs2
-rw-r--r--test/web/metadata/rel_me_test.exs2
-rw-r--r--test/web/metadata/twitter_card_test.exs33
-rw-r--r--test/web/metadata/utils_test.exs32
-rw-r--r--test/web/mongooseim/mongoose_im_controller_test.exs2
-rw-r--r--test/web/node_info_test.exs47
-rw-r--r--test/web/oauth/app_test.exs2
-rw-r--r--test/web/oauth/authorization_test.exs2
-rw-r--r--test/web/oauth/ldap_authorization_test.exs2
-rw-r--r--test/web/oauth/oauth_controller_test.exs131
-rw-r--r--test/web/oauth/token/utils_test.exs2
-rw-r--r--test/web/oauth/token_test.exs2
-rw-r--r--test/web/ostatus/ostatus_controller_test.exs89
-rw-r--r--test/web/pleroma_api/controllers/account_controller_test.exs158
-rw-r--r--test/web/pleroma_api/controllers/emoji_api_controller_test.exs49
-rw-r--r--test/web/pleroma_api/controllers/mascot_controller_test.exs41
-rw-r--r--test/web/pleroma_api/controllers/pleroma_api_controller_test.exs102
-rw-r--r--test/web/pleroma_api/controllers/scrobble_controller_test.exs19
-rw-r--r--test/web/plugs/federating_plug_test.exs5
-rw-r--r--test/web/push/impl_test.exs57
-rw-r--r--test/web/rel_me_test.exs2
-rw-r--r--test/web/rich_media/aws_signed_url_test.exs2
-rw-r--r--test/web/rich_media/helpers_test.exs2
-rw-r--r--test/web/rich_media/parser_test.exs2
-rw-r--r--test/web/rich_media/parsers/twitter_card_test.exs54
-rw-r--r--test/web/static_fe/static_fe_controller_test.exs142
-rw-r--r--test/web/streamer/ping_test.exs2
-rw-r--r--test/web/streamer/state_test.exs2
-rw-r--r--test/web/streamer/streamer_test.exs125
-rw-r--r--test/web/twitter_api/password_controller_test.exs4
-rw-r--r--test/web/twitter_api/remote_follow_controller_test.exs237
-rw-r--r--test/web/twitter_api/twitter_api_controller_test.exs142
-rw-r--r--test/web/twitter_api/twitter_api_test.exs46
-rw-r--r--test/web/twitter_api/util_controller_test.exs458
-rw-r--r--test/web/uploader_controller_test.exs2
-rw-r--r--test/web/views/error_view_test.exs2
-rw-r--r--test/web/web_finger/web_finger_controller_test.exs2
-rw-r--r--test/web/web_finger/web_finger_test.exs2
-rw-r--r--test/workers/cron/clear_oauth_token_worker_test.exs22
-rw-r--r--test/workers/cron/digest_emails_worker_test.exs (renamed from test/daemons/digest_email_daemon_test.exs)31
-rw-r--r--test/workers/cron/new_users_digest_worker_test.exs44
-rw-r--r--test/workers/cron/purge_expired_activities_worker_test.exs56
-rw-r--r--test/workers/scheduled_activity_worker_test.exs52
-rw-r--r--test/xml_builder_test.exs2
243 files changed, 11562 insertions, 4333 deletions
diff --git a/test/activity/ir/topics_test.exs b/test/activity/ir/topics_test.exs
index e75f83586..44aec1e19 100644
--- a/test/activity/ir/topics_test.exs
+++ b/test/activity/ir/topics_test.exs
@@ -59,8 +59,8 @@ defmodule Pleroma.Activity.Ir.TopicsTest do
describe "public visibility create events" do
setup do
activity = %Activity{
- object: %Object{data: %{"type" => "Create", "attachment" => []}},
- data: %{"to" => [Pleroma.Constants.as_public()]}
+ object: %Object{data: %{"attachment" => []}},
+ data: %{"type" => "Create", "to" => [Pleroma.Constants.as_public()]}
}
{:ok, activity: activity}
@@ -98,8 +98,8 @@ defmodule Pleroma.Activity.Ir.TopicsTest do
describe "public visibility create events with attachments" do
setup do
activity = %Activity{
- object: %Object{data: %{"type" => "Create", "attachment" => ["foo"]}},
- data: %{"to" => [Pleroma.Constants.as_public()]}
+ object: %Object{data: %{"attachment" => ["foo"]}},
+ data: %{"type" => "Create", "to" => [Pleroma.Constants.as_public()]}
}
{:ok, activity: activity}
diff --git a/test/activity_expiration_test.exs b/test/activity_expiration_test.exs
index 4948fae16..4cda5e985 100644
--- a/test/activity_expiration_test.exs
+++ b/test/activity_expiration_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ActivityExpirationTest do
@@ -7,6 +7,8 @@ defmodule Pleroma.ActivityExpirationTest do
alias Pleroma.ActivityExpiration
import Pleroma.Factory
+ clear_config([ActivityExpiration, :enabled])
+
test "finds activities due to be deleted only" do
activity = insert(:note_activity)
expiration_due = insert(:expiration_in_the_past, %{activity_id: activity.id})
@@ -24,4 +26,27 @@ defmodule Pleroma.ActivityExpirationTest do
now = NaiveDateTime.utc_now()
assert {:error, _} = ActivityExpiration.create(activity, now)
end
+
+ test "deletes an expiration activity" do
+ Pleroma.Config.put([ActivityExpiration, :enabled], true)
+ activity = insert(:note_activity)
+
+ naive_datetime =
+ NaiveDateTime.add(
+ NaiveDateTime.utc_now(),
+ -:timer.minutes(2),
+ :millisecond
+ )
+
+ expiration =
+ insert(
+ :expiration_in_the_past,
+ %{activity_id: activity.id, scheduled_at: naive_datetime}
+ )
+
+ Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker.perform(:ops, :pid)
+
+ refute Pleroma.Repo.get(Pleroma.Activity, activity.id)
+ refute Pleroma.Repo.get(Pleroma.ActivityExpiration, expiration.id)
+ end
end
diff --git a/test/activity_test.exs b/test/activity_test.exs
index e7ea2bd5e..46b55beaa 100644
--- a/test/activity_test.exs
+++ b/test/activity_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ActivityTest do
@@ -138,6 +138,8 @@ defmodule Pleroma.ActivityTest do
}
end
+ clear_config([:instance, :limit_to_local_content])
+
test "finds utf8 text in statuses", %{
japanese_activity: japanese_activity,
user: user
@@ -165,7 +167,6 @@ defmodule Pleroma.ActivityTest do
%{local_activity: local_activity} do
Pleroma.Config.put([:instance, :limit_to_local_content], :all)
assert [^local_activity] = Activity.search(nil, "find me")
- Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
end
test "find all statuses for unauthenticated users when `limit_to_local_content` is `false`",
@@ -178,8 +179,6 @@ defmodule Pleroma.ActivityTest do
activities = Enum.sort_by(Activity.search(nil, "find me"), & &1.id)
assert [^local_activity, ^remote_activity] = activities
-
- Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
end
end
diff --git a/test/bbs/handler_test.exs b/test/bbs/handler_test.exs
index 4f0c13417..74982547b 100644
--- a/test/bbs/handler_test.exs
+++ b/test/bbs/handler_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.BBS.HandlerTest do
diff --git a/test/bookmark_test.exs b/test/bookmark_test.exs
index e54bd359c..021f79322 100644
--- a/test/bookmark_test.exs
+++ b/test/bookmark_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.BookmarkTest do
diff --git a/test/captcha_test.exs b/test/captcha_test.exs
index 9f395d6b4..5e29b48b0 100644
--- a/test/captcha_test.exs
+++ b/test/captcha_test.exs
@@ -1,16 +1,20 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.CaptchaTest do
- use ExUnit.Case
+ use Pleroma.DataCase
import Tesla.Mock
+ alias Pleroma.Captcha
alias Pleroma.Captcha.Kocaptcha
+ alias Pleroma.Captcha.Native
@ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}]
+ clear_config([Pleroma.Captcha, :enabled])
+
describe "Kocaptcha" do
setup do
ets_name = Kocaptcha.Ets
@@ -30,17 +34,83 @@ defmodule Pleroma.CaptchaTest do
test "new and validate" do
new = Kocaptcha.new()
- assert new[:type] == :kocaptcha
- assert new[:token] == "afa1815e14e29355e6c8f6b143a39fa2"
- assert new[:url] ==
- "https://captcha.kotobank.ch/captchas/afa1815e14e29355e6c8f6b143a39fa2.png"
+ token = "afa1815e14e29355e6c8f6b143a39fa2"
+ url = "https://captcha.kotobank.ch/captchas/afa1815e14e29355e6c8f6b143a39fa2.png"
+
+ assert %{
+ answer_data: answer,
+ token: ^token,
+ url: ^url,
+ type: :kocaptcha
+ } = new
+
+ assert Kocaptcha.validate(token, "7oEy8c", answer) == :ok
+ end
+ end
+
+ describe "Native" do
+ test "new and validate" do
+ new = Native.new()
+
+ assert %{
+ answer_data: answer,
+ token: token,
+ type: :native,
+ url: "data:image/png;base64," <> _
+ } = new
+
+ assert is_binary(answer)
+ assert :ok = Native.validate(token, answer, answer)
+ assert {:error, "Invalid CAPTCHA"} == Native.validate(token, answer, answer <> "foobar")
+ end
+ end
+
+ describe "Captcha Wrapper" do
+ test "validate" do
+ Pleroma.Config.put([Pleroma.Captcha, :enabled], true)
+
+ new = Captcha.new()
+
+ assert %{
+ answer_data: answer,
+ token: token
+ } = new
+
+ assert is_binary(answer)
+ assert :ok = Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer)
+ end
+
+ test "doesn't validate invalid answer" do
+ Pleroma.Config.put([Pleroma.Captcha, :enabled], true)
+
+ new = Captcha.new()
+
+ assert %{
+ answer_data: answer,
+ token: token
+ } = new
+
+ assert is_binary(answer)
+
+ assert {:error, "Invalid answer data"} =
+ Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer <> "foobar")
+ end
+
+ test "nil answer_data" do
+ Pleroma.Config.put([Pleroma.Captcha, :enabled], true)
+
+ new = Captcha.new()
+
+ assert %{
+ answer_data: answer,
+ token: token
+ } = new
+
+ assert is_binary(answer)
- assert Kocaptcha.validate(
- new[:token],
- "7oEy8c",
- new[:answer_data]
- ) == :ok
+ assert {:error, "Invalid answer data"} =
+ Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", nil)
end
end
end
diff --git a/test/config/config_db_test.exs b/test/config/config_db_test.exs
new file mode 100644
index 000000000..ac3dde681
--- /dev/null
+++ b/test/config/config_db_test.exs
@@ -0,0 +1,713 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ConfigDBTest do
+ use Pleroma.DataCase, async: true
+ import Pleroma.Factory
+ alias Pleroma.ConfigDB
+
+ test "get_by_key/1" do
+ config = insert(:config)
+ insert(:config)
+
+ assert config == ConfigDB.get_by_params(%{group: config.group, key: config.key})
+ end
+
+ test "create/1" do
+ {:ok, config} = ConfigDB.create(%{group: ":pleroma", key: ":some_key", value: "some_value"})
+ assert config == ConfigDB.get_by_params(%{group: ":pleroma", key: ":some_key"})
+ end
+
+ test "update/1" do
+ config = insert(:config)
+ {:ok, updated} = ConfigDB.update(config, %{value: "some_value"})
+ loaded = ConfigDB.get_by_params(%{group: config.group, key: config.key})
+ assert loaded == updated
+ end
+
+ test "get_all_as_keyword/0" do
+ saved = insert(:config)
+ insert(:config, group: ":quack", key: ":level", value: ConfigDB.to_binary(:info))
+ insert(:config, group: ":quack", key: ":meta", value: ConfigDB.to_binary([:none]))
+
+ insert(:config,
+ group: ":quack",
+ key: ":webhook_url",
+ value: ConfigDB.to_binary("https://hooks.slack.com/services/KEY/some_val")
+ )
+
+ config = ConfigDB.get_all_as_keyword()
+
+ assert config[:pleroma] == [
+ {ConfigDB.from_string(saved.key), ConfigDB.from_binary(saved.value)}
+ ]
+
+ assert config[:quack] == [
+ level: :info,
+ meta: [:none],
+ webhook_url: "https://hooks.slack.com/services/KEY/some_val"
+ ]
+ end
+
+ describe "update_or_create/1" do
+ test "common" do
+ config = insert(:config)
+ key2 = "another_key"
+
+ params = [
+ %{group: "pleroma", key: key2, value: "another_value"},
+ %{group: config.group, key: config.key, value: "new_value"}
+ ]
+
+ assert Repo.all(ConfigDB) |> length() == 1
+
+ Enum.each(params, &ConfigDB.update_or_create(&1))
+
+ assert Repo.all(ConfigDB) |> length() == 2
+
+ config1 = ConfigDB.get_by_params(%{group: config.group, key: config.key})
+ config2 = ConfigDB.get_by_params(%{group: "pleroma", key: key2})
+
+ assert config1.value == ConfigDB.transform("new_value")
+ assert config2.value == ConfigDB.transform("another_value")
+ end
+
+ test "partial update" do
+ config = insert(:config, value: ConfigDB.to_binary(key1: "val1", key2: :val2))
+
+ {:ok, _config} =
+ ConfigDB.update_or_create(%{
+ group: config.group,
+ key: config.key,
+ value: [key1: :val1, key3: :val3]
+ })
+
+ updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})
+
+ value = ConfigDB.from_binary(updated.value)
+ assert length(value) == 3
+ assert value[:key1] == :val1
+ assert value[:key2] == :val2
+ assert value[:key3] == :val3
+ end
+
+ test "deep merge" do
+ config = insert(:config, value: ConfigDB.to_binary(key1: "val1", key2: [k1: :v1, k2: "v2"]))
+
+ {:ok, config} =
+ ConfigDB.update_or_create(%{
+ group: config.group,
+ key: config.key,
+ value: [key1: :val1, key2: [k2: :v2, k3: :v3], key3: :val3]
+ })
+
+ updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})
+
+ assert config.value == updated.value
+
+ value = ConfigDB.from_binary(updated.value)
+ assert value[:key1] == :val1
+ assert value[:key2] == [k1: :v1, k2: :v2, k3: :v3]
+ assert value[:key3] == :val3
+ end
+
+ test "only full update for some keys" do
+ config1 = insert(:config, key: ":ecto_repos", value: ConfigDB.to_binary(repo: Pleroma.Repo))
+
+ config2 =
+ insert(:config, group: ":cors_plug", key: ":max_age", value: ConfigDB.to_binary(18))
+
+ {:ok, _config} =
+ ConfigDB.update_or_create(%{
+ group: config1.group,
+ key: config1.key,
+ value: [another_repo: [Pleroma.Repo]]
+ })
+
+ {:ok, _config} =
+ ConfigDB.update_or_create(%{
+ group: config2.group,
+ key: config2.key,
+ value: 777
+ })
+
+ updated1 = ConfigDB.get_by_params(%{group: config1.group, key: config1.key})
+ updated2 = ConfigDB.get_by_params(%{group: config2.group, key: config2.key})
+
+ assert ConfigDB.from_binary(updated1.value) == [another_repo: [Pleroma.Repo]]
+ assert ConfigDB.from_binary(updated2.value) == 777
+ end
+
+ test "full update if value is not keyword" do
+ config =
+ insert(:config,
+ group: ":tesla",
+ key: ":adapter",
+ value: ConfigDB.to_binary(Tesla.Adapter.Hackney)
+ )
+
+ {:ok, _config} =
+ ConfigDB.update_or_create(%{
+ group: config.group,
+ key: config.key,
+ value: Tesla.Adapter.Httpc
+ })
+
+ updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})
+
+ assert ConfigDB.from_binary(updated.value) == Tesla.Adapter.Httpc
+ end
+
+ test "only full update for some subkeys" do
+ config1 =
+ insert(:config,
+ key: ":emoji",
+ value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1])
+ )
+
+ config2 =
+ insert(:config,
+ key: ":assets",
+ value: ConfigDB.to_binary(mascots: [a: 1, b: 2], key: [a: 1])
+ )
+
+ {:ok, _config} =
+ ConfigDB.update_or_create(%{
+ group: config1.group,
+ key: config1.key,
+ value: [groups: [c: 3, d: 4], key: [b: 2]]
+ })
+
+ {:ok, _config} =
+ ConfigDB.update_or_create(%{
+ group: config2.group,
+ key: config2.key,
+ value: [mascots: [c: 3, d: 4], key: [b: 2]]
+ })
+
+ updated1 = ConfigDB.get_by_params(%{group: config1.group, key: config1.key})
+ updated2 = ConfigDB.get_by_params(%{group: config2.group, key: config2.key})
+
+ assert ConfigDB.from_binary(updated1.value) == [groups: [c: 3, d: 4], key: [a: 1, b: 2]]
+ assert ConfigDB.from_binary(updated2.value) == [mascots: [c: 3, d: 4], key: [a: 1, b: 2]]
+ end
+ end
+
+ describe "delete/1" do
+ test "error on deleting non existing setting" do
+ {:error, error} = ConfigDB.delete(%{group: ":pleroma", key: ":key"})
+ assert error =~ "Config with params %{group: \":pleroma\", key: \":key\"} not found"
+ end
+
+ test "full delete" do
+ config = insert(:config)
+ {:ok, deleted} = ConfigDB.delete(%{group: config.group, key: config.key})
+ assert Ecto.get_meta(deleted, :state) == :deleted
+ refute ConfigDB.get_by_params(%{group: config.group, key: config.key})
+ end
+
+ test "partial subkeys delete" do
+ config = insert(:config, value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1]))
+
+ {:ok, deleted} =
+ ConfigDB.delete(%{group: config.group, key: config.key, subkeys: [":groups"]})
+
+ assert Ecto.get_meta(deleted, :state) == :loaded
+
+ assert deleted.value == ConfigDB.to_binary(key: [a: 1])
+
+ updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})
+
+ assert updated.value == deleted.value
+ end
+
+ test "full delete if remaining value after subkeys deletion is empty list" do
+ config = insert(:config, value: ConfigDB.to_binary(groups: [a: 1, b: 2]))
+
+ {:ok, deleted} =
+ ConfigDB.delete(%{group: config.group, key: config.key, subkeys: [":groups"]})
+
+ assert Ecto.get_meta(deleted, :state) == :deleted
+
+ refute ConfigDB.get_by_params(%{group: config.group, key: config.key})
+ end
+ end
+
+ describe "transform/1" do
+ test "string" do
+ binary = ConfigDB.transform("value as string")
+ assert binary == :erlang.term_to_binary("value as string")
+ assert ConfigDB.from_binary(binary) == "value as string"
+ end
+
+ test "boolean" do
+ binary = ConfigDB.transform(false)
+ assert binary == :erlang.term_to_binary(false)
+ assert ConfigDB.from_binary(binary) == false
+ end
+
+ test "nil" do
+ binary = ConfigDB.transform(nil)
+ assert binary == :erlang.term_to_binary(nil)
+ assert ConfigDB.from_binary(binary) == nil
+ end
+
+ test "integer" do
+ binary = ConfigDB.transform(150)
+ assert binary == :erlang.term_to_binary(150)
+ assert ConfigDB.from_binary(binary) == 150
+ end
+
+ test "atom" do
+ binary = ConfigDB.transform(":atom")
+ assert binary == :erlang.term_to_binary(:atom)
+ assert ConfigDB.from_binary(binary) == :atom
+ end
+
+ test "ssl options" do
+ binary = ConfigDB.transform([":tlsv1", ":tlsv1.1", ":tlsv1.2"])
+ assert binary == :erlang.term_to_binary([:tlsv1, :"tlsv1.1", :"tlsv1.2"])
+ assert ConfigDB.from_binary(binary) == [:tlsv1, :"tlsv1.1", :"tlsv1.2"]
+ end
+
+ test "pleroma module" do
+ binary = ConfigDB.transform("Pleroma.Bookmark")
+ assert binary == :erlang.term_to_binary(Pleroma.Bookmark)
+ assert ConfigDB.from_binary(binary) == Pleroma.Bookmark
+ end
+
+ test "pleroma string" do
+ binary = ConfigDB.transform("Pleroma")
+ assert binary == :erlang.term_to_binary("Pleroma")
+ assert ConfigDB.from_binary(binary) == "Pleroma"
+ end
+
+ test "phoenix module" do
+ binary = ConfigDB.transform("Phoenix.Socket.V1.JSONSerializer")
+ assert binary == :erlang.term_to_binary(Phoenix.Socket.V1.JSONSerializer)
+ assert ConfigDB.from_binary(binary) == Phoenix.Socket.V1.JSONSerializer
+ end
+
+ test "tesla module" do
+ binary = ConfigDB.transform("Tesla.Adapter.Hackney")
+ assert binary == :erlang.term_to_binary(Tesla.Adapter.Hackney)
+ assert ConfigDB.from_binary(binary) == Tesla.Adapter.Hackney
+ end
+
+ test "ExSyslogger module" do
+ binary = ConfigDB.transform("ExSyslogger")
+ assert binary == :erlang.term_to_binary(ExSyslogger)
+ assert ConfigDB.from_binary(binary) == ExSyslogger
+ end
+
+ test "Quack.Logger module" do
+ binary = ConfigDB.transform("Quack.Logger")
+ assert binary == :erlang.term_to_binary(Quack.Logger)
+ assert ConfigDB.from_binary(binary) == Quack.Logger
+ end
+
+ test "Swoosh.Adapters modules" do
+ binary = ConfigDB.transform("Swoosh.Adapters.SMTP")
+ assert binary == :erlang.term_to_binary(Swoosh.Adapters.SMTP)
+ assert ConfigDB.from_binary(binary) == Swoosh.Adapters.SMTP
+ binary = ConfigDB.transform("Swoosh.Adapters.AmazonSES")
+ assert binary == :erlang.term_to_binary(Swoosh.Adapters.AmazonSES)
+ assert ConfigDB.from_binary(binary) == Swoosh.Adapters.AmazonSES
+ end
+
+ test "sigil" do
+ binary = ConfigDB.transform("~r[comp[lL][aA][iI][nN]er]")
+ assert binary == :erlang.term_to_binary(~r/comp[lL][aA][iI][nN]er/)
+ assert ConfigDB.from_binary(binary) == ~r/comp[lL][aA][iI][nN]er/
+ end
+
+ test "link sigil" do
+ binary = ConfigDB.transform("~r/https:\/\/example.com/")
+ assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/)
+ assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/
+ end
+
+ test "link sigil with um modifiers" do
+ binary = ConfigDB.transform("~r/https:\/\/example.com/um")
+ assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/um)
+ assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/um
+ end
+
+ test "link sigil with i modifier" do
+ binary = ConfigDB.transform("~r/https:\/\/example.com/i")
+ assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/i)
+ assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/i
+ end
+
+ test "link sigil with s modifier" do
+ binary = ConfigDB.transform("~r/https:\/\/example.com/s")
+ assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/s)
+ assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/s
+ end
+
+ test "raise if valid delimiter not found" do
+ assert_raise ArgumentError, "valid delimiter for Regex expression not found", fn ->
+ ConfigDB.transform("~r/https://[]{}<>\"'()|example.com/s")
+ end
+ end
+
+ test "2 child tuple" do
+ binary = ConfigDB.transform(%{"tuple" => ["v1", ":v2"]})
+ assert binary == :erlang.term_to_binary({"v1", :v2})
+ assert ConfigDB.from_binary(binary) == {"v1", :v2}
+ end
+
+ test "proxy tuple with localhost" do
+ binary =
+ ConfigDB.transform(%{
+ "tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]
+ })
+
+ assert binary == :erlang.term_to_binary({:proxy_url, {:socks5, :localhost, 1234}})
+ assert ConfigDB.from_binary(binary) == {:proxy_url, {:socks5, :localhost, 1234}}
+ end
+
+ test "proxy tuple with domain" do
+ binary =
+ ConfigDB.transform(%{
+ "tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]
+ })
+
+ assert binary == :erlang.term_to_binary({:proxy_url, {:socks5, 'domain.com', 1234}})
+ assert ConfigDB.from_binary(binary) == {:proxy_url, {:socks5, 'domain.com', 1234}}
+ end
+
+ test "proxy tuple with ip" do
+ binary =
+ ConfigDB.transform(%{
+ "tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]
+ })
+
+ assert binary == :erlang.term_to_binary({:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}})
+ assert ConfigDB.from_binary(binary) == {:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}}
+ end
+
+ test "tuple with n childs" do
+ binary =
+ ConfigDB.transform(%{
+ "tuple" => [
+ "v1",
+ ":v2",
+ "Pleroma.Bookmark",
+ 150,
+ false,
+ "Phoenix.Socket.V1.JSONSerializer"
+ ]
+ })
+
+ assert binary ==
+ :erlang.term_to_binary(
+ {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer}
+ )
+
+ assert ConfigDB.from_binary(binary) ==
+ {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer}
+ end
+
+ test "map with string key" do
+ binary = ConfigDB.transform(%{"key" => "value"})
+ assert binary == :erlang.term_to_binary(%{"key" => "value"})
+ assert ConfigDB.from_binary(binary) == %{"key" => "value"}
+ end
+
+ test "map with atom key" do
+ binary = ConfigDB.transform(%{":key" => "value"})
+ assert binary == :erlang.term_to_binary(%{key: "value"})
+ assert ConfigDB.from_binary(binary) == %{key: "value"}
+ end
+
+ test "list of strings" do
+ binary = ConfigDB.transform(["v1", "v2", "v3"])
+ assert binary == :erlang.term_to_binary(["v1", "v2", "v3"])
+ assert ConfigDB.from_binary(binary) == ["v1", "v2", "v3"]
+ end
+
+ test "list of modules" do
+ binary = ConfigDB.transform(["Pleroma.Repo", "Pleroma.Activity"])
+ assert binary == :erlang.term_to_binary([Pleroma.Repo, Pleroma.Activity])
+ assert ConfigDB.from_binary(binary) == [Pleroma.Repo, Pleroma.Activity]
+ end
+
+ test "list of atoms" do
+ binary = ConfigDB.transform([":v1", ":v2", ":v3"])
+ assert binary == :erlang.term_to_binary([:v1, :v2, :v3])
+ assert ConfigDB.from_binary(binary) == [:v1, :v2, :v3]
+ end
+
+ test "list of mixed values" do
+ binary =
+ ConfigDB.transform([
+ "v1",
+ ":v2",
+ "Pleroma.Repo",
+ "Phoenix.Socket.V1.JSONSerializer",
+ 15,
+ false
+ ])
+
+ assert binary ==
+ :erlang.term_to_binary([
+ "v1",
+ :v2,
+ Pleroma.Repo,
+ Phoenix.Socket.V1.JSONSerializer,
+ 15,
+ false
+ ])
+
+ assert ConfigDB.from_binary(binary) == [
+ "v1",
+ :v2,
+ Pleroma.Repo,
+ Phoenix.Socket.V1.JSONSerializer,
+ 15,
+ false
+ ]
+ end
+
+ test "simple keyword" do
+ binary = ConfigDB.transform([%{"tuple" => [":key", "value"]}])
+ assert binary == :erlang.term_to_binary([{:key, "value"}])
+ assert ConfigDB.from_binary(binary) == [{:key, "value"}]
+ assert ConfigDB.from_binary(binary) == [key: "value"]
+ end
+
+ test "keyword with partial_chain key" do
+ binary =
+ ConfigDB.transform([%{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}])
+
+ assert binary == :erlang.term_to_binary(partial_chain: &:hackney_connect.partial_chain/1)
+ assert ConfigDB.from_binary(binary) == [partial_chain: &:hackney_connect.partial_chain/1]
+ end
+
+ test "keyword" do
+ binary =
+ ConfigDB.transform([
+ %{"tuple" => [":types", "Pleroma.PostgresTypes"]},
+ %{"tuple" => [":telemetry_event", ["Pleroma.Repo.Instrumenter"]]},
+ %{"tuple" => [":migration_lock", nil]},
+ %{"tuple" => [":key1", 150]},
+ %{"tuple" => [":key2", "string"]}
+ ])
+
+ assert binary ==
+ :erlang.term_to_binary(
+ types: Pleroma.PostgresTypes,
+ telemetry_event: [Pleroma.Repo.Instrumenter],
+ migration_lock: nil,
+ key1: 150,
+ key2: "string"
+ )
+
+ assert ConfigDB.from_binary(binary) == [
+ types: Pleroma.PostgresTypes,
+ telemetry_event: [Pleroma.Repo.Instrumenter],
+ migration_lock: nil,
+ key1: 150,
+ key2: "string"
+ ]
+ end
+
+ test "complex keyword with nested mixed childs" do
+ binary =
+ ConfigDB.transform([
+ %{"tuple" => [":uploader", "Pleroma.Uploaders.Local"]},
+ %{"tuple" => [":filters", ["Pleroma.Upload.Filter.Dedupe"]]},
+ %{"tuple" => [":link_name", true]},
+ %{"tuple" => [":proxy_remote", false]},
+ %{"tuple" => [":common_map", %{":key" => "value"}]},
+ %{
+ "tuple" => [
+ ":proxy_opts",
+ [
+ %{"tuple" => [":redirect_on_failure", false]},
+ %{"tuple" => [":max_body_length", 1_048_576]},
+ %{
+ "tuple" => [
+ ":http",
+ [%{"tuple" => [":follow_redirect", true]}, %{"tuple" => [":pool", ":upload"]}]
+ ]
+ }
+ ]
+ ]
+ }
+ ])
+
+ assert binary ==
+ :erlang.term_to_binary(
+ uploader: Pleroma.Uploaders.Local,
+ filters: [Pleroma.Upload.Filter.Dedupe],
+ link_name: true,
+ proxy_remote: false,
+ common_map: %{key: "value"},
+ proxy_opts: [
+ redirect_on_failure: false,
+ max_body_length: 1_048_576,
+ http: [
+ follow_redirect: true,
+ pool: :upload
+ ]
+ ]
+ )
+
+ assert ConfigDB.from_binary(binary) ==
+ [
+ uploader: Pleroma.Uploaders.Local,
+ filters: [Pleroma.Upload.Filter.Dedupe],
+ link_name: true,
+ proxy_remote: false,
+ common_map: %{key: "value"},
+ proxy_opts: [
+ redirect_on_failure: false,
+ max_body_length: 1_048_576,
+ http: [
+ follow_redirect: true,
+ pool: :upload
+ ]
+ ]
+ ]
+ end
+
+ test "common keyword" do
+ binary =
+ ConfigDB.transform([
+ %{"tuple" => [":level", ":warn"]},
+ %{"tuple" => [":meta", [":all"]]},
+ %{"tuple" => [":path", ""]},
+ %{"tuple" => [":val", nil]},
+ %{"tuple" => [":webhook_url", "https://hooks.slack.com/services/YOUR-KEY-HERE"]}
+ ])
+
+ assert binary ==
+ :erlang.term_to_binary(
+ level: :warn,
+ meta: [:all],
+ path: "",
+ val: nil,
+ webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
+ )
+
+ assert ConfigDB.from_binary(binary) == [
+ level: :warn,
+ meta: [:all],
+ path: "",
+ val: nil,
+ webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
+ ]
+ end
+
+ test "complex keyword with sigil" do
+ binary =
+ ConfigDB.transform([
+ %{"tuple" => [":federated_timeline_removal", []]},
+ %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]},
+ %{"tuple" => [":replace", []]}
+ ])
+
+ assert binary ==
+ :erlang.term_to_binary(
+ federated_timeline_removal: [],
+ reject: [~r/comp[lL][aA][iI][nN]er/],
+ replace: []
+ )
+
+ assert ConfigDB.from_binary(binary) ==
+ [federated_timeline_removal: [], reject: [~r/comp[lL][aA][iI][nN]er/], replace: []]
+ end
+
+ test "complex keyword with tuples with more than 2 values" do
+ binary =
+ ConfigDB.transform([
+ %{
+ "tuple" => [
+ ":http",
+ [
+ %{
+ "tuple" => [
+ ":key1",
+ [
+ %{
+ "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 binary ==
+ :erlang.term_to_binary(
+ http: [
+ key1: [
+ _: [
+ {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
+ {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
+ {Phoenix.Transports.WebSocket,
+ {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, []}}},
+ {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
+ ]
+ ]
+ ]
+ )
+
+ assert ConfigDB.from_binary(binary) == [
+ http: [
+ key1: [
+ {:_,
+ [
+ {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
+ {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
+ {Phoenix.Transports.WebSocket,
+ {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, []}}},
+ {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
+ ]}
+ ]
+ ]
+ ]
+ end
+ end
+end
diff --git a/test/config/holder_test.exs b/test/config/holder_test.exs
new file mode 100644
index 000000000..15d48b5c7
--- /dev/null
+++ b/test/config/holder_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.Config.HolderTest do
+ use ExUnit.Case, async: true
+
+ alias Pleroma.Config.Holder
+
+ test "default_config/0" do
+ config = Holder.default_config()
+ assert config[:pleroma][Pleroma.Uploaders.Local][:uploads] == "test/uploads"
+ assert config[:tesla][:adapter] == Tesla.Mock
+
+ refute config[:pleroma][Pleroma.Repo]
+ refute config[:pleroma][Pleroma.Web.Endpoint]
+ refute config[:pleroma][:env]
+ refute config[:pleroma][:configurable_from_database]
+ refute config[:pleroma][:database]
+ refute config[:phoenix][:serve_endpoints]
+ end
+
+ test "default_config/1" do
+ pleroma_config = Holder.default_config(:pleroma)
+ assert pleroma_config[Pleroma.Uploaders.Local][:uploads] == "test/uploads"
+ tesla_config = Holder.default_config(:tesla)
+ assert tesla_config[:adapter] == Tesla.Mock
+ end
+
+ test "default_config/2" do
+ assert Holder.default_config(:pleroma, Pleroma.Uploaders.Local) == [uploads: "test/uploads"]
+ assert Holder.default_config(:tesla, :adapter) == Tesla.Mock
+ end
+end
diff --git a/test/config/loader_test.exs b/test/config/loader_test.exs
new file mode 100644
index 000000000..607572f4e
--- /dev/null
+++ b/test/config/loader_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.Config.LoaderTest do
+ use ExUnit.Case, async: true
+
+ alias Pleroma.Config.Loader
+
+ test "read/1" do
+ config = Loader.read("test/fixtures/config/temp.secret.exs")
+ assert config[:pleroma][:first_setting][:key] == "value"
+ assert config[:pleroma][:first_setting][:key2] == [Pleroma.Repo]
+ assert config[:quack][:level] == :info
+ end
+
+ test "filter_group/2" do
+ assert Loader.filter_group(:pleroma,
+ pleroma: [
+ {Pleroma.Repo, [a: 1, b: 2]},
+ {Pleroma.Upload, [a: 1, b: 2]},
+ {Pleroma.Web.Endpoint, []},
+ env: :test,
+ configurable_from_database: true,
+ database: []
+ ]
+ ) == [{Pleroma.Upload, [a: 1, b: 2]}]
+ end
+end
diff --git a/test/config/transfer_task_test.exs b/test/config/transfer_task_test.exs
index 9074f3b97..01d04761d 100644
--- a/test/config/transfer_task_test.exs
+++ b/test/config/transfer_task_test.exs
@@ -1,51 +1,185 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.TransferTaskTest do
use Pleroma.DataCase
- clear_config([:instance, :dynamic_configuration]) do
- Pleroma.Config.put([:instance, :dynamic_configuration], true)
+ import ExUnit.CaptureLog
+
+ alias Pleroma.Config.TransferTask
+ alias Pleroma.ConfigDB
+
+ clear_config(:configurable_from_database) do
+ Pleroma.Config.put(:configurable_from_database, true)
end
test "transfer config values from db to env" do
refute Application.get_env(:pleroma, :test_key)
refute Application.get_env(:idna, :test_key)
+ refute Application.get_env(:quack, :test_key)
- Pleroma.Web.AdminAPI.Config.create(%{
- group: "pleroma",
- key: "test_key",
+ ConfigDB.create(%{
+ group: ":pleroma",
+ key: ":test_key",
value: [live: 2, com: 3]
})
- Pleroma.Web.AdminAPI.Config.create(%{
- group: "idna",
- key: "test_key",
+ ConfigDB.create(%{
+ group: ":idna",
+ key: ":test_key",
value: [live: 15, com: 35]
})
- Pleroma.Config.TransferTask.start_link([])
+ ConfigDB.create(%{
+ group: ":quack",
+ key: ":test_key",
+ value: [:test_value1, :test_value2]
+ })
+
+ TransferTask.start_link([])
assert Application.get_env(:pleroma, :test_key) == [live: 2, com: 3]
assert Application.get_env(:idna, :test_key) == [live: 15, com: 35]
+ assert Application.get_env(:quack, :test_key) == [:test_value1, :test_value2]
on_exit(fn ->
Application.delete_env(:pleroma, :test_key)
Application.delete_env(:idna, :test_key)
+ Application.delete_env(:quack, :test_key)
end)
end
- test "non existing atom" do
- Pleroma.Web.AdminAPI.Config.create(%{
- group: "pleroma",
- key: "undefined_atom_key",
- value: [live: 2, com: 3]
+ test "transfer config values for 1 group and some keys" do
+ level = Application.get_env(:quack, :level)
+ meta = Application.get_env(:quack, :meta)
+
+ ConfigDB.create(%{
+ group: ":quack",
+ key: ":level",
+ value: :info
+ })
+
+ ConfigDB.create(%{
+ group: ":quack",
+ key: ":meta",
+ value: [:none]
+ })
+
+ TransferTask.start_link([])
+
+ assert Application.get_env(:quack, :level) == :info
+ assert Application.get_env(:quack, :meta) == [:none]
+ default = Pleroma.Config.Holder.default_config(:quack, :webhook_url)
+ assert Application.get_env(:quack, :webhook_url) == default
+
+ on_exit(fn ->
+ Application.put_env(:quack, :level, level)
+ Application.put_env(:quack, :meta, meta)
+ end)
+ end
+
+ test "transfer config values with full subkey update" do
+ emoji = Application.get_env(:pleroma, :emoji)
+ assets = Application.get_env(:pleroma, :assets)
+
+ ConfigDB.create(%{
+ group: ":pleroma",
+ key: ":emoji",
+ value: [groups: [a: 1, b: 2]]
})
- assert ExUnit.CaptureLog.capture_log(fn ->
- Pleroma.Config.TransferTask.start_link([])
- end) =~
- "updating env causes error, key: \"undefined_atom_key\", error: %ArgumentError{message: \"argument error\"}"
+ ConfigDB.create(%{
+ group: ":pleroma",
+ key: ":assets",
+ value: [mascots: [a: 1, b: 2]]
+ })
+
+ TransferTask.start_link([])
+
+ emoji_env = Application.get_env(:pleroma, :emoji)
+ assert emoji_env[:groups] == [a: 1, b: 2]
+ assets_env = Application.get_env(:pleroma, :assets)
+ assert assets_env[:mascots] == [a: 1, b: 2]
+
+ on_exit(fn ->
+ Application.put_env(:pleroma, :emoji, emoji)
+ Application.put_env(:pleroma, :assets, assets)
+ end)
+ end
+
+ describe "pleroma restart" do
+ setup do
+ on_exit(fn -> Restarter.Pleroma.refresh() end)
+ end
+
+ test "don't restart if no reboot time settings were changed" do
+ emoji = Application.get_env(:pleroma, :emoji)
+ on_exit(fn -> Application.put_env(:pleroma, :emoji, emoji) end)
+
+ ConfigDB.create(%{
+ group: ":pleroma",
+ key: ":emoji",
+ value: [groups: [a: 1, b: 2]]
+ })
+
+ refute String.contains?(
+ capture_log(fn -> TransferTask.start_link([]) end),
+ "pleroma restarted"
+ )
+ end
+
+ test "on reboot time key" do
+ chat = Application.get_env(:pleroma, :chat)
+ on_exit(fn -> Application.put_env(:pleroma, :chat, chat) end)
+
+ ConfigDB.create(%{
+ group: ":pleroma",
+ key: ":chat",
+ value: [enabled: false]
+ })
+
+ assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted"
+ end
+
+ test "on reboot time subkey" do
+ captcha = Application.get_env(:pleroma, Pleroma.Captcha)
+ on_exit(fn -> Application.put_env(:pleroma, Pleroma.Captcha, captcha) end)
+
+ ConfigDB.create(%{
+ group: ":pleroma",
+ key: "Pleroma.Captcha",
+ value: [seconds_valid: 60]
+ })
+
+ assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted"
+ end
+
+ test "don't restart pleroma on reboot time key and subkey if there is false flag" do
+ chat = Application.get_env(:pleroma, :chat)
+ captcha = Application.get_env(:pleroma, Pleroma.Captcha)
+
+ on_exit(fn ->
+ Application.put_env(:pleroma, :chat, chat)
+ Application.put_env(:pleroma, Pleroma.Captcha, captcha)
+ end)
+
+ ConfigDB.create(%{
+ group: ":pleroma",
+ key: ":chat",
+ value: [enabled: false]
+ })
+
+ ConfigDB.create(%{
+ group: ":pleroma",
+ key: "Pleroma.Captcha",
+ value: [seconds_valid: 60]
+ })
+
+ refute String.contains?(
+ capture_log(fn -> TransferTask.load_and_update_env([], false) end),
+ "pleroma restarted"
+ )
+ end
end
end
diff --git a/test/config_test.exs b/test/config_test.exs
index 438fe62ee..a46ab4302 100644
--- a/test/config_test.exs
+++ b/test/config_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ConfigTest do
diff --git a/test/conversation/participation_test.exs b/test/conversation/participation_test.exs
index 863270022..3536842e8 100644
--- a/test/conversation/participation_test.exs
+++ b/test/conversation/participation_test.exs
@@ -1,11 +1,13 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Conversation.ParticipationTest do
use Pleroma.DataCase
import Pleroma.Factory
+ alias Pleroma.Conversation
alias Pleroma.Conversation.Participation
+ alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
@@ -98,7 +100,9 @@ defmodule Pleroma.Conversation.ParticipationTest do
assert participation.user_id == user.id
assert participation.conversation_id == conversation.id
+ # Needed because updated_at is accurate down to a second
:timer.sleep(1000)
+
# Creating again returns the same participation
{:ok, %Participation{} = participation_two} =
Participation.create_for_user_and_conversation(user, conversation)
@@ -121,9 +125,10 @@ defmodule Pleroma.Conversation.ParticipationTest do
test "it marks a participation as read" do
participation = insert(:participation, %{read: false})
- {:ok, participation} = Participation.mark_as_read(participation)
+ {:ok, updated_participation} = Participation.mark_as_read(participation)
- assert participation.read
+ assert updated_participation.read
+ assert updated_participation.updated_at == participation.updated_at
end
test "it marks a participation as unread" do
@@ -150,9 +155,7 @@ defmodule Pleroma.Conversation.ParticipationTest do
test "gets all the participations for a user, ordered by updated at descending" do
user = insert(:user)
{:ok, activity_one} = CommonAPI.post(user, %{"status" => "x", "visibility" => "direct"})
- :timer.sleep(1000)
{:ok, activity_two} = CommonAPI.post(user, %{"status" => "x", "visibility" => "direct"})
- :timer.sleep(1000)
{:ok, activity_three} =
CommonAPI.post(user, %{
@@ -161,6 +164,17 @@ defmodule Pleroma.Conversation.ParticipationTest do
"in_reply_to_status_id" => activity_one.id
})
+ # Offset participations because the accuracy of updated_at is down to a second
+
+ for {activity, offset} <- [{activity_two, 1}, {activity_three, 2}] do
+ conversation = Conversation.get_for_ap_id(activity.data["context"])
+ participation = Participation.for_user_and_conversation(user, conversation)
+ updated_at = NaiveDateTime.add(Map.get(participation, :updated_at), offset)
+
+ Ecto.Changeset.change(participation, %{updated_at: updated_at})
+ |> Repo.update!()
+ end
+
assert [participation_one, participation_two] = Participation.for_user(user)
object2 = Pleroma.Object.normalize(activity_two)
@@ -252,7 +266,7 @@ defmodule Pleroma.Conversation.ParticipationTest do
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 4
- {:ok, blocker} = User.block(blocker, blocked)
+ {:ok, _user_relationship} = User.block(blocker, blocked)
# The conversations with the blocked user are marked as read
assert [%{read: true}, %{read: true}, %{read: true}, %{read: false}] =
@@ -274,7 +288,7 @@ defmodule Pleroma.Conversation.ParticipationTest do
blocked = insert(:user)
third_user = insert(:user)
- {:ok, blocker} = User.block(blocker, blocked)
+ {:ok, _user_relationship} = User.block(blocker, blocked)
# When the blocked user is the author
{:ok, _direct1} =
@@ -311,7 +325,7 @@ defmodule Pleroma.Conversation.ParticipationTest do
"visibility" => "direct"
})
- {:ok, blocker} = User.block(blocker, blocked)
+ {:ok, _user_relationship} = User.block(blocker, blocked)
assert [%{read: true}] = Participation.for_user(blocker)
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
diff --git a/test/conversation_test.exs b/test/conversation_test.exs
index 693427d80..dc0027d04 100644
--- a/test/conversation_test.exs
+++ b/test/conversation_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ConversationTest do
diff --git a/test/daemons/activity_expiration_daemon_test.exs b/test/daemons/activity_expiration_daemon_test.exs
deleted file mode 100644
index b51132fb0..000000000
--- a/test/daemons/activity_expiration_daemon_test.exs
+++ /dev/null
@@ -1,17 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.ActivityExpirationWorkerTest do
- use Pleroma.DataCase
- alias Pleroma.Activity
- import Pleroma.Factory
-
- test "deletes an activity" do
- activity = insert(:note_activity)
- expiration = insert(:expiration_in_the_past, %{activity_id: activity.id})
- Pleroma.Daemons.ActivityExpirationDaemon.perform(:execute, expiration.id)
-
- refute Repo.get(Activity, activity.id)
- end
-end
diff --git a/test/daemons/scheduled_activity_daemon_test.exs b/test/daemons/scheduled_activity_daemon_test.exs
deleted file mode 100644
index c8e464491..000000000
--- a/test/daemons/scheduled_activity_daemon_test.exs
+++ /dev/null
@@ -1,19 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.ScheduledActivityDaemonTest do
- use Pleroma.DataCase
- alias Pleroma.ScheduledActivity
- import Pleroma.Factory
-
- test "creates a status from the scheduled activity" do
- user = insert(:user)
- scheduled_activity = insert(:scheduled_activity, user: user, params: %{status: "hi"})
- Pleroma.Daemons.ScheduledActivityDaemon.perform(:execute, 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
-end
diff --git a/test/docs/generator_test.exs b/test/docs/generator_test.exs
new file mode 100644
index 000000000..9c9f4357b
--- /dev/null
+++ b/test/docs/generator_test.exs
@@ -0,0 +1,230 @@
+defmodule Pleroma.Docs.GeneratorTest do
+ use ExUnit.Case, async: true
+ alias Pleroma.Docs.Generator
+
+ @descriptions [
+ %{
+ group: :pleroma,
+ key: Pleroma.Upload,
+ type: :group,
+ description: "",
+ children: [
+ %{
+ key: :uploader,
+ type: :module,
+ description: "",
+ suggestions:
+ Generator.list_modules_in_dir(
+ "lib/pleroma/upload/filter",
+ "Elixir.Pleroma.Upload.Filter."
+ )
+ },
+ %{
+ key: :filters,
+ type: {:list, :module},
+ description: "",
+ suggestions:
+ Generator.list_modules_in_dir(
+ "lib/pleroma/web/activity_pub/mrf",
+ "Elixir.Pleroma.Web.ActivityPub.MRF."
+ )
+ },
+ %{
+ key: Pleroma.Upload,
+ type: :string,
+ description: "",
+ suggestions: [""]
+ },
+ %{
+ key: :some_key,
+ type: :keyword,
+ description: "",
+ suggestions: [],
+ children: [
+ %{
+ key: :another_key,
+ type: :integer,
+ description: "",
+ suggestions: [5]
+ },
+ %{
+ key: :another_key_with_label,
+ label: "Another label",
+ type: :integer,
+ description: "",
+ suggestions: [7]
+ }
+ ]
+ },
+ %{
+ key: :key1,
+ type: :atom,
+ description: "",
+ suggestions: [
+ :atom,
+ Pleroma.Upload,
+ {:tuple, "string", 8080},
+ [:atom, Pleroma.Upload, {:atom, Pleroma.Upload}]
+ ]
+ },
+ %{
+ key: Pleroma.Upload,
+ label: "Special Label",
+ type: :string,
+ description: "",
+ suggestions: [""]
+ },
+ %{
+ group: {:subgroup, Swoosh.Adapters.SMTP},
+ key: :auth,
+ type: :atom,
+ description: "`Swoosh.Adapters.SMTP` adapter specific setting",
+ suggestions: [:always, :never, :if_available]
+ },
+ %{
+ key: "application/xml",
+ type: {:list, :string},
+ suggestions: ["xml"]
+ },
+ %{
+ key: :versions,
+ type: {:list, :atom},
+ description: "List of TLS version to use",
+ suggestions: [:tlsv1, ":tlsv1.1", ":tlsv1.2"]
+ }
+ ]
+ },
+ %{
+ group: :tesla,
+ key: :adapter,
+ type: :group,
+ description: ""
+ },
+ %{
+ group: :cors_plug,
+ type: :group,
+ children: [%{key: :key1, type: :string, suggestions: [""]}]
+ },
+ %{group: "Some string group", key: "Some string key", type: :group}
+ ]
+
+ describe "convert_to_strings/1" do
+ test "group, key, label" do
+ [desc1, desc2 | _] = Generator.convert_to_strings(@descriptions)
+
+ assert desc1[:group] == ":pleroma"
+ assert desc1[:key] == "Pleroma.Upload"
+ assert desc1[:label] == "Pleroma.Upload"
+
+ assert desc2[:group] == ":tesla"
+ assert desc2[:key] == ":adapter"
+ assert desc2[:label] == "Adapter"
+ end
+
+ test "group without key" do
+ descriptions = Generator.convert_to_strings(@descriptions)
+ desc = Enum.at(descriptions, 2)
+
+ assert desc[:group] == ":cors_plug"
+ refute desc[:key]
+ assert desc[:label] == "Cors plug"
+ end
+
+ test "children key, label, type" do
+ [%{children: [child1, child2, child3, child4 | _]} | _] =
+ Generator.convert_to_strings(@descriptions)
+
+ assert child1[:key] == ":uploader"
+ assert child1[:label] == "Uploader"
+ assert child1[:type] == :module
+
+ assert child2[:key] == ":filters"
+ assert child2[:label] == "Filters"
+ assert child2[:type] == {:list, :module}
+
+ assert child3[:key] == "Pleroma.Upload"
+ assert child3[:label] == "Pleroma.Upload"
+ assert child3[:type] == :string
+
+ assert child4[:key] == ":some_key"
+ assert child4[:label] == "Some key"
+ assert child4[:type] == :keyword
+ end
+
+ test "child with predefined label" do
+ [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+ child = Enum.at(children, 5)
+ assert child[:key] == "Pleroma.Upload"
+ assert child[:label] == "Special Label"
+ end
+
+ test "subchild" do
+ [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+ child = Enum.at(children, 3)
+ %{children: [subchild | _]} = child
+
+ assert subchild[:key] == ":another_key"
+ assert subchild[:label] == "Another key"
+ assert subchild[:type] == :integer
+ end
+
+ test "subchild with predefined label" do
+ [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+ child = Enum.at(children, 3)
+ %{children: subchildren} = child
+ subchild = Enum.at(subchildren, 1)
+
+ assert subchild[:key] == ":another_key_with_label"
+ assert subchild[:label] == "Another label"
+ end
+
+ test "module suggestions" do
+ [%{children: [%{suggestions: suggestions} | _]} | _] =
+ Generator.convert_to_strings(@descriptions)
+
+ Enum.each(suggestions, fn suggestion ->
+ assert String.starts_with?(suggestion, "Pleroma.")
+ end)
+ end
+
+ test "atoms in suggestions with leading `:`" do
+ [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+ %{suggestions: suggestions} = Enum.at(children, 4)
+ assert Enum.at(suggestions, 0) == ":atom"
+ assert Enum.at(suggestions, 1) == "Pleroma.Upload"
+ assert Enum.at(suggestions, 2) == {":tuple", "string", 8080}
+ assert Enum.at(suggestions, 3) == [":atom", "Pleroma.Upload", {":atom", "Pleroma.Upload"}]
+
+ %{suggestions: suggestions} = Enum.at(children, 6)
+ assert Enum.at(suggestions, 0) == ":always"
+ assert Enum.at(suggestions, 1) == ":never"
+ assert Enum.at(suggestions, 2) == ":if_available"
+ end
+
+ test "group, key as string in main desc" do
+ descriptions = Generator.convert_to_strings(@descriptions)
+ desc = Enum.at(descriptions, 3)
+ assert desc[:group] == "Some string group"
+ assert desc[:key] == "Some string key"
+ end
+
+ test "key as string subchild" do
+ [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+ child = Enum.at(children, 7)
+ assert child[:key] == "application/xml"
+ end
+
+ test "suggestion for tls versions" do
+ [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+ child = Enum.at(children, 8)
+ assert child[:suggestions] == [":tlsv1", ":tlsv1.1", ":tlsv1.2"]
+ end
+
+ test "subgroup with module name" do
+ [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+
+ %{group: subgroup} = Enum.at(children, 6)
+ assert subgroup == {":subgroup", "Swoosh.Adapters.SMTP"}
+ end
+ end
+end
diff --git a/test/earmark_renderer_test.exs b/test/earmark_renderer_test.exs
new file mode 100644
index 000000000..220d97d16
--- /dev/null
+++ b/test/earmark_renderer_test.exs
@@ -0,0 +1,79 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+defmodule Pleroma.EarmarkRendererTest do
+ use ExUnit.Case
+
+ test "Paragraph" do
+ code = ~s[Hello\n\nWorld!]
+ result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer})
+ assert result == "<p>Hello</p><p>World!</p>"
+ end
+
+ test "raw HTML" do
+ code = ~s[<a href="http://example.org/">OwO</a><!-- what's this?-->]
+ result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer})
+ assert result == "<p>#{code}</p>"
+ end
+
+ test "rulers" do
+ code = ~s[before\n\n-----\n\nafter]
+ result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer})
+ assert result == "<p>before</p><hr /><p>after</p>"
+ end
+
+ test "headings" do
+ code = ~s[# h1\n## h2\n### h3\n]
+ result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer})
+ assert result == ~s[<h1>h1</h1><h2>h2</h2><h3>h3</h3>]
+ end
+
+ test "blockquote" do
+ code = ~s[> whoms't are you quoting?]
+ result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer})
+ assert result == "<blockquote><p>whoms’t are you quoting?</p></blockquote>"
+ end
+
+ test "code" do
+ code = ~s[`mix`]
+ result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer})
+ assert result == ~s[<p><code class="inline">mix</code></p>]
+
+ code = ~s[``mix``]
+ result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer})
+ assert result == ~s[<p><code class="inline">mix</code></p>]
+
+ code = ~s[```\nputs "Hello World"\n```]
+ result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer})
+ assert result == ~s[<pre><code class="">puts &quot;Hello World&quot;</code></pre>]
+ end
+
+ test "lists" do
+ code = ~s[- one\n- two\n- three\n- four]
+ result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer})
+ assert result == "<ul><li>one</li><li>two</li><li>three</li><li>four</li></ul>"
+
+ code = ~s[1. one\n2. two\n3. three\n4. four\n]
+ result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer})
+ assert result == "<ol><li>one</li><li>two</li><li>three</li><li>four</li></ol>"
+ end
+
+ test "delegated renderers" do
+ code = ~s[a<br/>b]
+ result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer})
+ assert result == "<p>#{code}</p>"
+
+ code = ~s[*aaaa~*]
+ result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer})
+ assert result == ~s[<p><em>aaaa~</em></p>]
+
+ code = ~s[**aaaa~**]
+ result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer})
+ assert result == ~s[<p><strong>aaaa~</strong></p>]
+
+ # strikethrought
+ code = ~s[<del>aaaa~</del>]
+ result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer})
+ assert result == ~s[<p><del>aaaa~</del></p>]
+ end
+end
diff --git a/test/emails/admin_email_test.exs b/test/emails/admin_email_test.exs
index ad89f9213..bc871a0a9 100644
--- a/test/emails/admin_email_test.exs
+++ b/test/emails/admin_email_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.AdminEmailTest do
@@ -19,8 +19,8 @@ defmodule Pleroma.Emails.AdminEmailTest do
AdminEmail.report(to_user, reporter, account, [%{name: "Test", id: "12"}], "Test comment")
status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, "12")
- reporter_url = Helpers.feed_url(Pleroma.Web.Endpoint, :feed_redirect, reporter.id)
- account_url = Helpers.feed_url(Pleroma.Web.Endpoint, :feed_redirect, account.id)
+ reporter_url = Helpers.user_feed_url(Pleroma.Web.Endpoint, :feed_redirect, reporter.id)
+ account_url = Helpers.user_feed_url(Pleroma.Web.Endpoint, :feed_redirect, account.id)
assert res.to == [{to_user.name, to_user.email}]
assert res.from == {config[:name], config[:notify_email]}
diff --git a/test/emails/mailer_test.exs b/test/emails/mailer_test.exs
index 2425c85dd..f30aa6a72 100644
--- a/test/emails/mailer_test.exs
+++ b/test/emails/mailer_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.MailerTest do
diff --git a/test/emails/user_email_test.exs b/test/emails/user_email_test.exs
index 9e145977e..a75623bb4 100644
--- a/test/emails/user_email_test.exs
+++ b/test/emails/user_email_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.UserEmailTest do
diff --git a/test/emoji/formatter_test.exs b/test/emoji/formatter_test.exs
index fda80d470..3bfee9420 100644
--- a/test/emoji/formatter_test.exs
+++ b/test/emoji/formatter_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji.FormatterTest do
diff --git a/test/emoji/loader_test.exs b/test/emoji/loader_test.exs
index 045eef150..804039e7e 100644
--- a/test/emoji/loader_test.exs
+++ b/test/emoji/loader_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji.LoaderTest do
diff --git a/test/emoji_test.exs b/test/emoji_test.exs
index 7bdf2b6fa..b36047578 100644
--- a/test/emoji_test.exs
+++ b/test/emoji_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EmojiTest do
diff --git a/test/federation/federation_test.exs b/test/federation/federation_test.exs
index 45800568a..10d71fb88 100644
--- a/test/federation/federation_test.exs
+++ b/test/federation/federation_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Integration.FederationTest do
diff --git a/test/filter_test.exs b/test/filter_test.exs
index b31c68efd..b2a8330ee 100644
--- a/test/filter_test.exs
+++ b/test/filter_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.FilterTest do
diff --git a/test/fixtures/config/temp.secret.exs b/test/fixtures/config/temp.secret.exs
new file mode 100644
index 000000000..f4686c101
--- /dev/null
+++ b/test/fixtures/config/temp.secret.exs
@@ -0,0 +1,9 @@
+use Mix.Config
+
+config :pleroma, :first_setting, key: "value", key2: [Pleroma.Repo]
+
+config :pleroma, :second_setting, key: "value2", key2: ["Activity"]
+
+config :quack, level: :info
+
+config :pleroma, Pleroma.Repo, pool: Ecto.Adapters.SQL.Sandbox
diff --git a/test/fixtures/emoji-reaction-no-emoji.json b/test/fixtures/emoji-reaction-no-emoji.json
new file mode 100644
index 000000000..ef3bbe55c
--- /dev/null
+++ b/test/fixtures/emoji-reaction-no-emoji.json
@@ -0,0 +1,30 @@
+{
+ "type": "EmojiReact",
+ "signature": {
+ "type": "RsaSignature2017",
+ "signatureValue": "fdxMfQSMwbC6wP6sh6neS/vM5879K67yQkHTbiT5Npr5wAac0y6+o3Ij+41tN3rL6wfuGTosSBTHOtta6R4GCOOhCaCSLMZKypnp1VltCzLDoyrZELnYQIC8gpUXVmIycZbREk22qWUe/w7DAFaKK4UscBlHDzeDVcA0K3Se5Sluqi9/Zh+ldAnEzj/rSEPDjrtvf5wGNf3fHxbKSRKFt90JvKK6hS+vxKUhlRFDf6/SMETw+EhwJSNW4d10yMUakqUWsFv4Acq5LW7l+HpYMvlYY1FZhNde1+uonnCyuQDyvzkff8zwtEJmAXC4RivO/VVLa17SmqheJZfI8oluVg==",
+ "creator": "http://mastodon.example.org/users/admin#main-key",
+ "created": "2018-02-17T18:57:49Z"
+ },
+ "object": "http://localtesting.pleroma.lol/objects/eb92579d-3417-42a8-8652-2492c2d4f454",
+ "content": "~",
+ "nickname": "lain",
+ "id": "http://mastodon.example.org/users/admin#reactions/2",
+ "actor": "http://mastodon.example.org/users/admin",
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "toot": "http://joinmastodon.org/ns#",
+ "sensitive": "as:sensitive",
+ "ostatus": "http://ostatus.org#",
+ "movedTo": "as:movedTo",
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "atomUri": "ostatus:atomUri",
+ "Hashtag": "as:Hashtag",
+ "Emoji": "toot:Emoji"
+ }
+ ]
+}
diff --git a/test/fixtures/emoji-reaction-too-long.json b/test/fixtures/emoji-reaction-too-long.json
new file mode 100644
index 000000000..e917c9a68
--- /dev/null
+++ b/test/fixtures/emoji-reaction-too-long.json
@@ -0,0 +1,30 @@
+{
+ "type": "EmojiReact",
+ "signature": {
+ "type": "RsaSignature2017",
+ "signatureValue": "fdxMfQSMwbC6wP6sh6neS/vM5879K67yQkHTbiT5Npr5wAac0y6+o3Ij+41tN3rL6wfuGTosSBTHOtta6R4GCOOhCaCSLMZKypnp1VltCzLDoyrZELnYQIC8gpUXVmIycZbREk22qWUe/w7DAFaKK4UscBlHDzeDVcA0K3Se5Sluqi9/Zh+ldAnEzj/rSEPDjrtvf5wGNf3fHxbKSRKFt90JvKK6hS+vxKUhlRFDf6/SMETw+EhwJSNW4d10yMUakqUWsFv4Acq5LW7l+HpYMvlYY1FZhNde1+uonnCyuQDyvzkff8zwtEJmAXC4RivO/VVLa17SmqheJZfI8oluVg==",
+ "creator": "http://mastodon.example.org/users/admin#main-key",
+ "created": "2018-02-17T18:57:49Z"
+ },
+ "object": "http://localtesting.pleroma.lol/objects/eb92579d-3417-42a8-8652-2492c2d4f454",
+ "content": "👌👌",
+ "nickname": "lain",
+ "id": "http://mastodon.example.org/users/admin#reactions/2",
+ "actor": "http://mastodon.example.org/users/admin",
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "toot": "http://joinmastodon.org/ns#",
+ "sensitive": "as:sensitive",
+ "ostatus": "http://ostatus.org#",
+ "movedTo": "as:movedTo",
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "atomUri": "ostatus:atomUri",
+ "Hashtag": "as:Hashtag",
+ "Emoji": "toot:Emoji"
+ }
+ ]
+}
diff --git a/test/fixtures/emoji-reaction.json b/test/fixtures/emoji-reaction.json
index 3812e43ad..fe1fecddb 100644
--- a/test/fixtures/emoji-reaction.json
+++ b/test/fixtures/emoji-reaction.json
@@ -1,5 +1,5 @@
{
- "type": "EmojiReaction",
+ "type": "EmojiReact",
"signature": {
"type": "RsaSignature2017",
"signatureValue": "fdxMfQSMwbC6wP6sh6neS/vM5879K67yQkHTbiT5Npr5wAac0y6+o3Ij+41tN3rL6wfuGTosSBTHOtta6R4GCOOhCaCSLMZKypnp1VltCzLDoyrZELnYQIC8gpUXVmIycZbREk22qWUe/w7DAFaKK4UscBlHDzeDVcA0K3Se5Sluqi9/Zh+ldAnEzj/rSEPDjrtvf5wGNf3fHxbKSRKFt90JvKK6hS+vxKUhlRFDf6/SMETw+EhwJSNW4d10yMUakqUWsFv4Acq5LW7l+HpYMvlYY1FZhNde1+uonnCyuQDyvzkff8zwtEJmAXC4RivO/VVLa17SmqheJZfI8oluVg==",
diff --git a/test/fixtures/margaret-corbin-grave-west-point.html b/test/fixtures/margaret-corbin-grave-west-point.html
new file mode 100644
index 000000000..f6d387cc8
--- /dev/null
+++ b/test/fixtures/margaret-corbin-grave-west-point.html
@@ -0,0 +1,2895 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <link rel="preload" href="https://fonts.atlasobscura.com/2/Platform-Regular-Web.woff2" as="font" type="font/woff2"
+ crossorigin>
+ <link rel="preload" href="https://fonts.atlasobscura.com/2/Platform-Medium-Web.woff2" as="font" type="font/woff2"
+ crossorigin>
+ <link rel="preload" href="https://fonts.atlasobscura.com/2/FreigTexProBookWeb.woff2" as="font" type="font/woff2"
+ crossorigin>
+ <link rel="preload" href="https://fonts.atlasobscura.com/2/FreigTexProBookItWeb.woff2" as="font" type="font/woff2"
+ crossorigin>
+ <link rel="preload" href="https://fonts.atlasobscura.com/icons2/atlasobscura.woff2?3sjg72" as="font" type="font/woff2"
+ crossorigin>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
+ <link rel="dns-prefetch" href="https://assets.atlasobscura.com">
+ <link rel="dns-prefetch" href="//b.scorecardresearch.com">
+ <link rel="dns-prefetch" href="https://maps.google.com">
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta property="fb:app_id" content="206394544492" />
+ <meta property="fb:admins" content="784963490" />
+ <meta property="fb:admins" content="514970725" />
+ <meta property="fb:pages" content="103921782727" />
+ <meta property="og:site_name" content="Atlas Obscura" />
+ <meta name="p:domain_verify" content="0f207004875a5511f774fc29f0a5a3f3" />
+ <meta name="pocket-site-verification" content="8353ea6cd97e141f193687e3013ce3" />
+ <link rel='alternate' type='application/rss+xml' title='Atlas Obscura - Latest Articles and Places'
+ href='https://www.atlasobscura.com/feeds/latest'>
+ <link rel="apple-touch-icon"
+ href='https://assets.atlasobscura.com/media/W1siZnUiLCJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vYXRsYXMtZGV2L21pc2MvaWNvbnMvYXBwbGUtdG91Y2gtaWNvbi5wbmciXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLXN0cmlwIl1d/apple-touch-icon.png'>
+ <link rel="apple-touch-icon-precomposed" sizes='144x144'
+ href='https://assets.atlasobscura.com/media/W1siZnUiLCJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vYXRsYXMtZGV2L21pc2MvaWNvbnMvYXBwbGUtdG91Y2gtaWNvbi0xNDR4MTQ0LXByZWNvbXBvc2VkLnBuZyJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtc3RyaXAiXV0/apple-touch-icon-144x144-precomposed.png'>
+ <link rel="apple-touch-icon-precomposed" sizes='114x114'
+ href='https://assets.atlasobscura.com/media/W1siZnUiLCJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vYXRsYXMtZGV2L21pc2MvaWNvbnMvYXBwbGUtdG91Y2gtaWNvbi0xMTR4MTE0LXByZWNvbXBvc2VkLnBuZyJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtc3RyaXAiXV0/apple-touch-icon-114x114-precomposed.png'>
+ <link rel="apple-touch-icon-precomposed"
+ href='https://assets.atlasobscura.com/media/W1siZnUiLCJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vYXRsYXMtZGV2L21pc2MvaWNvbnMvYXBwbGUtdG91Y2gtaWNvbi1wcmVjb21wb3NlZC5wbmciXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLXN0cmlwIl1d/apple-touch-icon-precomposed.png'>
+ <link rel="icon" type="image/png" sizes="32x32"
+ href="https://assets.atlasobscura.com/media/W1siZnUiLCJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vYXRsYXMtZGV2L21pc2MvaWNvbnMvZmF2aWNvbi0zMngzMi5wbmciXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLXN0cmlwIl1d/favicon-32x32.png">
+ <link rel="icon" type="image/png" sizes="16x16"
+ href="https://assets.atlasobscura.com/media/W1siZnUiLCJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vYXRsYXMtZGV2L21pc2MvaWNvbnMvZmF2aWNvbi0xNngxNi5wbmciXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLXN0cmlwIl1d/favicon-16x16.png">
+ <link rel="manifest" href="https://s3.amazonaws.com/atlas-dev/misc/icons/manifest.json">
+ <link rel="mask-icon" href="https://s3.amazonaws.com/atlas-dev/misc/icons/safari-pinned-tab.svg" color="#53b19f">
+ <link rel="shortcut icon" href="https://s3.amazonaws.com/atlas-dev/misc/icons/favicon.ico" sizes="48x48">
+ <meta name="msapplication-config" content="https://s3.amazonaws.com/atlas-dev/misc/icons/browserconfig.xml">
+ <meta name="theme-color" content="#ffffff">
+ <link rel="canonical" href="https://www.atlasobscura.com/articles/margaret-corbin-grave-west-point">
+ <title>The Missing Grave of Margaret Corbin, Revolutionary War Veteran - Atlas Obscura</title>
+ <meta property="description"
+ content="She&#39;s the only woman veteran honored with a monument at West Point. But where was she buried?" />
+ <style>
+ .async-hide {
+ opacity: 1 !important
+ }
+ </style>
+
+ <link rel="stylesheet" media="all"
+ href="https://assets.atlasobscura.com/assets/application-b89b3d9664b00a9207c1e551a84c8d61278379549ee4ff231dd01555e21ac4ac.css" />
+ <meta property="og:title" content="The Missing Grave of Margaret Corbin, Revolutionary War Veteran" />
+ <meta property="og:url" content="http://www.atlasobscura.com/articles/margaret-corbin-grave-west-point" />
+ <meta property="og:image"
+ content="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzLzkwYzgyMzI4LThlMDUtNGRiNS05MDg3LTUzMGUxZTM5N2RmMmVkOTM5ZDM4MGM4OTIxYTQ5MF9EQVIgZXhodW1hdGlvbiBvZiBNYXJnYXJldCBDb3JiaW4gZ3JhdmUgMTkyNi5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiNjAweD4iXV0/DAR%20exhumation%20of%20Margaret%20Corbin%20grave%201926.jpg" />
+ <meta property="og:description"
+ content="She&#39;s the only woman veteran honored with a monument at West Point. But where was she buried?" />
+ <meta property="og:type" content="article" />
+ <meta property="article:published_time" content="2020-01-14 11:15:00 -0500" />
+ <meta property="article:modified_time" content="2020-01-14 16:31:46 -0500" />
+ <meta property="article:author" content="Shane Cashman">
+ <meta property="article:tag" content="history" />
+ <meta property="article:tag" content="monuments" />
+ <meta property="article:tag" content="military history" />
+ <meta property="article:tag" content="cemeteries" />
+ <meta property="article:tag" content="graveyards" />
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:site" content="@atlasobscura">
+ <meta name="twitter:image"
+ content="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzLzkwYzgyMzI4LThlMDUtNGRiNS05MDg3LTUzMGUxZTM5N2RmMmVkOTM5ZDM4MGM4OTIxYTQ5MF9EQVIgZXhodW1hdGlvbiBvZiBNYXJnYXJldCBDb3JiaW4gZ3JhdmUgMTkyNi5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiNjAweD4iXV0/DAR%20exhumation%20of%20Margaret%20Corbin%20grave%201926.jpg">
+ <link rel="amphtml" href="https://www.atlasobscura.com/articles/margaret-corbin-grave-west-point.amp" />
+</head>
+
+<body class="articles show ArticleTemplate --">
+ <div id="fb-root"></div>
+
+ <div class="hidden-xs hidden-sm">
+ <div class="ad-background gastro-top-ad">
+ <div class='ad-wrapper hidden-print top-of-site-wrapper' style='position: relative; z-index: 4999;'>
+ <div class="htl-ad" data-unit="Site_Top_Full_Width" data-ref="Site_Top_Full_Width_div"
+ data-sizes="0x0:|668x0:728x90,940x385,970x250" data-prebid="Site_Top_Full_Width" data-eager></div>
+ </div>
+ </div>
+ </div>
+ <header class="page-header">
+ <div class="container-fluid no-pad ">
+ <nav class="navbar-main">
+ <div class="nav-header">
+ <div class="row">
+ <div class="mobile-logo-link">
+ <a class="logo-link" title="Atlas Obscura" href="/">
+ <i class="icon icon-atlas-icon"></i></i><i class="icon icon-atlas-logo-alt"></i>
+ </a>
+ </div>
+ <div
+ class="hidden-xs hidden-sm hidden-print nav-tools-right with-notification-spacer hide-spacer js-notification-spaceable">
+ </div>
+ <div class="nav-search hidden-md hidden-lg hidden-print">
+ <a id="m-search-dropdown-link" class="js-launch-search-link nav-header-search-link-m non-decorated-link"
+ aria-label="Open Site Search Form" data-search-dock="search-dock-m" data-category="Search Suggest"
+ data-action="Opened Search Form" data-label="Global Search" href="javascript:void(0)">
+ <i class="icon-search"></i>
+ <i class="icon-menu-close"></i>
+ </a>
+ <div class="sitewide-hero-search vue-js-nav-search-bar mobile-search">
+ <div class="hero-search-bar-bg"></div>
+ <div class="container hero-search-wrapper js-hero-search-wrapper">
+ <div class="row">
+ <div class="col-md-10 col-md-offset-1 hero-search-bar">
+ <form autocomplete="off" class="js-search-form-to-submit js-hero-search" action="/search"
+ accept-charset="UTF-8" method="get"><input name="utf8" type="hidden" value="&#x2713;" />
+ <search-input></search-input>
+ </form>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-10 col-md-offset-1">
+ <search-suggestions></search-suggestions>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="nav-toggle-container"></div>
+ <a class="icon-menu-open nav-toggle js-nav-toggle visible-xs visible-sm" href="javascript:void(0)"
+ aria-label="Open menu">
+ <span class="notifications-badge"></span>
+ </a>
+ </div>
+ </div>
+ <div class="nav-content js-nav-content is-slider-hidden-m">
+ <div class="nav-verticals">
+ <div class="nav-left-section">
+ <a class="logo-link" title="Atlas Obscura" href="/">
+ <i class="icon icon-atlas-icon"></i></i><i class="icon icon-atlas-logo-alt"></i>
+ </a>
+ </div>
+ <ul class="nav-center-section">
+ <li class="nav-vertical js-taphover nav-events nav-content-vertical nav-trips">
+ <a class="heading-sm nav-link" href="/unusual-trips">
+ <div class="heading-sm nav-link-heading">Trips</div>
+ </a>
+ <div class="nav-dropdown">
+ <div class="container nav-dropdown-content">
+ <div class="fake-bg"></div>
+ <div class="row table-display">
+ <div class="col-md-3 left-panel">
+ <ul class="nav-links-column links-column nav-hoverable-links-column">
+ <li>
+ <a class="tab selected js-nav-rollover-tab" data-target="trip-upcoming-panel"
+ href="">Upcoming Trips<span class="icon-arrow-down nav-arrow-right"></span></a>
+ </li>
+
+ <li>
+ <a href="/unusual-trips/all">All Trips</a>
+ <a href="https://blog.atlasobscura.com">Trips Blog</a>
+ </li>
+ </ul>
+ <div id="nav-shop-callout-wrap" style="padding-top: 50%;">
+ <a class="shop-callout non-decorated-link" target="" href="/unique-gifts/atlas-obscura-book">
+ <figure class="shop-callout-image-wrap">
+ <img class="img-responsive"
+ src="https://assets.atlasobscura.com/media/W1siZnUiLCJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vYXRsYXMtZGV2L21pc2MvYXBwLWltYWdlcy9ib29rLzJuZC1lZGl0aW9uLW9uLXNpdGUtcHJvbW8ucG5nIl0sWyJwIiwidGh1bWIiLCIyNTB4PiJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtc3RyaXAiXV0/2nd-edition-on-site-promo.png"
+ alt="2nd edition on site promo" />
+ </figure>
+ <div class="shop-callout-text">
+ <div class="detail-md">Get the Atlas Obscura book</div>
+ <div class="cta-xs shop-action-text">Shop Now »</div>
+ </div>
+ </a>
+ </div>
+ </div>
+ <div id="trip-upcoming-panel" class="col-md-9 right-panel">
+ <div class="row">
+ <div class="col-md-9">
+ <h4 class="nav-category-heading heading-md-non-uppercase">Upcoming Trips</h4>
+ </div>
+ <div class="col-md-3">
+ <a class="detail-sm nav-dropdown-viewall-link" href="/unusual-trips/all">View All Trips
+ »</a>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-3">
+ <a class="nav-card" href="/unusual-trips/peru-amazon">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsIjIwMTkvMDQvMTMvMTkvNDgvNDYvZTdkNDNkODctZGQwMy00MzJlLTg5NjMtMzhiNjRjY2IwNGMwL01hY2F3IHBlcnUyLmpwZyJdLFsicCIsImNvbnZlcnQiLCIiXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLWF1dG8tb3JpZW50Il0sWyJwIiwidGh1bWIiLCIyMjJ4MTQ4IyJdXQ/Macaw%20peru2.jpg"
+ alt="" data-width="2466" data-height="1504" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <h3 class="title-sm nav-card-title">
+ <span class="title-underline">Expedition Amazon</span>
+ </h3>
+ </a>
+ </div>
+ <div class="col-md-3">
+ <a class="nav-card" href="/unusual-trips/new-orleans">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvZXZlbnRfaW1hZ2VzLzc4ZjIyYzE3LWQ2Y2UtNGM5Yi1hY2U2LWQ4NTZiYzUzMGExNmU4NmQ3NjJiYWU2MWFmOTA5N19OT0xBX1Bvc3RlcjEuSlBHIl0sWyJwIiwiY29udmVydCIsIiJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtYXV0by1vcmllbnQiXSxbInAiLCJ0aHVtYiIsIjIyMngxNDgjIl1d/NOLA_Poster1.JPG"
+ alt="" data-width="6000" data-height="4006" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <h3 class="title-sm nav-card-title">
+ <span class="title-underline">Shifting Tides: Art in New Orleans</span>
+ </h3>
+ </a>
+ </div>
+ <div class="col-md-3">
+ <a class="nav-card" href="/unusual-trips/galicia-culinary">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvZXZlbnRfaW1hZ2VzLzQ4M2M1Zjk2LWVmYjYtNGNmZC1hMzBkLWE4NDNiZWJjOGYzYTM1ZTJmZTcxNTdjZWU0ZDliZV9EU0NfMjI4OCAoMSkuanBnIl0sWyJwIiwiY29udmVydCIsIiJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtYXV0by1vcmllbnQiXSxbInAiLCJ0aHVtYiIsIjIyMngxNDgjIl1d/DSC_2288%20%281%29.jpg"
+ alt="" data-width="3008" data-height="2000" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <h3 class="title-sm nav-card-title">
+ <span class="title-underline">Barnacles, Bluffs, and Brine: A Galician Seafood
+ Pilgrimage</span>
+ </h3>
+ </a>
+ </div>
+ <div class="col-md-3">
+ <a class="nav-card" href="/unusual-trips/borrego-springs-photography">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvZXZlbnRfaW1hZ2VzLzlhY2Q0Yjk5LTM4NjQtNGJiNy04NTZlLWVjOTdjZTUzZjgyZjViZGRkNWM5ZjNjZTRlZmEyNV9EZXNlcnQgQmVhc3RzIEtlaW1pZy02LmpwZyJdLFsicCIsImNvbnZlcnQiLCIiXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLWF1dG8tb3JpZW50Il0sWyJwIiwidGh1bWIiLCIyMjJ4MTQ4IyJdXQ/Desert%20Beasts%20Keimig-6.jpg"
+ alt="" data-width="1800" data-height="1202" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <h3 class="title-sm nav-card-title">
+ <span class="title-underline">Dark Skies, Desert Beasts: Night Photography in Borrego
+ Springs, California</span>
+ </h3>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ <li class="nav-vertical js-taphover nav-events nav-content-vertical">
+ <a class="heading-sm nav-link" href="/experiences">
+ <div class="heading-sm nav-link-heading">Experiences</div>
+ </a>
+ <div class="nav-dropdown">
+ <div class="container nav-dropdown-content">
+ <div class="fake-bg"></div>
+ <div class="row table-display">
+ <div class="col-md-3 left-panel">
+ <h3 class="nav-panel-title">Quick Links</h3>
+ <ul class="nav-links-column links-column">
+ <li><a href="/experiences">All Experiences</a></li>
+ </ul>
+ <h3 class="nav-panel-title">Experiences by City</h3>
+ <ul class="nav-links-column links-column">
+ <li><a href="/things-to-do/chicago-illinois#upcoming-experiences">Chicago</a></li>
+ <li><a href="/things-to-do/denver-colorado#upcoming-experiences">Denver</a></li>
+ <li><a href="/things-to-do/los-angeles-california#upcoming-experiences">Los Angeles</a></li>
+ <li><a href="/things-to-do/new-york#upcoming-experiences">New York</a></li>
+ <li><a href="/things-to-do/philadelphia-pennsylvania#upcoming-experiences">Philadelphia</a>
+ </li>
+ <li><a href="/things-to-do/seattle-washington#upcoming-experiences">Seattle</a></li>
+ <li><a href="/things-to-do/washington-dc#upcoming-experiences">Washington, D.C.</a></li>
+ </ul>
+ </div>
+ <div class="col-md-9 right-panel">
+ <div class="row">
+ <div class="col-md-9">
+ <h4 class="nav-category-heading heading-md-non-uppercase">Upcoming Experiences</h4>
+ </div>
+ <div class="col-md-3">
+ <a class="detail-sm nav-dropdown-viewall-link" href="/experiences">View All Experiences
+ »</a>
+ </div>
+ </div>
+ <div class="row">
+ <a class="col-md-3 nav-card" href="/experiences/walking-the-hidden-wonders-of-midtown">
+ <div class="event-image-date-ko">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvZXhwZXJpZW5jZV9zZXJpZXNfaW1hZ2VzLzIwNC81ZjdhYmNkNzc0NTNmZDkyNzgwMS9jMzJkMjlhZi1iNWJlLTRjOGUtYmYxYy1jZTMxMWMzZTVhZjEuanBlZyJdLFsicCIsImNvbnZlcnQiLCIiXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLWF1dG8tb3JpZW50Il0sWyJwIiwidGh1bWIiLCIyMjJ4MTQ4IyJdXQ/c32d29af-b5be-4c8e-bf1c-ce311c3e5af1.jpeg"
+ alt="" data-width="1066" data-height="1600" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ </div>
+ <div class="detail-sm nav-card-location">New York</div>
+ <h3 class="title-sm nav-card-title">
+ <span class="title-underline">Walking the Hidden Wonders of Midtown</span>
+ </h3>
+ </a> <a class="col-md-3 nav-card" href="/experiences/visit-nycs-oldest-magic-shop-after-dark">
+ <div class="event-image-date-ko">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvZXhwZXJpZW5jZV9zZXJpZXNfaW1hZ2VzLzcyLzNkMTFlODQwZDc1MGIyMDFkMzBiL2YzMDA3NTI0LTE0ODUtNDE1Yy1iY2M3LWEzNzJlOTZjMDNhYy5qcGVnIl0sWyJwIiwiY29udmVydCIsIiJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtYXV0by1vcmllbnQiXSxbInAiLCJ0aHVtYiIsIjIyMngxNDgjIl1d/f3007524-1485-415c-bcc7-a372e96c03ac.jpeg"
+ alt="" data-width="1440" data-height="960" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ </div>
+ <div class="detail-sm nav-card-location">New York</div>
+ <h3 class="title-sm nav-card-title">
+ <span class="title-underline">Visit NYC&#39;s Oldest Magic Shop After Dark</span>
+ </h3>
+ </a> <a class="col-md-3 nav-card" href="/experiences/atlas-obscuras-wonders-of-fidi">
+ <div class="event-image-date-ko">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvZXhwZXJpZW5jZV9zZXJpZXNfaW1hZ2VzLzIwNS80MDYwNTY3N2IyZjk2ZWQyNTQ2ZC82OTY4OGRmMy1hNWY4LTRkMmYtYjk0OC0zZjk0MmFiNTAzOTMuanBlZyJdLFsicCIsImNvbnZlcnQiLCIiXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLWF1dG8tb3JpZW50Il0sWyJwIiwidGh1bWIiLCIyMjJ4MTQ4IyJdXQ/69688df3-a5f8-4d2f-b948-3f942ab50393.jpeg"
+ alt="" data-width="1074" data-height="1600" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ </div>
+ <div class="detail-sm nav-card-location">New York</div>
+ <h3 class="title-sm nav-card-title">
+ <span class="title-underline">Atlas Obscura&#39;s Wonders of FiDi</span>
+ </h3>
+ </a> <a class="col-md-3 nav-card" href="/experiences/a-hoppin-good-time-at-the-bunny-museum">
+ <div class="event-image-date-ko">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvZXhwZXJpZW5jZV9zZXJpZXNfaW1hZ2VzLzIwNy9kOWI4ODczZDUzOTAxODQ2YmVhYy8xYTMyZWI2NS04YTg2LTQzOGYtOGM2YS0yMDgxMjQxMjVlMjMuanBlZyJdLFsicCIsImNvbnZlcnQiLCIiXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLWF1dG8tb3JpZW50Il0sWyJwIiwidGh1bWIiLCIyMjJ4MTQ4IyJdXQ/1a32eb65-8a86-438f-8c6a-208124125e23.jpeg"
+ alt="" data-width="1067" data-height="1600" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ </div>
+ <div class="detail-sm nav-card-location">Altadena</div>
+ <h3 class="title-sm nav-card-title">
+ <span class="title-underline">A Hoppin&#39; Good Time at The Bunny Museum</span>
+ </h3>
+ </a> </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ <li class="nav-vertical js-taphover nav-atlas nav-content-vertical">
+ <a class="heading-sm nav-link" href="/articles/all-places-in-the-atlas-on-one-map">
+ <div class="heading-sm nav-link-heading">Places</div>
+ </a>
+ <div class="nav-dropdown">
+ <div class="container nav-dropdown-content">
+ <div class="fake-bg"></div>
+ <div class="row table-display">
+ <div class="col-md-3 left-panel">
+ <ul class="nav-links-column links-column nav-hoverable-links-column">
+ <li>
+ <a class="tab selected js-nav-rollover-tab" data-target="destinations-panel" href="">
+ Top Destinations<span class="icon-arrow-down nav-arrow-right"></span>
+ </a>
+ </li>
+ <li>
+ <a class="tab js-nav-rollover-tab" data-target="recent-places-panel" href="">
+ Newly Added Places<span class="icon-arrow-down nav-arrow-right"></span>
+ </a>
+ </li>
+ <li>
+ <a href="/places?sort=likes_count">
+ Most Popular Places
+ </a>
+ </li>
+ <li>
+ <a class="js-dropdown-random" href="/random">
+ Random Place
+ </a>
+ </li>
+ <li>
+ <a href="/lists">
+ Lists
+ </a>
+ </li>
+ <li>
+ <a href="/itineraries">
+ Itineraries
+ </a>
+ </li>
+ <li>
+ <hr>
+ <a class="add-place js-user-required" data-cause-key="p_add" href=/places/new> <i
+ class="icon-add-place"></i>
+ Add a Place
+ </a>
+ </li>
+ <li id="nav-dropdown-forum-link-wrap">
+ <hr>
+ <a class="js-social-action-tracked nav-dropdown-forum-link" data-service="Forum"
+ data-action="Discuss" data-position="Places Desktop Nav"
+ href="https://community.atlasobscura.com"><i class='material-icons'>forum</i> Visit Our
+ Forums</a>
+ </li>
+ </ul>
+ </div>
+ <div id="recent-places-panel" class="col-md-9 right-panel" style="display:none">
+ <div class="row">
+ <div class="col-md-8">
+ <h4 class="nav-category-heading heading-md-non-uppercase">Newly Added Places</h4>
+ </div>
+ <div class="col-md-4">
+ <a class="detail-sm nav-dropdown-viewall-link" href="/places">View All Places »</a>
+ </div>
+ </div>
+ <div class="row">
+ <a class="col-md-3 nav-card" href="/places/captain-america-statue-brooklyn">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvcGxhY2VfaW1hZ2VzLzQzMWYyZDFkOTdlZTkyNjk0OF9JTUdfMjExMi5KUEciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiMjIyeDE0OCMiXV0/IMG_2112.JPG"
+ alt="" data-width="3264" data-height="2448" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <div class="detail-sm nav-card-location">Brooklyn, New York</div>
+ <h3 class="title-sm nav-card-title"><span class="title-underline">Captain America
+ Statue</span></h3>
+ <div class="lat-lng nav-card-details" aria-hidden="true">
+ 40.6592, -74.0046
+ </div>
+ </a> <a class="col-md-3 nav-card" href="/places/plimoth-plantation">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvcGxhY2VfaW1hZ2VzLzBiZmM5NzAxLTI4MWQtNGI1Ny04YzlmLWQ1ZTFjNjJmM2Y4MTY2ZDI5NDJlYTU2NDNkNjc4Yl8xMjgwcHgtUGxpbW90aF9QbGFudGF0aW9uX0ZlbmNlLmpwZWciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiMjIyeDE0OCMiXV0/1280px-Plimoth_Plantation_Fence.jpeg"
+ alt="" data-width="1280" data-height="853" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <div class="detail-sm nav-card-location">Plymouth, Massachusetts</div>
+ <h3 class="title-sm nav-card-title"><span class="title-underline">Plimoth Plantation</span>
+ </h3>
+ <div class="lat-lng nav-card-details" aria-hidden="true">
+ 41.9382, -70.6254
+ </div>
+ </a> <a class="col-md-3 nav-card" href="/places/the-churchill-arms-pub">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvcGxhY2VfaW1hZ2VzLzMyMGZmNWVjMWJmZTEzYzA1MF8xODgzOTg1NDU0M18yYWExMWM5NzM2X28uanBnIl0sWyJwIiwiY29udmVydCIsIiJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtYXV0by1vcmllbnQiXSxbInAiLCJ0aHVtYiIsIjIyMngxNDgjIl1d/18839854543_2aa11c9736_o.jpg"
+ alt="The pub&#39;s exterior is covered in hundreds of flowers." data-width="1440"
+ data-height="1920" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <div class="detail-sm nav-card-location">London, England</div>
+ <h3 class="title-sm nav-card-title"><span class="title-underline">The Churchill Arms</span>
+ </h3>
+ <div class="lat-lng nav-card-details" aria-hidden="true">
+ 51.5069, -0.1948
+ </div>
+ </a> <a class="col-md-3 nav-card" href="/places/barney-ford-house-museum">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvcGxhY2VfaW1hZ2VzLzFlNjRmNTk2LWQ2NDgtNGRkYy1iNDMzLWFkMDhiZDRkZTk4Y2NlMTFjNDY3M2FkMDczOTU1NF9CYXJuZXlGb3JkMDEuMDIuMjBEaW5pbmdSb29tLmpwZyJdLFsicCIsImNvbnZlcnQiLCIiXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLWF1dG8tb3JpZW50Il0sWyJwIiwidGh1bWIiLCIyMjJ4MTQ4IyJdXQ/BarneyFord01.02.20DiningRoom.jpg"
+ alt="Barney Ford House Museum" data-width="4032" data-height="1960"
+ class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <div class="detail-sm nav-card-location">Breckenridge, Colorado</div>
+ <h3 class="title-sm nav-card-title"><span class="title-underline">Barney Ford House
+ Museum</span></h3>
+ <div class="lat-lng nav-card-details" aria-hidden="true">
+ 39.4806, -106.0455
+ </div>
+ </a> </div>
+ </div>
+ <div id="destinations-panel" class="col-md-9 right-panel">
+ <div class="row">
+ <div class="col-md-8">
+ <h4 class="nav-category-heading heading-md-non-uppercase">Top Destinations</h4>
+ </div>
+ <div class="col-md-4">
+ <a class="detail-sm nav-dropdown-viewall-link" href="/destinations">View All Destinations
+ »</a>
+ </div>
+ </div>
+ <div class="row">
+ <section class="section-top-content">
+ <div class="col-md-3 section-top-content-col">
+ <div class="places-section-top-content-header">
+ <h4 class="section-top-content-title detail-sm">Countries</h4>
+ </div>
+ <ul id="top-destinations-list-nav"
+ class="top-content-list links-column top-content-list-col-sm-2">
+ <li>
+ <a href='/things-to-do/australia' title='Australia'>Australia</a>
+ </li>
+ <li>
+ <a href='/things-to-do/canada' title='Canada'>Canada</a>
+ </li>
+ <li>
+ <a href='/things-to-do/china' title='China'>China</a>
+ </li>
+ <li>
+ <a href='/things-to-do/france' title='France'>France</a>
+ </li>
+ <li>
+ <a href='/things-to-do/germany' title='Germany'>Germany</a>
+ </li>
+ <li>
+ <a href='/things-to-do/india' title='India'>India</a>
+ </li>
+ <li>
+ <a href='/things-to-do/italy' title='Italy'>Italy</a>
+ </li>
+ <li>
+ <a href='/things-to-do/japan' title='Japan'>Japan</a>
+ </li>
+ </ul>
+ </div>
+ <div class="col-md-9 section-top-content-col">
+ <div class="places-section-top-content-header">
+ <h4 class="section-top-content-title detail-sm">Cities</h4>
+ </div>
+ <ul class="top-content-list links-column top-content-list-col-sm-2">
+ <li>
+ <a href='/things-to-do/amsterdam-netherlands' title='Amsterdam'>Amsterdam</a>
+ </li>
+ <li>
+ <a href='/things-to-do/barcelona-spain' title='Barcelona'>Barcelona</a>
+ </li>
+ <li>
+ <a href='/things-to-do/beijing-china' title='Beijing'>Beijing</a>
+ </li>
+ <li>
+ <a href='/things-to-do/berlin-germany' title='Berlin'>Berlin</a>
+ </li>
+ <li>
+ <a href='/things-to-do/boston-massachusetts' title='Boston'>Boston</a>
+ </li>
+ <li>
+ <a href='/things-to-do/budapest-hungary' title='Budapest'>Budapest</a>
+ </li>
+ <li>
+ <a href='/things-to-do/chicago-illinois' title='Chicago'>Chicago</a>
+ </li>
+ <li>
+ <a href='/things-to-do/london-england' title='London'>London</a>
+ </li>
+ <li>
+ <a href='/things-to-do/los-angeles-california' title='Los Angeles'>Los Angeles</a>
+ </li>
+ <li>
+ <a href='/things-to-do/mexico-city-mexico' title='Mexico City'>Mexico City</a>
+ </li>
+ <li>
+ <a href='/things-to-do/montreal-quebec' title='Montreal'>Montreal</a>
+ </li>
+ <li>
+ <a href='/things-to-do/moscow-russia' title='Moscow'>Moscow</a>
+ </li>
+ <li>
+ <a href='/things-to-do/new-orleans-louisiana' title='New Orleans'>New Orleans</a>
+ </li>
+ <li>
+ <a href='/things-to-do/new-york' title='New York City'>New York City</a>
+ </li>
+ <li>
+ <a href='/things-to-do/paris-france' title='Paris'>Paris</a>
+ </li>
+ <li>
+ <a href='/things-to-do/philadelphia-pennsylvania'
+ title='Philadelphia'>Philadelphia</a>
+ </li>
+ <li>
+ <a href='/things-to-do/rome-italy' title='Rome'>Rome</a>
+ </li>
+ <li>
+ <a href='/things-to-do/san-francisco-california' title='San Francisco'>San
+ Francisco</a>
+ </li>
+ <li>
+ <a href='/things-to-do/seattle-washington' title='Seattle'>Seattle</a>
+ </li>
+ <li>
+ <a href='/things-to-do/stockholm-sweden' title='Stockholm'>Stockholm</a>
+ </li>
+ <li>
+ <a href='/things-to-do/tokyo-japan' title='Tokyo'>Tokyo</a>
+ </li>
+ <li>
+ <a href='/things-to-do/toronto-ontario' title='Toronto'>Toronto</a>
+ </li>
+ <li>
+ <a href='/things-to-do/vienna-austria' title='Vienna'>Vienna</a>
+ </li>
+ <li>
+ <a href='/things-to-do/washington-dc' title='Washington, D.C.'>Washington, D.C.</a>
+ </li>
+ </ul>
+ </div>
+ </section>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ <li class="nav-vertical js-taphover nav-foods nav-content-vertical">
+ <a class="heading-sm nav-link" href="/gastro">
+ <div class="heading-sm nav-link-heading">Foods</div>
+ </a>
+ <div class="nav-dropdown">
+ <div class="container nav-dropdown-content">
+ <div class="row table-display">
+ <div class="nav-full-panel col-md-12">
+ <div class="row">
+ <div class="col-md-8">
+ <h4 class="nav-category-heading heading-md-non-uppercase">Newly Added Food & Drink</h4>
+ </div>
+ <div class="col-md-4">
+ <a class="detail-sm nav-dropdown-viewall-link" href="/foods">View All Food &amp; Drink »</a>
+ </div>
+ </div>
+ <div class="row">
+ <a class="nav-card" href="/foods/neua-tune-45-year-soup-wattana-panich">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvdGhpbmdfaW1hZ2VzLzBhYzExMzJhLTg2YjItNDNlOS1hNWFiLTQzYmNkYTNlZDQ4ZWJjNzg5ZDc2ZTNkODUxZTg2Zl9uZXVhdHVuZV9hbGV4eXFqLmpwZWciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiMjIyeDE0OCMiXV0/neuatune_alexyqj.jpeg"
+ alt="" data-width="3024" data-height="4032" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <div class="detail-sm food-card-supertag">Prepared Foods</div>
+ <h3 class="title-sm nav-card-title"><span class="title-underline">Long-Simmering Soup at
+ Wattana Panich</span></h3>
+ </a> <a class="nav-card" href="/foods/chitlins-american-south">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvdGhpbmdfaW1hZ2VzLzFlMTJiZjYwYTM1N2VhZWQzMl9jaGl0bGluczEuanBnIl0sWyJwIiwiY29udmVydCIsIiJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtYXV0by1vcmllbnQiXSxbInAiLCJ0aHVtYiIsIjIyMngxNDgjIl1d/chitlins1.jpg"
+ alt="A bowl of chitlins." data-width="1312" data-height="1312" class="lazy img-responsive"
+ nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <div class="detail-sm food-card-supertag">Prepared Foods</div>
+ <h3 class="title-sm nav-card-title"><span class="title-underline">Chitlins</span></h3>
+ </a> <a class="nav-card" href="/foods/hoppin-john">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvdGhpbmdfaW1hZ2VzLzQzYTA2ODM0LWNiYWItNGY5OC05YTNkLWM5M2ZlZTM5NGViZDg3ZDMzYjNlNmM0ZWM1NjUxMF9Ib3BwaW4gSm9obl9DQyBCeSAyLjAuanBnIl0sWyJwIiwiY29udmVydCIsIiJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtYXV0by1vcmllbnQiXSxbInAiLCJ0aHVtYiIsIjIyMngxNDgjIl1d/Hoppin%20John_CC%20By%202.0.jpg"
+ alt="" data-width="4288" data-height="2848" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <div class="detail-sm food-card-supertag">Prepared Foods</div>
+ <h3 class="title-sm nav-card-title"><span class="title-underline">Hoppin&#39; John</span>
+ </h3>
+ </a> <a class="nav-card" href="/foods/twelve-grapes-new-years-eve">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvdGhpbmdfaW1hZ2VzL2VlZTYxNmVmYzU4NmU0MzZmNl9SYWnMiG1fZGVfQ2FwX2QnQW55LmpwZyJdLFsicCIsImNvbnZlcnQiLCIiXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLWF1dG8tb3JpZW50Il0sWyJwIiwidGh1bWIiLCIyMjJ4MTQ4IyJdXQ/Rai%CC%88m_de_Cap_d%27Any.jpg"
+ alt="Are you feeling lucky?" data-width="945" data-height="633"
+ class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <div class="detail-sm food-card-supertag">Ritual &amp; Medicinal</div>
+ <h3 class="title-sm nav-card-title"><span class="title-underline">Twelve Grapes</span></h3>
+ </a> <a class="nav-card" href="/foods/reveillon-dinner">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvdGhpbmdfaW1hZ2VzLzU3ODNhYzVmMzcxZjYwNTc1NV9SZXZlaWxsb24yX0RhdmlkUmljaG1vbmQuanBnIl0sWyJwIiwiY29udmVydCIsIiJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtYXV0by1vcmllbnQiXSxbInAiLCJ0aHVtYiIsIjIyMngxNDgjIl1d/Reveillon2_DavidRichmond.jpg"
+ alt="A modern spread of Reveillon delights." data-width="1170" data-height="1035"
+ class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <div class="detail-sm food-card-supertag">Ritual &amp; Medicinal</div>
+ <h3 class="title-sm nav-card-title"><span class="title-underline">Réveillon Dinner</span>
+ </h3>
+ </a> </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ <li class="nav-vertical js-taphover nav-articles nav-content-vertical">
+ <a class="heading-sm nav-link" href="/articles">
+ <div class="heading-sm nav-link-heading">Stories</div>
+ </a>
+ <div class="nav-dropdown">
+ <div class="container nav-dropdown-content">
+ <div class="row table-display">
+ <div class="nav-full-panel">
+ <div class="row">
+ <div class="col-md-8">
+ <h4 class="nav-category-heading heading-md-non-uppercase">Most Recent Stories</h4>
+ </div>
+ <div class="col-md-4">
+ <a class="detail-sm nav-dropdown-viewall-link" href="/articles">View All Stories »</a>
+ </div>
+ </div>
+ <div class="row">
+ <a class="col-md-3 nav-card" href="/articles/meet-diving-grannies-new-caledonia">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzLzQ4YmUzNTQ3LTU1NmUtNDAzZi1iMjI1LWRhNzA4Y2QzNTVmY2RhOTZlMDAwYzljN2I4OGE5NF9kaXZpbmcuZ3Jhbm5pZXMuY3JvcHBlZC50aW1lc3RhbXAubGVhZC5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiMjIyeDE0OCMiXV0/diving.grannies.cropped.timestamp.lead.jpg"
+ alt="Two of the &quot;Fantastic Grandmothers&quot; document a greater sea snake, photo-identifying its uniquely patterned tail for their growing database."
+ data-width="1828" data-height="1223" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <h3 class="title-sm nav-card-title"><span class="title-underline">The Snorkeling Grannies of
+ New Caledonia</span></h3>
+ <div class="detail-sm nav-card-details js-time-ago"
+ data-timestamp="2020-01-27 22:10:00 UTC"></div>
+ </a> <a class="col-md-3 nav-card" href="/articles/were-there-witchhunts-in-south-america">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzL2ZlYzA3N2Y5LTcxOWEtNDI1My04ODUwLTkxMDFiZmExYjgxNDlkNGJkM2ZiNDgzMzBhNzBlYl9BdGxhc19PYnNjdXJhX1dpdGNoZXNfTEFfMS5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiMjIyeDE0OCMiXV0/Atlas_Obscura_Witches_LA_1.jpg"
+ alt="According to Inquisition records, men were often fearful that women would slip magic potions into their morning hot chocolate. "
+ data-width="1280" data-height="853" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <h3 class="title-sm nav-card-title"><span class="title-underline">The Chocolate-Brewing
+ Witches of Colonial Latin America</span></h3>
+ <div class="detail-sm nav-card-details js-time-ago"
+ data-timestamp="2020-01-27 21:51:00 UTC"></div>
+ </a> <a class="col-md-3 nav-card" href="/articles/ww2-bombs-berlin">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzLzYzZjU4NTNjLWM5ODAtNDAyYi05YjRhLTQ1ZmVjODRhYzhmNzcyNTBiMjhlYzliNTNkNTRhNF9HZXR0eUltYWdlcy0xMTk1MTkzNzc5X2Nyb3AuanBnIl0sWyJwIiwiY29udmVydCIsIiJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtYXV0by1vcmllbnQiXSxbInAiLCJ0aHVtYiIsIjIyMngxNDgjIl1d/GettyImages-1195193779_crop.jpg"
+ alt="German authorities with a 500-pound bomb discovered during construction work. Such discoveries are regular occurrences in cities that were bombed during World War II. "
+ data-width="1280" data-height="888" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <h3 class="title-sm nav-card-title"><span class="title-underline">What Happens When They
+ Find a World War II Bomb Down the Street</span></h3>
+ <div class="detail-sm nav-card-details js-time-ago"
+ data-timestamp="2020-01-27 20:07:00 UTC"></div>
+ </a> <a class="col-md-3 nav-card" href="/articles/eritrean-tank-graveyard">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzL2I4YTI1Y2RkLWM5MjktNGRiZi05YTg3LTNmMzM0YTk1NDYwN2NiNTU2NmY4NDc3ZGI0YzMwNF8xNjAwcHgtUGFzc2luZ190aGVfdGFua19ncmF2ZXlhcmQuanBnIl0sWyJwIiwiY29udmVydCIsIiJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtYXV0by1vcmllbnQiXSxbInAiLCJ0aHVtYiIsIjIyMngxNDgjIl1d/1600px-Passing_the_tank_graveyard.jpg"
+ alt="A cyclist rides past the tank graveyard in 2016." data-width="1600"
+ data-height="1060" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <h3 class="title-sm nav-card-title"><span class="title-underline">This Tank Graveyard Is a
+ Monument to Eritrea&#39;s Struggle for Liberation</span></h3>
+ <div class="detail-sm nav-card-details js-time-ago"
+ data-timestamp="2020-01-27 20:05:00 UTC"></div>
+ </a> <a class="col-md-3 nav-card"
+ href="/articles/french-missionary-english-duke-bring-back-chinese-deer">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzLzFiM2EzOGE0LTM2OGQtNDRkZi1iNzVhLWYyNzAwM2I3MjcwNjhhMjg0MTAxMWMwMjZiZTFkYV9kZWVyLmZvcmVzdC5ncmF6aW5nLmpwZyJdLFsicCIsImNvbnZlcnQiLCIiXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLWF1dG8tb3JpZW50Il0sWyJwIiwidGh1bWIiLCIyMjJ4MTQ4IyJdXQ/deer.forest.grazing.jpg"
+ alt="Today nearly 7,000 Père David&#39;s deer roam the wetlands of China. "
+ data-width="800" data-height="533" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ <h3 class="title-sm nav-card-title"><span class="title-underline">The French Missionary and
+ the English Duke Who Saved a Chinese Deer From Extinction</span></h3>
+ <div class="detail-sm nav-card-details js-time-ago"
+ data-timestamp="2020-01-24 23:20:00 UTC"></div>
+ </a> </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ <li class="nav-vertical nav-videos nav-content-vertical js-taphover">
+ <a class="heading-sm nav-link" href="/videos">
+ <div class="heading-sm nav-link-heading">Videos</div>
+ </a>
+ <div class="nav-dropdown">
+ <div class="container nav-dropdown-content">
+ <div class="row table-display">
+ <div class="nav-full-panel">
+ <div class="row">
+ <div class="col-md-8">
+ <h4 class="nav-category-heading heading-md-non-uppercase">Most Recent Videos</h4>
+ </div>
+ <div class="col-md-4">
+ <a class="detail-sm nav-dropdown-viewall-link" href="/videos">View All Videos »</a>
+ </div>
+ </div>
+ <div class="row">
+ <a class="col-md-3 nav-card" href="/videos/welsh-town-all-books">
+ <div class="video-thumb-container">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsIjIwMjAvMDEvMjIvMTUvMzkvMTIvNDhlYTcxNTItYWI1MC00NGVjLTgyN2YtYzVjY2E1NTY5NTMyL2hheV9vbl93eWVfc3RpbGxfMDEuanBnIl0sWyJwIiwiY29udmVydCIsIiJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtYXV0by1vcmllbnQiXSxbInAiLCJ0aHVtYiIsIjQ0MHgyNDgjIl1d"
+ alt="" data-width="1920" data-height="1080" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ </div>
+ <h3 class="title-sm nav-card-title"><span class="title-underline">This Tiny Welsh Town Is
+ Brimming With Books</span></h3>
+ <div class="detail-sm nav-card-details video-duration">5:04</div>
+ </a> <a class="col-md-3 nav-card"
+ href="/videos/see-cars-roll-uphill-on-scotland-s-electric-brae">
+ <div class="video-thumb-container">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsIjIwMjAvMDEvMjIvMTUvNDIvMjYvOWVkZDBkNDYtZDhkOC00OWI0LWJmNWUtYTE2N2VkMjk2M2FmL2FvX2VsZWN0cmljX2JyYWVfZmJfMDExMzE5X3YxLjAwXzAyXzU0XzIxLnN0aWxsMDAxXzcyMC5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiNDQweDI0OCMiXV0"
+ alt="" data-width="720" data-height="405" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ </div>
+ <h3 class="title-sm nav-card-title"><span class="title-underline">On Scotland&#39;s Electric
+ Brae, Cars Really Do Seem to Roll Uphill</span></h3>
+ <div class="detail-sm nav-card-details video-duration">4:54</div>
+ </a> <a class="col-md-3 nav-card"
+ href="/videos/dynasty-handbag-los-angeles-performance-artist">
+ <div class="video-thumb-container">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsIjIwMjAvMDEvMDgvMjAvMTIvMzAvMTY2ZTA0ZWEtYjYwMy00MWI3LWEyM2QtNmY3NTY0YTY0ODZiL0R5bmFzdHkgSGFuZGJhZyBTdGlsbCAwMS5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiNDQweDI0OCMiXV0"
+ alt="" data-width="1920" data-height="1080" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ </div>
+ <h3 class="title-sm nav-card-title"><span class="title-underline">Meet Dynasty Handbag,
+ L.A.’s Queen of Weird</span></h3>
+ <div class="detail-sm nav-card-details video-duration">6:14</div>
+ </a> <a class="col-md-3 nav-card" href="/videos/the-unusual-italian-village-in-wales">
+ <div class="video-thumb-container">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsIjIwMTkvMTIvMjAvMjAvMjEvMTEvMzc5ZWFlMzctZmI3My00Zjc5LTlkMjItMzMzZWNkZTc4YmNlL0FPIFBvcnRtZWlyaW9uIFN0aWxsIDAyLmpwZyJdLFsicCIsImNvbnZlcnQiLCIiXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLWF1dG8tb3JpZW50Il0sWyJwIiwidGh1bWIiLCI0NDB4MjQ4IyJdXQ"
+ alt="" data-width="1920" data-height="1080" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ </div>
+ <h3 class="title-sm nav-card-title"><span class="title-underline">Why There&#39;s an
+ &#39;Italian&#39; Village in Wales</span></h3>
+ <div class="detail-sm nav-card-details video-duration">5:27</div>
+ </a> <a class="col-md-3 nav-card" href="/videos/surfing-alaska-famous-bore-tide">
+ <div class="video-thumb-container">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsIjIwMTkvMTIvMjAvMjAvMTMvNDAvZWExMzNhMTQtMDRmNy00ODZkLTliNDYtMDRlNmQ1M2JiMzU4L0FPIEJvcmUgVGlkZSBTdGlsbCAwMy5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiNDQweDI0OCMiXV0"
+ alt="" data-width="3840" data-height="2160" class="lazy img-responsive" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ </div>
+ <h3 class="title-sm nav-card-title"><span class="title-underline">Surfing Alaska&#39;s
+ Unique Bore Tide</span></h3>
+ <div class="detail-sm nav-card-details video-duration">4:14</div>
+ </a> </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ <li class="hidden-md hidden-lg" style="width: 100%;">
+
+ <div class="nav-vertical nav-vertical-sessions-m">
+ <div class="nav-session-links-wrap-m">
+
+ <a href="/sign-in" class="heading-sm nav-session-link-m nav-link">
+ <i class="icon-profile nav-session-icon"></i>Sign In
+ </a><i class="or-pipe or-pipe-sessions-m"></i>
+ <a href="/join" class="heading-sm nav-session-link-m nav-link">
+ Join
+ </a>
+ </div>
+ </div>
+ </li>
+ </ul>
+ <ul class="nav-right-section">
+ <li class="user-cell">
+ <div id="nav-profile-menu" class="dropdown">
+ <div class="nav-session-link detail-sm profile-dropdown js-profile-dropdown js-taphover" tabindex="0"
+ aria-label="User Profile Options" aria-haspopup="true">
+ <a href="javascript:void(0)" class="profile-nav-hover-target" aria-label="Account options">
+ <i class="icon user-icon icon-profile"></i>
+ </a>
+ <div class="profile-dropdown-menu js-profile-dropdown-menu dropdown-menu dropdown-menu-right"
+ tabindex="0">
+ <a id="nav-sign-in-link" class="nav-session-link detail-sm dropdown-item" href="/sign-in">Sign
+ In</a>
+ <hr>
+ <a id="nav-sign-up-link" class="nav-session-link detail-sm dropdown-item" href="/join">Join</a>
+ </div>
+ </div>
+ </div>
+ </li>
+ <li class="search-cell">
+ <div class="nav-search">
+ <a id="d-search-dropdown-link" class="heading-sm nav-link js-launch-search-link"
+ aria-label="Open Site Search Form" data-search-dock="nav-dropdown-search-dock"
+ data-category="Search Suggest" data-action="Opened Search Form" data-label="Global Search"
+ href="javascript:void(0)">
+ <i class="icon-search"></i>
+ <i class="icon-menu-close"></i>
+ </a>
+ <div class="sitewide-hero-search vue-js-nav-search-bar desktop-search">
+ <div class="hero-search-bar-bg"></div>
+ <div class="container hero-search-wrapper js-hero-search-wrapper">
+ <div class="row">
+ <div class="col-md-10 col-md-offset-1 hero-search-bar">
+ <form autocomplete="off" class="js-search-form-to-submit js-hero-search" action="/search"
+ accept-charset="UTF-8" method="get"><input name="utf8" type="hidden" value="&#x2713;" />
+ <search-input></search-input>
+ </form>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-10 col-md-offset-1">
+ <search-suggestions></search-suggestions>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </nav>
+ <div id="search-dock-m" class="container-fluid search-dock"></div>
+ <div class="m-nearby-search">
+ <a class="js-search-nearby btn btn-places btn-icon js-link-tracked" data-category="Search Nearby"
+ data-action="Submitted" data-label="articles" href="javascript:void(0)">
+ <i class="icon-nearby-place"></i>
+ What's near me?
+ </a>
+ <a id="delta-nearby-wrap" href="https://ad.doubleclick.net/ddm/clk/416154737;217514481;g" target="_blank"
+ rel="noopener" class="hidden non-decorated-link">
+ <div class="cta-xs">Brought to you by</div>
+ <div style="width: 130px; margin-left: 15px;">
+ <img class="img-responsive"
+ src="https://assets.atlasobscura.com/media/W1siZnUiLCJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vYXRsYXMtZGV2L21pc2Mvc3BvbnNvci1vbmUtb2Zmcy9kZWx0YV9sb2dvLnBuZyJdLFsicCIsInRodW1iIiwiMjYweD4iXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLXN0cmlwIl1d/delta_logo.png"
+ alt="Delta logo" />
+ </div>
+ </a>
+ </div>
+ </div>
+ <div class="oop-tracking-pixel">
+ <div class="htl-ad" data-unit="1x1_tracking_pixel" data-ref="1x1_tracking_pixel_div" data-sizes="1x1"
+ data-prebid="1x1_tracking_pixel" data-eager></div>
+ </div>
+ </header>
+ <div id="page-content">
+ <article class="athanasius item-content js-article-content article-content article--content ">
+ <div class="ArticleHeaderBg ArticleHeaderBg--dark ArticleHeaderBg--wide-hero ArticleHeaderBg-- ">
+ <header class="ArticleHeader js-item-header ArticleHeader--wide-hero ">
+ <div class="container">
+ <h1 class="ArticleHeader__title">The Missing Grave of Margaret Corbin, Revolutionary War Veteran</h1>
+ <h2 class="ArticleHeader__subtitle">
+ She&#8217;s the only woman veteran honored with a monument at West Point. But where was she buried?
+ </h2>
+ <div class="ArticleHeader__end-matter">
+ <div class="ArticleHeader__byline-dateline">
+ <span class="ArticleHeader__byline">by <a href="/users/shanecashman?view=articles">Shane
+ Cashman</a></span>
+ <span class="ArticleHeader__pub-date">January 14, 2020</span>
+ </div>
+ <div class="ArticleHeader__social hidden-sm hidden-xs">
+ <div class="SocialLinks ">
+ <a class="js-share-button-facebook js-social-action-tracked" data-position="Header"
+ data-service="Facebook" data-action="Share" aria-label="Share on Facebook" href="">
+ <i class="icon icon-facebook"></i>
+ </a>
+ <a target="_blank" class="js-social-action-tracked js-social-ask-for-follow" data-position="Header"
+ data-service="Twitter" data-action="Share" aria-label="Share on Twitter"
+ href="https://twitter.com/share?text=The%20Missing%20Grave%20of%20Margaret%20Corbin%2C%20Revolutionary%20War%20Veteran%20@atlasobscura&amp;count=none&amp;url=https://www.atlasobscura.com/articles/margaret-corbin-grave-west-point">
+ <i class="icon icon-twitter"></i>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </header>
+ </div>
+ <div id="btf-nav" class="container-fluid hidden athanasius">
+ <div class="row">
+ <div class="mini-nav-wrapper Subnav--with-depth">
+ <div class="container-fluid">
+ <nav>
+ <div class="mini-nav-contents">
+ <div id="btf-nav-home" class="btf-nav-square hidden-sm hidden-xs">
+ <a class="mini-nav-logo-link non-decorated-link" title="Atlas Obscura" href="/"><i
+ class="icon icon-atlas-icon"></i></a>
+ </div>
+ <div id="btf-nav-title" class="detail hidden-sm hidden-xs">
+ The Missing Grave of Margaret Corbin, Revolutionary War Veteran
+ </div>
+ <div class="social-links">
+ <div class="btf-nav-square first-in-series">
+ <a href="javascript:void(0)"
+ class="icon icon-facebook link-facebook social-link js-social-action-tracked js-share-button-facebook"
+ data-position="Sticky Header" data-service="Facebook" data-action="Share" target="_blank"
+ aria-label="Share on Facebook"></a>
+ </div>
+ <div class="btf-nav-square">
+ <a href="https://twitter.com/share?text=The%20Missing%20Grave%20of%20Margaret%20Corbin%2C%20Revolutionary%20War%20Veteran%20@atlasobscura&amp;count=none&amp;url=https://www.atlasobscura.com/articles/margaret-corbin-grave-west-point"
+ class="icon icon-twitter social-link link-twitter js-social-action-tracked js-social-ask-for-follow"
+ target="_blank" data-position="Sticky Header" data-service="Twitter" data-action="Share"
+ aria-label="Share on Twitter"></a>
+ </div>
+ <div class="btf-nav-square">
+ <a href="https://www.reddit.com/submit?url=https%3A%2F%2Fwww.atlasobscura.com%2Farticles%2Fmargaret-corbin-grave-west-point"
+ class="icon icon-reddit link-reddit social-link js-social-action-tracked"
+ data-position="Sticky Header" data-service="reddit" data-action="Share" target="_blank"
+ aria-label="Share on Reddit"></a>
+ </div>
+ <div class="btf-nav-square">
+ <a href="mailto:?subject=The Missing Grave of Margaret Corbin, Revolutionary War Veteran&body=Discovered on Atlas Obscura: https://www.atlasobscura.com/articles/margaret-corbin-grave-west-point"
+ class="icon icon-envelope link-email social-link js-social-action-tracked js-btn-email-share hidden-md hidden-lg"
+ data-position="Sticky Header" data-service="Email" data-action="Send"
+ aria-label="Email this"></a>
+ </div>
+ </div>
+ </div>
+ </nav>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="article-container" class="container">
+ <div class="row">
+ <div id="article-hero" class="hero-img-wide col-xs-12 col-md-8 hidden-print ArticleHero ">
+ <img
+ alt="In 1926, the Daughters of the American Revolution joined forces with the U.S. Military Academy to exhume the supposed burial site of Margaret Corbin."
+ data-width="2304" data-height="1583" width="1280" class="article-image"
+ src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzLzkwYzgyMzI4LThlMDUtNGRiNS05MDg3LTUzMGUxZTM5N2RmMmVkOTM5ZDM4MGM4OTIxYTQ5MF9EQVIgZXhodW1hdGlvbiBvZiBNYXJnYXJldCBDb3JiaW4gZ3JhdmUgMTkyNi5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiMTI4MHg-Il1d/DAR%20exhumation%20of%20Margaret%20Corbin%20grave%201926.jpg" />
+ <div class="article-caption-pre-rd article-hero-img-caption">
+ In 1926, the Daughters of the American Revolution joined forces with the U.S. Military Academy to exhume
+ the supposed burial site of Margaret Corbin. <span class='caption-credit'>Daughters of the American
+ Revolution</span>
+ </div>
+ </div>
+ </div>
+ <div class="row MainContentRow -- ">
+ <div id="ArticleLeftRail" class="article-left-siderail social-siderail-l hidden-print col-md-2">
+ <div class="siderail-placement siderail-associated-content-cards">
+ <div class="siderail-title o-heading fs--19">In This Story</div>
+ <div class="siderail-cards-container">
+ <div class="CardWrapper --PlaceWrapper js-CardWrapper --small-wrapper">
+ <div class="CardActionBtns">
+ <div class="CardActionBtns__positioner">
+ <div data-item-type="Place" data-place-title="Margaret Corbin&#39;s Grave"
+ data-place-city="West Point" data-place-country="United States" data-place-id="35141"
+ class="Card__action-btns vue-js-been-there-everywhere-place">
+ <been-there-everywhere></been-there-everywhere>
+ </div>
+ </div>
+ </div>
+ <a class="Card --content-card-v2 --content-card-item Card--small" data-type="Place"
+ data-item-id="35141" data-gtm-content-type="Place" data-lat="41.398684" data-lng="-73.967402"
+ data-city="West Point" data-state="New York" data-country="United States"
+ href="/places/margaret-corbin-grave">
+ <figure class="Card__figure js-Card__figure --content-card-figure js-content-card-figure">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvcGxhY2VfaW1hZ2VzL2RkZmJiNGI4LTZhYTktNDNiOC05ZTIxLWViY2NhMzdjYzE0ZDMwMmVmYjI2MGQ4MzllNzU4MF9NYXJnYXJldCBDb3JiaW4gUmVkZWRpY2F0aW9uIENlcmVtb255XzUuMS4xOC5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiNjAweDQwMCMiXV0/Margaret%20Corbin%20Rededication%20Ceremony_5.1.18.jpg"
+ alt="" data-width="960" data-height="640"
+ class="lazy Card__img u-img-responsive --img-responsive --content-card-img" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ </figure>
+ <div class="Card__content-wrap --content-card-text">
+ <div class="Card__hat --place">Place</div>
+ <h3 class="Card__heading --content-card-v2-title">
+ <span>Margaret Corbin&#39;s Grave</span>
+ </h3>
+ <div class="Card__content js-subtitle-content">
+ West Point’s only monument to a woman veteran stands above an empty grave.
+ </div>
+ </div>
+ </a>
+ </div>
+ <div class="CardWrapper --GeoWrapper js-CardWrapper --small-wrapper">
+ <a class="Card --content-card-v2 --content-card-item Card--small Card--flipped"
+ data-gtm-content-type="Geo" data-lat="40.712784" data-lng="-74.005941" data-state="New York"
+ data-country="United States" data-global-region="North America" href="/things-to-do/new-york-state">
+ <figure class="Card__figure js-Card__figure -- content-card-figure js-content-card-figure">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvcGxhY2VfaW1hZ2VzLzNiY2JkODcwMjMxYjNmNzM1NV81MTU5MTkxMjkwXzk4MDRlYTQyYjJfby5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiNjAweDQwMCMiXV0/5159191290_9804ea42b2_o.jpg"
+ alt="" data-width="1011" data-height="671"
+ class="lazy Card__img u-img-responsive --img-responsive --content-card-img" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ </figure>
+ <div class="Card__content-wrap --content-card-text">
+ <div class="Card__hat"> Destination Guide</div>
+ <h3 class="Card__heading --content-card-v2-title">
+ <span>New York State</span>
+ </h3>
+ </div>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="content-body col-md-6">
+ <section id="article-body" class="ArticleBody ArticleBody__default article--body ">
+ <p class="item-body-text-graf"><span class="section-start-text">In 2016, five days after
+ </span>Thanksgiving, Margaret Corbin&#8217;s grave was dug up for the second time since her death in
+ 1800. It began by accident. Contractors were working on a retaining wall near the West Point Cemetery,
+ at the U.S. Military Academy, when a hydraulic excavator got too close and chewed through the grave.</p>
+ <p class="item-body-text-graf">As soon as they noticed bones spilling from the soil, they alerted the
+ military police. The plot was quickly cordoned off, her monument was wrapped in tarp, and rumors started
+ to spread about Corbin&#8217;s resting place&#8212;that is, if it even <em>was</em> her resting place.
+ When forensic archaeologists arrived at the scene, they were perplexed: The bones seemed oddly large.
+ </p>
+ <p class="item-body-text-graf">The monument to Margaret Corbin is West Point&#8217;s only monument to a
+ woman veteran, and it greets visitors near the main gate, just feet from a neoclassical chapel. It faces
+ Washington Road, where the Academy&#8217;s top brass live, and depicts Corbin in a long dress, operating
+ a cannon as her long hair and cape fly in the wind. She wears a powder horn and holds a rammer to load
+ cannonballs; the rest of the rather cramped cemetery sprawls out behind her. The monument portrays the
+ moments before Corbin became a prisoner of war.</p>
+ <figure class=" contains-caption "><img class="article-image with-structured-caption lazy"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png"
+ alt="On the West Point monument, Corbin wears a long dress and a powder horn, and she operates a cannon while her long hair flies in the wind."
+ width="auto" data-kind="article-image" id="article-image-71546"
+ data-src="https://assets.atlasobscura.com/article_images/71546/image.jpg">
+ <figcaption class="caption structured-caption noskim">On the West Point monument, Corbin wears a long
+ dress and a powder horn, and she operates a cannon while her long hair flies in the wind. <span
+ class="caption-credit">Science History Images / Alamy Stock Photo</span></figcaption>
+ </figure>
+ <p class="item-body-text-graf">The story goes that Corbin joined her husband, John, to fight in the
+ American Revolution. At the time, many women followed their husbands to war, where they were commonly
+ known as &#8220;camp followers.&#8221; Typically, they foraged for food, cooked, and did laundry. Before
+ Martha Washington was the United States&#8217; first first lady, she was also a camp follower. In fact,
+ she and Margaret were with the same company&#8212;though the two experienced different lives, since
+ George was a general, and John manned a cannon.</p>
+ <p class="item-body-text-graf">At the Battle of Fort Washington, on November 16, 1776, in what is now
+ Washington Heights, the British and Hessians advanced far enough to make the Continental Army&#8217;s
+ position untenable. George Washington retreated with his forces to White Plains; John Corbin was shot
+ dead at his cannon. But Margaret was there to jump into John&#8217;s position and help fire the cannon.
+ During the battle, her jaw and shoulder were seriously injured, and grapeshot tore off part of her
+ breast. Despite the Continental Army&#8217;s efforts, the fort was soon surrendered, and Corbin was
+ captured along with approximately 2,837 soldiers.</p>
+ <figure class="article-image-full-width contains-caption "><img
+ class="article-image with-structured-caption lazy"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png"
+ alt="A watercolor by Thomas Davies depicting the attack on Fort Washington by the British and Hessian Brigades. Margaret Corbin was taken prisoner after fighting in the battle."
+ width="auto" data-kind="article-image" id="article-image-71538"
+ data-src="https://assets.atlasobscura.com/article_images/lg/71538/image.jpg">
+ <figcaption class="caption structured-caption noskim">A watercolor by Thomas Davies depicting the attack
+ on Fort Washington by the British and Hessian Brigades. Margaret Corbin was taken prisoner after
+ fighting in the battle. <span class="caption-credit">Alamy Stock Photo</span></figcaption>
+ </figure>
+ <p class="item-body-text-graf">The British may have been unsure what to do with an injured woman, because
+ she was released fairly soon after the battle. The ordeal was one of many traumas in her life: According
+ to records collected by the <a
+ href="http://www.historichudsonhighlands.org/historian-s-office.html">historian</a> Stella Bailey,
+ Margaret was only five years old when her father was killed in a conflict with Native Americans in
+ Pennsylvania, where they lived. Her mother was kidnapped, and Margaret and her brother moved in with an
+ uncle. They never saw her again.</p>
+ <p class="item-body-text-graf">After Corbin&#8217;s return, she joined the Corps of Invalids, a group of
+ wounded soldiers that were still able to contribute to the war effort. They were stationed at West
+ Point, New York, where Corbin became known as a cantankerous woman who had a tough time making a home
+ for herself in the neighboring village of Highland Falls. She moved between various local families who
+ tried to care for her. Having witnessed her husband&#8217;s death and sustaining wounds, she was
+ probably in constant mental and physical pain.</p>
+ <hr class="baseline-grid-hr">
+ <p class="item-body-text-graf section-break-graf"><span class="section-start-text">When Margaret Corbin
+ died in </span>1800, she was buried in a pauper&#8217;s cemetery in Highland Falls, just three miles
+ from West Point. But in 1926, the national society of women known as the Daughters of the American
+ Revolution saw to it that Corbin would earn her vaunted cemetery plot. The society, which is made up of
+ women who can trace their lineage to participants in the American Revolution, was celebrating the
+ sesquicentennial of American independence, and saw Corbin as the consummate symbol of both their
+ organization and the Revolution. A year-long effort convinced the U.S. Military Academy to help them
+ exhume and transport the remains to the prestigious cemetery, to be reburied with a military funeral.
+ </p>
+ <figure class="article-image-full-width contains-caption "><img
+ class="article-image with-structured-caption lazy"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png"
+ alt="This sign directs visitors to the United States Military Academy to the purported site of Margaret Corbin's grave."
+ width="auto" data-kind="article-image" id="article-image-71506"
+ data-src="https://assets.atlasobscura.com/article_images/lg/71506/image.jpg">
+ <figcaption class="caption structured-caption noskim">This sign directs visitors to the United States
+ Military Academy to the purported site of Margaret Corbin&#8217;s grave. <a class="caption-credit"
+ rel="nofollow" target="_blank"
+ href="https://commons.wikimedia.org/wiki/File:Margaret_Corbin_Historical_Road_Marker.JPG">Ahodges7 /
+ CC BY-SA 3.0</a></figcaption>
+ </figure>
+ <p class="item-body-text-graf">Exhumation was not a simple task: By the time the DAR began their campaign
+ to move Corbin, the location of her exact burial was known only by word-of-mouth, passed down through
+ generations. In collaboration with West Point, the Daughters found the great-grandson of the man who
+ supposedly dug Corbin&#8217;s original grave, a steamboat captain by the name of Farout. Her burial site
+ was apparently marked by the stump of a cedar tree; during the exhumation process, the gravedigger
+ accidentally drove the shovel through the skull. Still, the Army Surgeon reported injuries to the
+ skeleton that were consistent with grapeshot. The remains were given a new, flag-draped casket and
+ delivered to West Point by horse-drawn hearse.</p>
+ <p class="item-body-text-graf">Every year since then, the Daughters have gathered at Corbin&#8217;s
+ monument for Margaret Corbin Day. On the first Tuesday of May, the Daughters fill the chapel, share
+ Corbin&#8217;s story, sing hymns, and stand at the grave while soldiers perform a 21-gun salute.</p>
+ <figure class="article-image-full-width contains-caption "><img
+ class="article-image with-structured-caption lazy"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png"
+ alt="A horse-drawn hearse carried a flag-draped casket that was said to contain Corbin&#8217;s remains."
+ width="auto" data-kind="article-image" id="article-image-71497"
+ data-src="https://assets.atlasobscura.com/article_images/lg/71497/image.jpg">
+ <figcaption class="caption structured-caption noskim">A horse-drawn hearse carried a flag-draped casket
+ that was said to contain Corbin&#8217;s remains. <span class="caption-credit">Daughters of the
+ American Revolution</span></figcaption>
+ </figure>
+ <p class="item-body-text-graf">After the U.S. Military Academy unintentionally reopened the grave beneath
+ Corbin&#8217;s monument in 2016, they decided to conduct an emergency forensic archaeological
+ excavation. They enlisted the help of Elizabeth A. DiGangi, an anthropology professor at Binghamton
+ University, and Michael K. Trimble, an archaeologist for the Army Corps of Engineers. Almost
+ immediately, the pair noticed that the size of the bones didn&#8217;t match Corbin&#8217;s description.
+ Corbin was reportedly a stout woman. &#8220;One of the first bones I saw when I was on site was the
+ humerus, or upper arm bone,&#8221; DiGangi says. &#8220;It was very large, which is not what you would
+ expect with an arm bone from a woman.&#8221;</p>
+ <p class="item-body-text-graf">DiGangi took the remains to her laboratory at Binghamton University to do a
+ full analysis. Some worried that other remains were mixed up with Corbin&#8217;s. (In the past, West
+ Point has discovered unknown remains when they&#8217;ve broken new ground for construction.) Ultimately,
+ DiGangi&#8217;s analysis revealed something even more shocking: The remains in Corbin&#8217;s grave
+ actually came from an adult male. DiGangi determined that it was a large man, who could&#8217;ve been
+ anywhere from five-foot-seven to six and a half feet tall. The remains of Margaret Corbin were not in
+ Margaret Corbin&#8217;s grave.</p>
+ <figure class="article-image-full-width contains-caption "><img
+ class="article-image with-structured-caption lazy"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png"
+ alt="In 1926, the remains from Highland Park were reinterred at West Point, and sat at the foot of the Margaret Corbin monument until 2016."
+ width="auto" data-kind="article-image" id="article-image-71024"
+ data-src="https://assets.atlasobscura.com/article_images/lg/71024/image.jpg">
+ <figcaption class="caption structured-caption noskim">In 1926, the remains from Highland Park were
+ reinterred at West Point, and sat at the foot of the Margaret Corbin monument until 2016. <span
+ class="caption-credit">Daughters of the American Revolution</span></figcaption>
+ </figure>
+ <p class="item-body-text-graf">Once the archaeological excavation teams <a
+ href="https://www.dar.org/sites/default/files/FinalReportWestPointBurialRecovery.pdf">completed</a>
+ their reports, the Army National Cemeteries contacted the Daughters of the American Revolution. They
+ wanted a meeting at DAR headquarters in Washington, DC.</p>
+ <p class="item-body-text-graf">Jennifer Minus, the head of the New York chapter of the DAR, was among
+ those present at the meeting. Minus, a graduate of West Point and a former member of the Corbin Forum, a
+ club for cadet women, knew her Corbin history better than most. She asked how it could&#8217;ve been a
+ man in the grave, if in 1926 the Army surgeon said that grapeshot injuries were present. In her report,
+ DiGangi explains that what the surgeon considered a grapeshot injury was, in fact, post-mortem damage to
+ the remains.</p>
+ <p class="item-body-text-graf">So where is Margaret Corbin? Since the attempted reburial of Corbin&#8217;s
+ remains, in 1926, her original gravesite in Highland Falls has been lost to time. Sometime in the 1970s,
+ the town dropped a sewage plant where many believe it was once located. Yet Minus remains optimistic
+ that Corbin&#8217;s remains will one day be found.</p>
+ <figure class="article-image-full-width contains-caption "><img
+ class="article-image with-structured-caption lazy"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png"
+ alt="At left, another monument that pays homage to Margaret Corbin, near the site where she took over her husband's cannon; at right, a view of her monument at West Point."
+ width="auto" data-kind="article-image" id="article-image-71513"
+ data-src="https://assets.atlasobscura.com/article_images/lg/71513/image.jpg">
+ <figcaption class="caption structured-caption noskim">At left, another monument that pays homage to
+ Margaret Corbin, near the site where she took over her husband&#8217;s cannon; at right, a view of her
+ monument at West Point. <a class="caption-credit" rel="nofollow" target="_blank"
+ href="https://commons.wikimedia.org/wiki/Category:Margaret_Corbin#/media/File:2015_Fort_Tryon_Park_Margaret_Corbin_memorial.">Beyond
+ My Ken / CC BY-SA 4.0; Ahodges7 / CC BY-SA 3.0</a></figcaption>
+ </figure>
+ <p class="item-body-text-graf">As upsetting as it was to learn that her remains were missing, the
+ Daughters also tried to see the discovery as an opportunity to spread Margaret&#8217;s story. It&#8217;s
+ as though they picked up right where the 1926 DAR members left off. Minus formed an unofficial Margaret
+ Corbin Task Force, drawing on the strengths of DAR members: One was a genealogist, and another was a
+ Navy veteran who had worked on locating the remains of American soldiers overseas.</p>
+ <hr class="baseline-grid-hr">
+ <p class="item-body-text-graf section-break-graf"><span class="section-start-text">On April 30, 2019,
+ Minus </span>combed the woods of Highland Falls, looking for the original gravesite. They tried to
+ match up the old photographs with newer ones, but this proved difficult, because most of the trees in
+ the photographs were saplings at the time. They looked for flat areas that would have been suitable for
+ burials: It was common at the time to bury people in elevated areas, to avoid rising water tables that
+ could push the caskets back up to the surface.</p>
+ <p class="item-body-text-graf">The next day, the Daughters gathered again around Corbin&#8217;s monument,
+ dressed in large hats and sashes. The ground looked as though it had never been disturbed. A casual
+ viewer would&#8217;ve never known that Corbin wasn&#8217;t under their feet.</p>
+ <figure class="article-image-full-width contains-caption "><img
+ class="article-image with-structured-caption lazy"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png"
+ alt="In 2018, the Margaret Corbin monument was rededicated with a wreath laying.
+" width="auto" data-kind="article-image" id="article-image-71504"
+ data-src="https://assets.atlasobscura.com/article_images/lg/71504/image.jpg">
+ <figcaption class="caption structured-caption noskim">In 2018, the Margaret Corbin monument was
+ rededicated with a wreath laying.
+ <span class="caption-credit">Daughters of the American Revolution</span></figcaption>
+ </figure>
+ <p class="item-body-text-graf">It was an important day for the Daughters, but especially for Minus, who
+ joined the DAR in part because of Corbin. Once, before she graduated from West Point, she told her
+ grandparents about a lunch honoring Margaret Corbin. Her grandmother told her that her heritage made her
+ eligible to join the Daughters of the American Revolution, and a few years later, when she returned from
+ a post in Germany, her grandma prepared the necessary papers.</p>
+ <p class="item-body-text-graf">Minus is hopeful that they&#8217;ll find Corbin near the river, not far
+ from the grave that the Daughters dug up in 1926. &#8220;When they started digging, they found bones.
+ So, they didn&#8217;t make, like, 10 different holes over a field. They got it on their first attempt
+ and found bones. What I&#8217;m hoping is that they just had to do a 180, and she would&#8217;ve been
+ five feet over.&#8221;</p>
+ <p class="item-body-text-graf">The man they found in Corbin&#8217;s grave has since come back to the West
+ Point cemetery, to be reinterred with the other unidentified remains found in the area. No one yet knows
+ who the man could be. Some theorize it&#8217;s Corbin&#8217;s second husband&#8212;but there&#8217;s no
+ proof that she remarried. Others believe it was a Native American. It&#8217;s possible that the unknown
+ man might be dug up a third time, should the proper clues demand his participation. Corbin&#8217;s
+ original gravesite did not turn up <a
+ href="https://www.dar.org/national-society/dar-2018-search-efforts-0">in 2018</a>, but the search
+ continues.</p>
+ <p class="item-body-text-graf">On Corbin Day in 2019, after the 21-gun salute, the Daughters hold another
+ luncheon. This time, the Margaret Corbin Task Force has something special on display: a machine that
+ looks like a souped-up lawn mower. Many Daughters file into the room and ask what it is. &#8220;Just
+ wait,&#8221; Minus answers. Then Lieutenant Colonel Mindy Kimball, an environmental science professor at
+ West Point, holds a demonstration. It&#8217;s a ground-penetrating radar machine, which shoots
+ electromagnetic waves into the ground and sends information back up to the antennae, to identify
+ underground disturbances that could reveal human remains.</p>
+ <figure class="article-image-full-width contains-caption "><img
+ class="article-image with-structured-caption lazy"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png"
+ alt="The monument to Margaret Corbin is West Point&#8217;s only monument to a woman veteran."
+ width="auto" data-kind="article-image" id="article-image-71512"
+ data-src="https://assets.atlasobscura.com/article_images/lg/71512/image.jpg">
+ <figcaption class="caption structured-caption noskim">The monument to Margaret Corbin is West
+ Point&#8217;s only monument to a woman veteran. <a class="caption-credit" rel="nofollow"
+ target="_blank"
+ href="https://commons.wikimedia.org/wiki/File:Margaret_Corbin_Memorial_Base,_West_Point,_NY.JPG">Ahodges7
+ / CC BY-SA 3.0</a></figcaption>
+ </figure>
+ <p class="item-body-text-graf">Whether or not Corbin is ever located, just sharing her story helps to
+ immortalize her. Minus is fascinated by the many identities that Corbin came to inhabit.
+ &#8220;She&#8217;s an army spouse, and then an army widow, and then she was a soldier, and then she was
+ a wounded soldier, and then she was a prisoner of war, and then she was a veteran,&#8221; she says.
+ Corbin was also the first woman to receive a military pension from the government, and is mentioned by
+ name in the Congressional Record. &#8220;I really think of her as that building block for women in the
+ military.&#8221;</p>
+ <p class="item-body-text-graf">Stella Bailey, the town historian of Highland Falls, has been researching
+ Margaret Corbin for decades. She&#8217;s pored over old maps, trying to pinpoint exactly where Corbin
+ might have been buried in 1800. She even gets emails from people who think they might be related to
+ Corbin.</p>
+ <p class="item-body-text-graf item-body-last">Sitting in her office, overlooking Main Street in Highland
+ Falls, Bailey sighs. &#8220;We know she was real. West Point&#8217;s records acknowledge her
+ existence,&#8221; she says. But she can list discrepancies in Corbin&#8217;s story. Some say her husband
+ was shot in the head; some say he was shot in the heart. Others say Corbin dressed as a man to fight in
+ the war. Sometimes she wonders whether she will ever find answers. Perhaps these conflicting stories are
+ just a part of Corbin&#8217;s mystique. &#8220;The more I research, the less I know,&#8221; Bailey says.
+ </p>
+ <p class="item-body-text-graf"><em>You can <a
+ href="https://community.atlasobscura.com/t/the-missing-grave-of-margaret-corbin-revolutionary-war-veteran-discussion-thread/33149"
+ target="_blank" rel="noopener noreferrer">join the conversation </a>about this and other stories in
+ the Atlas Obscura Community Forums.</em></p>
+ </section>
+ <div class="ItemEndRow" data-category='Articles Page Recirc' data-action='Recirc Item Clicked'
+ data-label='Footer Next Article'>
+ <div class="HorizontalCardWrapper --ArticleWrapper ">
+ <a class="HorizontalCard" data-gtm-category="Articles Page Recirc" data-gtm-template="End Cap"
+ data-gtm-content-type="Article" data-gtm-recirc-association="Related"
+ href="/articles/most-expensive-sports-memorabilia-olympics-manifesto">
+ <div class="HorizontalCard__content-wrap">
+ <div class="HorizontalCard__hat">Read next</div>
+ <h3 class="HorizontalCard__heading">
+ <span class="js-socket-title">Sold: Pierre de Coubertin’s Blueprint for the Olympics</span>
+ </h3>
+ <div
+ class="HorizontalCard__content HorizontalCard__subheading js-subtitle-content js-socket-subtitle">
+ The 14-page speech is now the world’s most expensive piece of sports memorabilia.
+ </div>
+ </div>
+ <figure class="HorizontalCard__figure js-HorizontalCard__figure js-content-horizontal-card-figure">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzLzM2MWMwYjc0LWY2Y2MtNDE4MC1hODc3LTQwODk1NjA5ODE1ODZkYzNlYzc0NWVmMjg4OGZkNl9TY3JlZW4gU2hvdCAyMDIwLTAxLTEzIGF0IDUuMzMuMTEgUE0ucG5nIl0sWyJwIiwiY29udmVydCIsIiJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtYXV0by1vcmllbnQiXSxbInAiLCJ0aHVtYiIsIjYwMHg0MDAjIl1d/Screen%20Shot%202020-01-13%20at%205.33.11%20PM.png"
+ alt="" data-width="1073" data-height="615" class="lazy HorizontalCard__img u-img-responsive"
+ nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ </figure>
+ </a>
+ </div>
+ </div>
+ <div class="itemTags ItemEndRow">
+ <span class="itemTags__tag itemTags__tag--rounded"><a class="itemTags__link"
+ href="/categories/graveyards">graveyards</a></span><span
+ class="itemTags__tag itemTags__tag--rounded"><a class="itemTags__link"
+ href="/categories/military-history">military history</a></span><span
+ class="itemTags__tag itemTags__tag--rounded"><a class="itemTags__link"
+ href="/categories/monuments">monuments</a></span><span class="itemTags__tag itemTags__tag--rounded"><a
+ class="itemTags__link" href="/categories/cemeteries">cemeteries</a></span><span
+ class="itemTags__tag itemTags__tag--rounded"><a class="itemTags__link"
+ href="/categories/history">history</a></span>
+ </div>
+ </div>
+ <div id="ArticleStickyRailTrack" class="content-siderail hidden-print">
+ <div class="js-above-rail"></div>
+ <div id="ArticleStickyRail" class="js-sticky-siderail" data-below-of=".js-above-rail"
+ data-affixed-top-margin="74" data-affixed-bottom-margin="0" data-above-of=".js-below-rail">
+ <div class="siderail-ad hidden-sm hidden-xs">
+ <div class=" fixedPositionAdBg--300 ad-background">
+ <div class='ad-wrapper hidden-print'>
+ <div class="htl-ad" data-unit="Rotational_Rectangle_Desktop"
+ data-ref="Rotational_Rectangle_Desktop_div" data-refresh="viewable" data-refresh-secs="15"
+ data-sizes="0x0:|668x0:300x250,300x400,300x600" data-prebid="Rotational_Rectangle_Desktop"
+ data-lazy></div>
+ </div>
+ </div>
+ </div>
+ <aside id="sidebar-popular" class="js-most-popular-recircs aside-recirc-module related-articles-module">
+ </aside>
+ </div>
+ </div>
+ <div id="ArticleStickyRailBrakePositioner" class="hidden-xs hidden-sm">
+ <div id="ArticleStickyRailBrake" class="js-below-rail"></div>
+ </div>
+ <div class="col-md-4 ArticleRightRail__space-holder hidden-xs hidden-sm"></div>
+ </div>
+ </div>
+ </article>
+ <div class="recirc-footer-wrap hidden-print ">
+ <div class="card-grid js-inject-gtm-data-in-child-links" data-gtm-category='Articles Page Recirc'
+ data-gtm-action='Recirc Item Clicked' data-gtm-label='Footer Related Content Card'
+ data-gtm-template='Footer Cards' data-gtm-recirc-association='Related Mixed Content'>
+ <div class="athanasius">
+ <div
+ class="full-width-container CardRecircSection CardRecircSection--8-cards-lg full-width-container CardRecircSection__article-footer">
+ <div class="container">
+ <div class='CardRecircSection__header'>
+ <div class="CardRecircSection__title">Keep Exploring</div>
+ </div>
+ <div class="card-grid CardRecircSection__card-grid js-inject-gtm-data-in-child-links"
+ data-gtm-category='Article Page Recirc' data-gtm-action='Recirc Item Clicked'
+ data-gtm-label='Related Content Card' data-gtm-template='Footer Cards'
+ data-gtm-recirc-association='Related Mixed Content'>
+ <div class="CardWrapper js-CardWrapper">
+ <div class="CardWrapper --ArticleWrapper js-CardWrapper ">
+ <a class="Card --content-card-v2 --content-card-item Card--flat" data-type="Article"
+ data-item-id="13013" data-gtm-content-type="Article" data-lat="34.198423" data-lng="-90.553051"
+ href="/articles/where-is-robert-johnson-buried">
+ <figure class="Card__figure js-Card__figure --content-card-figure js-content-card-figure">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzLzdhOWJjZjViNGRmMmJlNjBmNl9EWE1XQ0guanBnIl0sWyJwIiwiY29udmVydCIsIiJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtYXV0by1vcmllbnQiXSxbInAiLCJ0aHVtYiIsIjYwMHg0MDAjIl1d/DXMWCH.jpg"
+ alt="" data-width="1280" data-height="843"
+ class="lazy Card__img u-img-responsive --img-responsive --content-card-img" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ </figure>
+ <div class="Card__content-wrap --content-card-text">
+ <div class="Card__hat">cemeteries</div>
+ <h3 class="Card__heading --content-card-v2-title">
+ <span>The Not-So-Mysterious Missing Grave of Blues Legend Robert Johnson</span>
+ </h3>
+ <div class="Card__content js-subtitle-content">
+ The supernatural has surrounded the guitar virtuoso for decades.
+ </div>
+ <div class="Card__footer --content-card-footer">
+ <div class="Card__byline-dateline --article-byline-dateline">
+ <span class="Card__byline --article-byline">Matthew Taub</span>
+ <span class="Card__dateline --article-byline-date">October 23, 2019</span>
+ </div>
+ </div>
+ </div>
+ </a>
+ </div>
+ </div>
+ <div class="CardWrapper js-CardWrapper">
+ <div class="CardWrapper --ArticleWrapper js-CardWrapper ">
+ <a class="Card --content-card-v2 --content-card-item Card--flat" data-type="Article"
+ data-item-id="12331" data-gtm-content-type="Article" href="/articles/london-double-sided-graves">
+ <figure class="Card__figure js-Card__figure --content-card-figure js-content-card-figure">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzL2IzYTVjMjI3OWVkNmJkYWNjNV8yMS0wMS0xOSBDTENDICgzNSkgMi5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiNjAweDQwMCMiXV0/21-01-19%20CLCC%20%2835%29%202.jpg"
+ alt="" data-width="4471" data-height="2981"
+ class="lazy Card__img u-img-responsive --img-responsive --content-card-img" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ </figure>
+ <div class="Card__content-wrap --content-card-text">
+ <div class="Card__hat">cemeteries</div>
+ <h3 class="Card__heading --content-card-v2-title">
+ <span>Are Double-Sided Graves the Solution to London&#39;s Burial Crisis? </span>
+ </h3>
+ <div class="Card__content js-subtitle-content">
+ Toward a new definition of eternity.
+ </div>
+ <div class="Card__footer --content-card-footer">
+ <div class="Card__byline-dateline --article-byline-dateline">
+ <span class="Card__byline --article-byline">Katie Thornton</span>
+ <span class="Card__dateline --article-byline-date">April 19, 2019</span>
+ </div>
+ </div>
+ </div>
+ </a>
+ </div>
+ </div>
+ <div class="CardWrapper js-CardWrapper">
+ <div class="CardWrapper --ArticleWrapper js-CardWrapper ">
+ <a class="Card --content-card-v2 --content-card-item Card--flat" data-type="Article"
+ data-item-id="9443" data-gtm-content-type="Article"
+ href="/articles/cemeteries-to-visit-before-you-die-monuments">
+ <figure class="Card__figure js-Card__figure --content-card-figure js-content-card-figure">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzLzBjNmZkOTY4OWRjODE0ZTg1Ml8xMjlfc3BhaW5fcG9ibGVub3VfZm90b2tvbu-AonNodXR0ZXJzdG9ja180Mjc2MjAwMjguanBnIl0sWyJwIiwiY29udmVydCIsIiJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtYXV0by1vcmllbnQiXSxbInAiLCJ0aHVtYiIsIjYwMHg0MDAjIl1d/129_spain_poblenou_fotokon%EF%80%A2shutterstock_427620028.jpg"
+ alt="" data-width="1200" data-height="873"
+ class="lazy Card__img u-img-responsive --img-responsive --content-card-img" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ </figure>
+ <div class="Card__content-wrap --content-card-text">
+ <div class="Card__hat">cemeteries</div>
+ <h3 class="Card__heading --content-card-v2-title">
+ <span>In Search of Cemeteries Alive With Beauty, Art, and History</span>
+ </h3>
+ <div class="Card__content js-subtitle-content">
+ These resting places celebrate life.
+ </div>
+ <div class="Card__footer --content-card-footer">
+ <div class="Card__byline-dateline --article-byline-dateline">
+ <span class="Card__byline --article-byline">Anika Burgess</span>
+ <span class="Card__dateline --article-byline-date">October 31, 2018</span>
+ </div>
+ </div>
+ </div>
+ </a>
+ </div>
+ </div>
+ <div class="CardWrapper js-CardWrapper">
+ <div class="CardWrapper --ArticleWrapper js-CardWrapper ">
+ <a class="Card --content-card-v2 --content-card-item Card--flat" data-type="Article"
+ data-item-id="10971" data-gtm-content-type="Article" data-lat="33.314837" data-lng="126.273449"
+ href="/articles/dark-past-jeju-island-korea">
+ <figure class="Card__figure js-Card__figure --content-card-figure js-content-card-figure">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzLzQ0NTdhN2QxOTNkZDQ4ZTkzNl9UaGUgbWVtb3JpYWwgY2VtZXRlcnkgYXQgdGhlIEplanUgNC4zIFBlYWNlIFBhcmtfU2FtdWVsIEJlcmdzdHJvbV8yMDE4LmpwZyJdLFsicCIsImNvbnZlcnQiLCIiXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLWF1dG8tb3JpZW50Il0sWyJwIiwidGh1bWIiLCI2MDB4NDAwIyJdXQ/The%20memorial%20cemetery%20at%20the%20Jeju%204.3%20Peace%20Park_Samuel%20Bergstrom_2018.jpg"
+ alt="" data-width="2000" data-height="1333"
+ class="lazy Card__img u-img-responsive --img-responsive --content-card-img" nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ </figure>
+ <div class="Card__content-wrap --content-card-text">
+ <div class="Card__hat">cemeteries</div>
+ <h3 class="Card__heading --content-card-v2-title">
+ <span>The Bloody Past of Korea’s ‘Honeymoon Island’</span>
+ </h3>
+ <div class="Card__content js-subtitle-content">
+ Reckoning with a dark legacy at a time of great change.
+ </div>
+ <div class="Card__footer --content-card-footer">
+ <div class="Card__byline-dateline --article-byline-dateline">
+ <span class="Card__byline --article-byline">Erin Craig</span>
+ <span class="Card__dateline --article-byline-date">May 11, 2018</span>
+ </div>
+ </div>
+ </div>
+ </a>
+ </div>
+ </div>
+ <div class="CardWrapper js-CardWrapper">
+ <div class="CardWrapper --VideoWrapper js-CardWrapper ">
+ <a class="Card --content-card-v2 --content-card-item Card--flat Card--video"
+ data-gtm-content-type="Video" href="/videos/how-to-dig-a-grave">
+ <figure class="Card__figure js-Card__figure -- content-card-figure js-content-card-figure">
+ <img
+ src="https://assets.atlasobscura.com/media/W1siZiIsIjIwMTkvMDQvMjMvMjAvMTcvMDEvMGU4NzI2ODgtOTdkOC00NjAwLWFlYTctZjhlMDQ4ODZhMmNiL0dyYXZlZGlnZ2luZyAwNi5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiMTIwMDB4MTIwMDA-Il1d"
+ class="Card__img u-img-responsive --img-responsive --content-card-img lazy" alt="" />
+ </figure>
+ <div class="Card__content-wrap --content-card-text">
+ <div class="Card__hat">Video</div>
+ <h3 class="Card__heading --content-card-v2-title">
+ <span>How to Dig a Grave</span>
+ </h3>
+ <div class="Card__subheading VideoCard__subheading --content-card-footer"><i
+ class=" atlas-svg-wrap wrap-icon-aoc-video">
+ <svg version="1.1" xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <use href="#icon-aoc-video" xlink:href="#icon-aoc-video" />
+ </svg>
+ </i>
+ <span>8:51</span></div>
+ </div>
+ </a>
+ </div>
+ </div>
+ <div class="CardWrapper js-CardWrapper">
+ <div class="CardWrapper --VideoWrapper js-CardWrapper ">
+ <a class="Card --content-card-v2 --content-card-item Card--flat Card--video"
+ data-gtm-content-type="Video" href="/videos/an-ancient-cemetery-pillaged-by-grave-robbers">
+ <figure class="Card__figure js-Card__figure -- content-card-figure js-content-card-figure">
+ <img
+ src="https://assets.atlasobscura.com/media/W1siZiIsIjIwMTkvMDUvMTQvMTQvMzAvMTEvYzdhZjlkNDQtMjczNS00OTg0LWEzZjEtOGY0YmE1ZDViNjIxL0NoYXVjaGlsbGFzdGlsbDA0LmpwZyJdLFsicCIsImNvbnZlcnQiLCIiXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLWF1dG8tb3JpZW50Il0sWyJwIiwidGh1bWIiLCIxMjAwMHgxMjAwMD4iXV0"
+ class="Card__img u-img-responsive --img-responsive --content-card-img lazy" alt="" />
+ </figure>
+ <div class="Card__content-wrap --content-card-text">
+ <div class="Card__hat">Video&nbsp;&bull; PinDrop</div>
+ <h3 class="Card__heading --content-card-v2-title">
+ <span>An Ancient Cemetery, Resurrected</span>
+ </h3>
+ <div class="Card__subheading VideoCard__subheading --content-card-footer"><i
+ class=" atlas-svg-wrap wrap-icon-aoc-video">
+ <svg version="1.1" xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <use href="#icon-aoc-video" xlink:href="#icon-aoc-video" />
+ </svg>
+ </i>
+ <span>1:51</span></div>
+ </div>
+ </a>
+ </div>
+ </div>
+ <div class="CardWrapper js-CardWrapper">
+ <div class="CardWrapper --VideoWrapper js-CardWrapper ">
+ <a class="Card --content-card-v2 --content-card-item Card--flat Card--video"
+ data-gtm-content-type="Video" href="/videos/abandoned-futuristic-fort-portland-maine">
+ <figure class="Card__figure js-Card__figure -- content-card-figure js-content-card-figure">
+ <img
+ src="https://assets.atlasobscura.com/media/W1siZiIsIjIwMTkvMDMvMjcvMDAvMDIvNTAvYTgwY2FkZTMtOWU3NS00MzQ4LWJmYTItZTg4NTE2MDEyY2IxL0JhdHRlcnlTdGVlbGVTdGlsbC5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiMTIwMDB4MTIwMDA-Il1d"
+ class="Card__img u-img-responsive --img-responsive --content-card-img lazy" alt="" />
+ </figure>
+ <div class="Card__content-wrap --content-card-text">
+ <div class="Card__hat">Video</div>
+ <h3 class="Card__heading --content-card-v2-title">
+ <span>There&#39;s an Abandoned Futuristic Fort in Portland, Maine</span>
+ </h3>
+ <div class="Card__subheading VideoCard__subheading --content-card-footer"><i
+ class=" atlas-svg-wrap wrap-icon-aoc-video">
+ <svg version="1.1" xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <use href="#icon-aoc-video" xlink:href="#icon-aoc-video" />
+ </svg>
+ </i>
+ <span>3:32</span></div>
+ </div>
+ </a>
+ </div>
+ </div>
+ <div class="CardWrapper js-CardWrapper">
+ <div class="CardWrapper --VideoWrapper js-CardWrapper ">
+ <a class="Card --content-card-v2 --content-card-item Card--flat Card--video"
+ data-gtm-content-type="Video" href="/videos/a-conversation-in-america-s-most-metal-cemetery">
+ <figure class="Card__figure js-Card__figure -- content-card-figure js-content-card-figure">
+ <img
+ src="https://assets.atlasobscura.com/media/W1siZiIsIjIwMTkvMDMvMjUvMTgvMzYvMzAvZDA2NzgwNDQtMGRiZi00NTlhLThiNjMtMzY4NDE5NmU2YjU3L0VsbGEgJiBDYWl0bGluIDAzLmpwZyJdLFsicCIsImNvbnZlcnQiLCIiXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLWF1dG8tb3JpZW50Il0sWyJwIiwidGh1bWIiLCIxMjAwMHgxMjAwMD4iXV0"
+ class="Card__img u-img-responsive --img-responsive --content-card-img lazy" alt="" />
+ </figure>
+ <div class="Card__content-wrap --content-card-text">
+ <div class="Card__hat">Video</div>
+ <h3 class="Card__heading --content-card-v2-title">
+ <span>An Introduction to America&#39;s Most Metal Cemetery</span>
+ </h3>
+ <div class="Card__subheading VideoCard__subheading --content-card-footer"><i
+ class=" atlas-svg-wrap wrap-icon-aoc-video">
+ <svg version="1.1" xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <use href="#icon-aoc-video" xlink:href="#icon-aoc-video" />
+ </svg>
+ </i>
+ <span>6:04</span></div>
+ </div>
+ </a>
+ </div>
+ </div>
+ <div class="CardWrapper js-CardWrapper">
+ <div class="CardWrapper --VideoWrapper js-CardWrapper ">
+ <a class="Card --content-card-v2 --content-card-item Card--flat Card--video"
+ data-gtm-content-type="Video" href="/videos/inside-hawai-i-s-native-language-newspaper-archive">
+ <figure class="Card__figure js-Card__figure -- content-card-figure js-content-card-figure">
+ <img
+ src="https://assets.atlasobscura.com/media/W1siZiIsIjIwMTkvMDMvMjcvMTQvNTIvMDEvMjhiOWM0ZjAtNGNlOC00YTlhLTg4MTEtYWZmNjE3ZDRiZGQ2L0hhd2FpaVN0aWxsLmpwZyJdLFsicCIsImNvbnZlcnQiLCIiXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLWF1dG8tb3JpZW50Il0sWyJwIiwidGh1bWIiLCIxMjAwMHgxMjAwMD4iXV0"
+ class="Card__img u-img-responsive --img-responsive --content-card-img lazy" alt="" />
+ </figure>
+ <div class="Card__content-wrap --content-card-text">
+ <div class="Card__hat">Video&nbsp;&bull; AO Docs</div>
+ <h3 class="Card__heading --content-card-v2-title">
+ <span>Hawaiʻi’s Native-Language Newspaper Archive</span>
+ </h3>
+ <div class="Card__subheading VideoCard__subheading --content-card-footer"><i
+ class=" atlas-svg-wrap wrap-icon-aoc-video">
+ <svg version="1.1" xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <use href="#icon-aoc-video" xlink:href="#icon-aoc-video" />
+ </svg>
+ </i>
+ <span>3:35</span></div>
+ <div class="Card__footer VideoCard__footer --content-card-footer">
+ <span class="sponsored-article-tag"><span class='js-tracked-sponsored-recirc'
+ data-recirc-pid='video-54'>Sponsored by Olukai</span>
+ </span>
+ </div>
+ </div>
+ </a>
+ </div>
+ </div>
+ <div class="CardWrapper js-CardWrapper">
+ <div class="CardWrapper --VideoWrapper js-CardWrapper ">
+ <a class="Card --content-card-v2 --content-card-item Card--flat Card--video"
+ data-gtm-content-type="Video" href="/videos/gaze-upon-a-million-monarch-butterflies-in-mexico">
+ <figure class="Card__figure js-Card__figure -- content-card-figure js-content-card-figure">
+ <img
+ src="https://assets.atlasobscura.com/media/W1siZiIsIjIwMTkvMDQvMDkvMjAvMzcvMzMvNDlmMGEzMjAtY2ViYS00MGYyLTg5NjktMzYzZjkyZDM1YmFlL21vbmFyY2gwMi5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiMTIwMDB4MTIwMDA-Il1d"
+ class="Card__img u-img-responsive --img-responsive --content-card-img lazy" alt="" />
+ </figure>
+ <div class="Card__content-wrap --content-card-text">
+ <div class="Card__hat">Video&nbsp;&bull; AO Docs</div>
+ <h3 class="Card__heading --content-card-v2-title">
+ <span>&#39;Discovering&#39; Mexico&#39;s Monarch Butterfly Migration</span>
+ </h3>
+ <div class="Card__subheading VideoCard__subheading --content-card-footer"><i
+ class=" atlas-svg-wrap wrap-icon-aoc-video">
+ <svg version="1.1" xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <use href="#icon-aoc-video" xlink:href="#icon-aoc-video" />
+ </svg>
+ </i>
+ <span>6:46</span></div>
+ </div>
+ </a>
+ </div>
+ </div>
+ <div class="CardWrapper js-CardWrapper">
+ <div class="CardWrapper --VideoWrapper js-CardWrapper ">
+ <a class="Card --content-card-v2 --content-card-item Card--flat Card--video"
+ data-gtm-content-type="Video" href="/videos/what-were-george-washingtons-dentures-made-of">
+ <figure class="Card__figure js-Card__figure -- content-card-figure js-content-card-figure">
+ <img
+ src="https://assets.atlasobscura.com/media/W1siZiIsIjIwMTkvMDkvMDMvMjAvMzIvMTQvNTUyZjg2YjAtMTg0NC00ZWI2LWIzMWEtOTllNzJkYWQxYTFiL0RlbnR1cmVzIDAzLmpwZyJdLFsicCIsImNvbnZlcnQiLCIiXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLWF1dG8tb3JpZW50Il0sWyJwIiwidGh1bWIiLCIxMjAwMHgxMjAwMD4iXV0"
+ class="Card__img u-img-responsive --img-responsive --content-card-img lazy" alt="" />
+ </figure>
+ <div class="Card__content-wrap --content-card-text">
+ <div class="Card__hat">Video&nbsp;&bull; Object of Intrigue</div>
+ <h3 class="Card__heading --content-card-v2-title">
+ <span>The Real Story Behind George Washington&#39;s Dentures</span>
+ </h3>
+ <div class="Card__subheading VideoCard__subheading --content-card-footer"><i
+ class=" atlas-svg-wrap wrap-icon-aoc-video">
+ <svg version="1.1" xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <use href="#icon-aoc-video" xlink:href="#icon-aoc-video" />
+ </svg>
+ </i>
+ <span>3:30</span></div>
+ </div>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="container ad-container">
+ <hr class="above-ad-border hidden-sm hidden-xs">
+ <div class='ad-wrapper hidden-print ad-inline-banner ad-banner-lg'>
+ <div class="htl-ad" data-unit="Site_Bottom_Full_Width" data-ref="Site_Bottom_Full_Width_div"
+ data-sizes="0x0:|668x0:728x90,970x250,1440x585" data-prebid="Site_Bottom_Full_Width" data-eager></div>
+ </div>
+ </div>
+ <div class="footer-reflow">
+ </div>
+ </div>
+ <div id="articleBody__interrupt-card" class="hidden hidden-print">
+ <div class="js-inject-gtm-data-in-child-links" data-gtm-category='Articles Page Recirc'
+ data-gtm-action='Recirc Item Clicked' data-gtm-label='Interrupt Related Article' data-gtm-template='Interruptor'
+ data-gtm-content-type='Article' data-gtm-recirc-association='Related Article'>
+ <div class="HorizontalCardWrapper --ArticleWrapper ">
+ <a class="HorizontalCard"
+ href="/articles/grim-history-hidden-under-baltimore-parking-lot-cemetery-african-american">
+ <div class="HorizontalCard__content-wrap">
+ <div class="HorizontalCard__hat">Related</div>
+ <h3 class="HorizontalCard__heading">
+ <span class="js-socket-title">The Grim History Hidden Under a Baltimore Parking Lot</span>
+ </h3>
+ <div class="HorizontalCard__content HorizontalCard__subheading js-subtitle-content js-socket-subtitle">
+ After an African-American cemetery was bulldozed, families wondered what happened to the graves.
+ </div>
+ <div class="HorizontalCard__footer">Read more</div>
+ </div>
+ <figure class="HorizontalCard__figure js-HorizontalCard__figure js-content-horizontal-card-figure">
+ <img
+ data-src="https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzLzk5N2EwYTgxZjlhYzA2MzRiYl9GcmFuayBBIEdyYXkgTmV3IE1hcCBvZiBCYWx0aW1vcmUgMTg3NiAoMSkuanBnIl0sWyJwIiwiY29udmVydCIsIi1hdXRvLW9yaWVudCAiXSxbInAiLCJ0aHVtYiIsIjg2NHg1NzYrMTc1KzU2Il0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9yaWVudCJdLFsicCIsInRodW1iIiwiNjAweDQwMCMiXV0/Frank%20A%20Gray%20New%20Map%20of%20Baltimore%201876%20%281%29.jpg"
+ alt="" data-width="1039" data-height="1074" class="lazy HorizontalCard__img u-img-responsive"
+ nopin="nopin"
+ src="https://assets.atlasobscura.com/assets/blank-11b9c95a68e295dddd0ea924647536578ce285b2c8469a223c01df1ff3166af1.png" />
+ </figure>
+ </a>
+ </div>
+ </div>
+ </div>
+ <div class="modal lightbox-modal fadeX" id="lightbox" tabindex="-1" role="dialog" aria-labelledby="lightbox">
+ <div id="lightbox-content">
+ <span class="icon-menu-close modal-close" data-dismiss="modal"></span>
+ <div id="lightbox-figure-box">
+ <figure id="lightbox-figure">
+ <img src="" id="lightbox-image" alt="" />
+ <figcaption id="lightbox-caption" class="caption">
+ </figcaption>
+ </figure>
+ </div>
+ <div id="lightbox-controls" class="hidden-sm hidden-xs">
+ <a class="js-lightbox-prev lightbox-gallery-control prev-arrow" href="javascript:void(0)"
+ aria-label="Previous image">
+ <i class="icon-expand_left"></i>
+ </a>
+ <a class="js-lightbox-next lightbox-gallery-control next-arrow" href="javascript:void(0)"
+ aria-label="Next image">
+ <i class="icon-expand_right"></i>
+ </a>
+ </div>
+ <div id="mobile-lightbox-control-prev" class="hidden-md hidden-lg">
+ <a class="js-lightbox-prev lightbox-gallery-control prev-arrow" href="javascript:void(0)">
+ <i class="icon-expand_left"></i>
+ </a>
+ </div>
+ <div id="mobile-lightbox-control-next" class="hidden-md hidden-lg">
+ <a class="js-lightbox-next lightbox-gallery-control next-arrow" href="javascript:void(0)">
+ <i class="icon-expand_right"></i>
+ </a>
+ </div>
+ </div>
+ <div class="lightbox-thumbnails hidden-sm hidden-xs hidden-md">
+ <div class="lightbox-thumbnails-container">
+ </div>
+ </div>
+ </div>
+ <div class="hidden-md hidden-lg m-social-adhesive-wrap">
+ <div id="js-item-social-adhesive" class="item-social-adhesive">
+ <div id="js-item-adhesive-btns" class="item-social-row ">
+ <a href=""
+ class="btn btn-adhesive btn-social-row btn-facebook btn-icon js-share-button-facebook js-social-action-tracked"
+ data-position="Bottom-Sticky-mobile" data-service="Facebook" data-action="Share"
+ aria-label="Share on Facebook">
+ <i class="icon-facebook"></i>
+ </a>
+ <a href="https://twitter.com/share?text=The%20Missing%20Grave%20of%20Margaret%20Corbin%2C%20Revolutionary%20War%20Veteran%3A%20@atlasobscura&amp;count=none&amp;url=https://www.atlasobscura.com/articles/margaret-corbin-grave-west-point"
+ class="btn btn-adhesive btn-social-row btn-twitter btn-icon js-social-action-tracked js-social-ask-for-follow"
+ target="_blank" data-position="Bottom-Sticky-mobile" data-service="Twitter" data-action="Share"
+ aria-label="Share on Twitter">
+ <i class="icon-twitter"></i>
+ </a>
+ <a href="https://www.reddit.com/submit?url=https%3A%2F%2Fwww.atlasobscura.com%2Farticles%2Fmargaret-corbin-grave-west-point"
+ target="_blank"
+ class="btn btn-adhesive btn-social-row btn-reddit btn-icon icon-reddit js-social-action-tracked"
+ data-position="Bottom-Sticky-mobile" data-service="reddit" data-action="Share"
+ aria-label="Share on Reddit"></a>
+ <a href="mailto:?subject=The%20Missing%20Grave%20of%20Margaret%20Corbin%2C%20Revolutionary%20War%20Veteran&body=Discovered%20on%20Atlas%20Obscura%3A%20https://www.atlasobscura.com/articles/margaret-corbin-grave-west-point"
+ class="btn btn-adhesive btn-social-row btn-email btn-icon js-social-action-tracked"
+ data-position="Bottom-Sticky-mobile" data-service="Email" data-action="Send" aria-label="Email this">
+ <i class="icon-envelope"></i>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ <footer class="new-footer page-footer">
+ <div class="footer-z-cover"></div>
+ <div class="container js-page-left-reference">
+ <div class="row">
+ <div class="col-md-6 col-lg-4">
+ <div class="footer-social">
+ <section class="subscribe-sm footer-subscribe">
+ <h4 class="heading-sm footer-links-heading js-footer-links-heading">Get Our Email Newsletter</h4>
+ <form class="footer-newsletter-form js-email-ask-form" action="/email_lists/signup" accept-charset="UTF-8"
+ data-remote="true" method="post"><input name="utf8" type="hidden" value="&#x2713;" />
+ <input type="hidden" name="source" value="footer-email-ask" />
+ <input type="hidden" name="subscribe_general" value="true" />
+ <input type="email" name="email" placeholder="enter your email" class="js-footer-email-ask-removable"
+ aria-label="Email" />
+ <input type="submit" name="commit" value="Subscribe"
+ class="btn btn-secondary btn-orange js-footer-email-ask-removable" />
+ <div id="js-footer-email-ask-thanks" class="subscribe-thanks detail">Thanks for subscribing!
+ <a target="_parent" data-category="View All Newsletters Link" data-action="Click" data-label="Footer"
+ class="js-view-newsletters" href="/newsletters">View all newsletters &raquo;</a>
+ </div>
+ </form>
+ </section>
+ </div>
+ </div>
+ <div class="col-md-3 social-icons">
+ <div class="row nested-row">
+ <h4 class="heading-sm footer-links-heading js-footer-links-heading social-icons-heading">Follow Us</h4>
+ <ul id="footer-social-list">
+ <li><a target="_blank" href="https://www.facebook.com/atlasobscura/"
+ class="icon-facebook btn-icon footer-social-btn js-social-action-tracked" data-position="Footer"
+ data-service="Facebook" data-action="Like" aria-label="Like us on Facebook"></a></li>
+ <li><a target="_blank" href="https://www.youtube.com/user/atlasobscura"
+ class="icon-youtube btn-icon footer-social-btn js-social-action-tracked" data-position="Footer"
+ data-service="Youtube" data-action="Follow" aria-label="Subscribe to our Youtube channel"></a></li>
+ <li><a target="_blank" href="https://twitter.com/atlasobscura"
+ class="icon-twitter btn-icon footer-social-btn js-social-action-tracked" data-position="Footer"
+ data-service="Twitter" data-action="Follow" aria-label="Follow us on Twitter"></a></li>
+ <li><a target="_blank" href="https://www.instagram.com/atlasobscura/"
+ class="icon-instagram btn-icon footer-social-btn js-social-action-tracked" data-position="Footer"
+ data-service="Instagram" data-action="Follow" aria-label="Follow us on Instagram"></a></li>
+ <li><a target="_blank" href="/feeds/latest"
+ class="icon-rss btn-icon footer-social-btn js-social-action-tracked" data-position="Footer"
+ data-service="RSS" data-action="Follow" aria-label="Subscribe to our RSS feed"></a></li>
+ </ul>
+ </div>
+ </div>
+ <div class="hidden-sm hidden-xs hidden-md promotional-content">
+ <div class="footer-shop-callout-wrap">
+ <a class="shop-callout non-decorated-link" target="" href="/unique-gifts/atlas-obscura-book">
+ <figure class="shop-callout-image-wrap">
+ <img class="img-responsive"
+ src="https://assets.atlasobscura.com/media/W1siZnUiLCJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vYXRsYXMtZGV2L21pc2MvYXBwLWltYWdlcy9ib29rLzJuZC1lZGl0aW9uLW9uLXNpdGUtcHJvbW8ucG5nIl0sWyJwIiwidGh1bWIiLCIyNTB4PiJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtc3RyaXAiXV0/2nd-edition-on-site-promo.png"
+ alt="2nd edition on site promo" />
+ </figure>
+ <div class="shop-callout-text">
+ <div class="detail-md">Get the Atlas Obscura book</div>
+ <div class="cta-xs shop-action-text">Shop Now »</div>
+ </div>
+ </a>
+ </div>
+ </div>
+ </div>
+ <div class="row footer-links">
+ <div class="col-md-7 five-wide-3">
+ <section class="atlas-section col-md-4">
+ <h3 class="heading-sm footer-links-heading mobile-accordion-title js-footer-links-heading">
+ Places <i class="icon-expand_more footer-expand-arrow visible-sm visible-xs"></i>
+ </h3>
+ <div class="is-slider-hidden-m">
+ <ul class="links-column">
+ <li><a target="" href="/places">Recently Added</a></li>
+ <li><a target="" href="/places?sort=likes_count">Most Popular</a></li>
+ <li><a target="" href="/random">Random</a></li>
+ <li><a target="" href="/search/search_nearby">Nearby</a></li>
+ <li><a class="js-user-required" data-cause-key="p_add" target="" href="/places/new">Add a Place</a></li>
+ </ul>
+ </div>
+ </section>
+ <section class="col-md-4">
+ <h3 class="heading-sm footer-links-heading mobile-accordion-title js-footer-links-heading">
+ Foods <i class="icon-expand_more footer-expand-arrow visible-sm visible-xs"></i>
+ </h3>
+ <div class="is-slider-hidden-m">
+ <ul class="links-column">
+ <li><a target="" href="/gastro">Latest</a></li>
+ <li><a target="" href="/unique-food-drink">Food &amp; Drink</a></li>
+ <li><a target="" href="/gastro/stories">Stories</a></li>
+ <li><a target="" href="/gastro/places">Places</a></li>
+ <li style="display: none;"><a target="" href="/foods/new">Add a Food</a></li>
+ </ul>
+ </div>
+ </section>
+ <section class="col-md-4">
+ <h3 class="heading-sm footer-links-heading mobile-accordion-title js-footer-links-heading">
+ Experiences & Trips <i class="icon-expand_more footer-expand-arrow visible-sm visible-xs"></i>
+ </h3>
+ <div class="is-slider-hidden-m">
+ <ul class="links-column">
+ <li><a target="" href="/experiences">Upcoming Experiences</a></li>
+ <li><a target="" href="/unusual-trips/all">Upcoming Trips</a></li>
+ <li><a href="https://blog.atlasobscura.com">Trips Blog</a></li>
+ </ul>
+ </div>
+ </section>
+ </div>
+ <div class="col-md-5 five-wide-2">
+ <section class="col-md-6">
+ <h3 class="heading-sm footer-links-heading mobile-accordion-title js-footer-links-heading">
+ Community <i class="icon-expand_more footer-expand-arrow visible-sm visible-xs"></i>
+ </h3>
+ <div class="is-slider-hidden-m">
+ <ul class="links-column">
+ <li><a target="" href="https://community.atlasobscura.com">Travel Forum</a></li>
+ <li><a target="" href="https://community.atlasobscura.com/latest">Latest Posts</a></li>
+ <li><a target="" href="https://community.atlasobscura.com/top">Top Posts</a></li>
+ </ul>
+ </div>
+ </section>
+ <section class="col-md-6">
+ <h3 class="heading-sm footer-links-heading mobile-accordion-title js-footer-links-heading">
+ Company <i class="icon-expand_more footer-expand-arrow visible-sm visible-xs"></i>
+ </h3>
+ <div class="is-slider-hidden-m">
+ <ul class="links-column">
+ <li><a target="" href="/about">About</a></li>
+ <li class="hidden-md hidden-lg"><a
+ onclick="window.open(this.href,'Contact Atlas Obscura','width=600,height=700,toolbar=no,menubar=no,scrollbars'); return false"
+ href="https://www.atlasobscura.com/contact_form" target="_blank">Contact Us</a></li>
+ <li><a target="" href="/faq">FAQ</a></li>
+ <li><a target="" href="/jobs">Work With Us</a></li>
+ <li><a target="" href="mailto:ads@atlasobscura.com">Advertising</a></li>
+ <li><a target="" href="/unique-gifts">Unique Gifts</a></li>
+ <li><a target="" href="/privacy">Privacy Policy</a></li>
+ <li><a target="" href="/terms">Terms of Use</a></li>
+ </ul>
+ </div>
+ </section>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-4 hidden-md hidden-lg hidden-xl promotional-content">
+ <div class="footer-shop-callout-wrap">
+ <a class="shop-callout non-decorated-link" target="" href="/unique-gifts/atlas-obscura-book">
+ <figure class="shop-callout-image-wrap">
+ <img class="img-responsive"
+ src="https://assets.atlasobscura.com/media/W1siZnUiLCJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vYXRsYXMtZGV2L21pc2MvYXBwLWltYWdlcy9ib29rLzJuZC1lZGl0aW9uLW9uLXNpdGUtcHJvbW8ucG5nIl0sWyJwIiwidGh1bWIiLCIyNTB4PiJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtc3RyaXAiXV0/2nd-edition-on-site-promo.png"
+ alt="2nd edition on site promo" />
+ </figure>
+ <div class="shop-callout-text">
+ <div class="detail-md">Get the Atlas Obscura book</div>
+ <div class="cta-xs shop-action-text">Shop Now »</div>
+ </div>
+ </a>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="copyright col-md-8 col-lg-6">
+ &copy; 2020 Atlas Obscura. All rights reserved.
+ </div>
+ </div>
+ </div>
+ </footer>
+ <a id="site-feedback-wrap" target="_blank"
+ onclick="window.open(this.href,'Contact Atlas Obscura','width=600,height=700,toolbar=no,menubar=no,scrollbars'); return false"
+ href="https://www.atlasobscura.com/contact_form">
+ <button id="btn-site-feedback" class="btn btn-stretch">Questions or Feedback? <span class="static-underline">Contact
+ Us</span></button>
+ </a>
+
+ <div class="js-paginate-content-modal-controls paginate-content-modal-controls close-button-container">
+ <button type="button" class="modal-close-button js-modal-close-button icon icon-menu-close"
+ aria-label="Close"></button>
+ </div>
+ <div class="js-paginate-content-modal paginate-content-modal modal">
+ <div class="modal-body">
+ </div>
+ </div>
+ <div class="js-confirmation-modal confirmation-modal modal">
+ <div class="modal-dialog">
+ <div class="modal-bg">
+ <div class="modal-header hidden"></div>
+ <div class="modal-body">
+ <div class="modal-dismiss">
+ <button type="button" data-dismiss="modal"
+ class="modal-close-button js-modal-close-button icon icon-menu-close" aria-label="Close"></button>
+ </div>
+ <div class="modal-content">
+ <div class="confirmation-modal-heading title-md baseline-standard baseline-mobile"></div>
+ <p class="confirmation-modal-text"></p>
+ </div>
+ <div class="modal-buttons">
+ <div class="submit-button btn btn-modal-cancel"></div>
+ <div data-dismiss="modal" class="back-button btn btn-modal-submit"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="social-follow-ask-modal" class="confirmation-modal modal">
+ <div class="modal-dialog">
+ <div class="modal-bg">
+ <div class="modal-body">
+ <div class="modal-dismiss">
+ <button type="button" data-dismiss="modal"
+ class="modal-close-button js-modal-close-button icon icon-menu-close" aria-label="Close"></button>
+ </div>
+ <div class="modal-content">
+ <div class="confirmation-modal-heading title-md baseline-standard baseline-mobile">Thanks for sharing!</div>
+ <p data-service="Twitter" style="display: none;" class="baseline-standard baseline-mobile">Follow us on
+ Twitter to get the latest on the world's hidden wonders.</p>
+ <p data-service="Facebook" style="display: none;" class="baseline-standard baseline-mobile">Like us on
+ Facebook to get the latest on the world's hidden wonders.</p>
+ <a href="https://twitter.com/atlasobscura"
+ class="js-social-action-tracked btn btn-twitter fullscreen-modal-social-btn" target="_blank"
+ data-service="Twitter" data-action="Follow" data-position="Modal After Social Action"
+ style="display: none;"><i class="icon icon-twitter"></i>Follow us on Twitter</a>
+ <a href="https://www.facebook.com/atlasobscura/"
+ class="js-social-action-tracked btn btn-facebook fullscreen-modal-social-btn" target="_blank"
+ data-service="Facebook" data-action="Like" data-position="Modal After Social Action"
+ style="display: none;"><i class="icon icon-facebook"></i>Like us on Facebook</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="book-contest-email-modal" class="modal modal-sm-fullscreen modal-md-fullscreen js-subscription-ask-modal"
+ role="dialog">
+ <div class="modal-body-fullscreen">
+ <div class="close-button-container">
+ <i class="modal-close-button icon icon-menu-close" data-dismiss="modal" aria-label="Close"></i>
+ </div>
+ <div id="contest-wrap" class="standalone-signup-wrap align-items-center fullpage-bg-modal plain-beige-version">
+ <div id="contest-bg" class="topographic transparent-topo"></div>
+ <div class="container pre-final-container">
+ <div class="row">
+ <center class="contest-contents col-lg-10 col-lg-push-1">
+ <div class="contest-form-wrap">
+ <form class="js-email-roadblock-form js-email-ask-form contest-signup-ui" id="contest-form"
+ action="/email_lists/signup" accept-charset="UTF-8" data-remote="true" method="post"><input
+ name="utf8" type="hidden" value="&#x2713;" />
+ <input type="hidden" name="subscribe_general" value="true" />
+ <input id="contest-source" type="hidden" name="source" value="Email Ask (Red, Free Book)" />
+ <input type="hidden" name="merge_vars[MMERGE15]" id="merge_vars_MMERGE15" value="1" />
+ <input type="hidden" name="merge_vars[MMERGE21]" id="merge_vars_MMERGE21"
+ value="Book Contest - January 2020" />
+ <h4 id="book-contest-title" class="title-lg baseline-standard baseline-mobile animate-swing-up">Want a
+ Free Book?</h4>
+ <div class="animate-text-reveal">
+ <div class="subtitle-lg baseline-standard baseline-mobile">Sign up for our newsletter and enter to
+ win the second edition of our book, <em>Atlas Obscura: An Explorer’s Guide to the World’s Hidden
+ Wonders</em>.</div>
+ <fieldset>
+ <div class="cta-lg validate-message"></div>
+ <div class="submit-inline baseline-standard baseline-mobile">
+ <input type="email" name="email" class="detail-md" required="required"
+ placeholder="Enter your email address" aria-label="email" />
+ <button type="submit" class="g-recaptcha btn btn-default js-contest-submit-btn"
+ data-badge="bottomright" data-callback="submitCaptchadContestForm"
+ data-sitekey="6LeCJy0UAAAAAO5grI_UrlSR1oz9AceexUbkcHgC" eager-recaptcha="1">Subscribe</button>
+ </div>
+ </fieldset>
+ <a href="javascript:void(0)" class="contest-dismiss-link cta-lg" data-dismiss="modal">No Thanks</a>
+ <a href="/" class="contest-take-me-home-link cta-lg" data-dismiss="modal">Visit AtlasObscura.com</a>
+ </div>
+ </form>
+ </div>
+ <div id="contest-image-wrap" class="hidden-xs hidden-sm">
+ <picture>
+ <source
+ data-srcset="https://assets.atlasobscura.com/media/W1siZnUiLCJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vYXRsYXMtZGV2L21pc2MvaWNvbnMvZmF2aWNvbi0xNngxNi5wbmciXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLXN0cmlwIl1d/favicon-16x16.png"
+ media="(max-width: 667px)"
+ srcset="https://assets.atlasobscura.com/media/W1siZnUiLCJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vYXRsYXMtZGV2L21pc2MvaWNvbnMvZmF2aWNvbi0xNngxNi5wbmciXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLXN0cmlwIl1d/favicon-16x16.png" />
+ <img class="img-responsive"
+ data-src="https://assets.atlasobscura.com/media/W1siZnUiLCJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vYXRsYXMtZGV2L21pc2MvaW50ZXJuYWwtb25lLW9mZnMvc2hvcC9zZWNvbmQtZWR0aW9uLW9wdGltLnBuZyJdLFsicCIsImNvbnZlcnQiLCItcXVhbGl0eSA4MSAtc3RyaXAiXV0/second-edtion-optim.png"
+ src="https://assets.atlasobscura.com/media/W1siZnUiLCJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vYXRsYXMtZGV2L21pc2MvaW50ZXJuYWwtb25lLW9mZnMvc2hvcC9zZWNvbmQtZWRpdGlvbi1vcHRpbS5wbmciXSxbInAiLCJjb252ZXJ0IiwiLXF1YWxpdHkgODEgLXN0cmlwIl1d/second-edition-optim.png"
+ alt="Atlas Obscura: An Explorer's Guide to the World's Hidden Wonders" />
+ </picture>
+ </div>
+ </center>
+ </div>
+ </div>
+ <div class="final-state-asks">
+ <center>
+ <h1 class="title-lg baseline-standard baseline-mobile" style="color: #fff;">Stay in Touch!</h1>
+ <div class="subtitle-lg baseline-standard baseline-mobile">Follow us on social media to add even more wonder
+ to your day.</div>
+ <a href="https://twitter.com/atlasobscura"
+ class="js-social-action-tracked js-hidden-if-contest btn btn-twitter fullscreen-modal-social-btn"
+ target="_blank" data-service="Twitter" data-action="Follow" data-position="Contest Ask"><i
+ class="icon icon-twitter"></i>Follow us on Twitter</a>
+ <a href="https://www.facebook.com/atlasobscura/"
+ class="js-social-action-tracked btn btn-facebook fullscreen-modal-social-btn" target="_blank"
+ data-service="Facebook" data-action="Like" data-position="Contest Ask"><i
+ class="icon icon-facebook"></i>Like us on Facebook</a>
+ <a href="https://www.instagram.com/atlasobscura/"
+ class="js-social-action-tracked btn btn-instagram fullscreen-modal-social-btn" target="_blank"
+ data-service="Instagram" data-action="Follow" data-position="Contest Ask" style="margin-bottom: 24px;"><i
+ class="icon icon-instagram"></i>Follow Us on Instagram</a>
+ <a href="javascript:void(0)" style="" class="contest-dismiss-link cta-lg" data-dismiss="modal">No Thanks</a>
+ <a href="/" class="contest-take-me-home-link cta-lg" data-dismiss="modal">Visit AtlasObscura.com</a>
+ </center>
+ </div>
+ <div class="container contest-disclaimer contest-signup-ui">
+ <div class="row">
+ <div class="col-lg-6 col-lg-push-1">
+ <div class="contest-footnote">No purchase necessary. Winner will be selected at random on 02/01/2020.
+ Offer available only in the U.S. (including Puerto Rico). Offer subject to change without notice. See <a
+ href="/newsletters/contest-rules">contest rules</a> for full details.</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="email-roadblock-topographic-modal" class="modal js-subscription-ask-modal " role="dialog">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-wrap modal-wrap-topographic topographic modal-wrap-responsive">
+ <i class="icon-modal-close modal-close" data-dismiss="modal"></i>
+ <div class="row modal-bg roadblock-modal-content">
+ <div class="modal-body" id="newsletter-email-collection-modal">
+ <div id="js-email-roadblock-topographic-modal-thanks" class="form-complete-notice"></div>
+ <form class="js-email-roadblock-form js-email-ask-form" action="/email_lists/signup"
+ accept-charset="UTF-8" data-remote="true" method="post"><input name="utf8" type="hidden"
+ value="&#x2713;" />
+ <input type="hidden" name="subscribe_general" value="true" />
+ <input type="hidden" name="source" value="email-roadblock-topographic-modal" />
+ <h4 class="title-lg modal-title-roadblock">Add Some Wonder to Your Inbox</h4>
+ <div class="subtitle-md">Every weekday we compile our most wondrous stories and deliver them straight to
+ you.</div>
+ <div id="js-email-roadblock-topographic-modal-error"></div>
+ <fieldset class="modal-fieldset">
+ <div class="cta-lg validate-message"></div>
+ <div class="email-submit-group-m">
+ <input type="email" name="email" class="col-md-12" required="required" placeholder="your email"
+ aria-label="Email" />
+ <button name="button" type="submit" class="btn btn-stretch btn-submit">
+ <i class="icon-envelope"></i><span class="hidden-sm hidden-xs">Subscribe</span>
+ </button> </div>
+ </fieldset>
+ <footer class="roadblock-footer">
+ <a href="" class="roadblock-dismiss-link cta-lg" data-dismiss="modal">No Thanks</a>
+ </footer>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="facebook-topographic-modal" class="modal js-subscription-ask-modal " role="dialog">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-wrap modal-wrap-topographic topographic modal-wrap-responsive modal-wrap-fb">
+ <i class="icon-modal-close modal-close" data-dismiss="modal"></i>
+ <div class="row modal-bg roadblock-modal-content">
+ <div class="modal-body">
+ <div id="js-facebook-topographic-modal-thanks" class="form-complete-notice"></div>
+ <h4 class="title-lg modal-title-roadblock" style="font-size: 38px;">We'd Like You to Like Us</h4>
+ <div class="subtitle-md">Like Atlas Obscura and get our latest and greatest stories in your Facebook feed.
+ </div>
+ <fieldset class="modal-fieldset">
+ <div id="fb-modal-like-wrap"></div>
+ </fieldset>
+ <footer class="roadblock-footer">
+ <a href="" class="roadblock-dismiss-link cta-lg" data-dismiss="modal">No Thanks</a>
+ </footer>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="cookie-consent-modal" class="modal" data-backdrop="static" data-keyboard="false" role="dialog">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-wrap modal-wrap-topographic topographic modal-wrap-responsive">
+ <div class="row modal-bg roadblock-modal-content">
+ <div class="modal-body">
+ <h6 class="title-md baseline-near baseline-mobile">We value your privacy</h6>
+ <p style="margin-bottom: 21px;">Atlas Obscura and our trusted partners use technology such as cookies on
+ our website to personalise ads, support social media features, and analyse our traffic. Please click
+ below to consent to the use of this technology while browsing our site. To learn more or withdraw
+ consent, please visit our <a target="_blank" href="/privacy">privacy policy</a>.</p>
+ <div>
+ <a href="javascript:void(0)" class="btn btn-submit js-cookie-consent-accept" data-dismiss="modal">I
+ Accept</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="fixed-notice-m" class="js-notice">
+ <div class="notice">
+ <i class="icon-info"></i>
+ <span id="fixed-notice-m-text" class="flash-message"></span>
+ <i class="icon-menu-close js-dismiss-notice"></i>
+ </div>
+ </div>
+ <noscript>
+ <img src="https://sb.scorecardresearch.com/p?c1=2&c2=17564338&cv=2.0&cj=1" />
+ </noscript>
+
+
+ <noscript>
+ <div style="display:none;"><img src="//pixel.quantserve.com/pixel/p-wCQ2x-2BzmYPY.gif" border="0" height="1"
+ width="1" alt="Quantcast" /></div>
+ </noscript>
+
+ <div id="parsely-root" style="display: none">
+ <span id="parsely-cfg" data-parsely-site="atlasobscura.com"></span>
+ </div>
+
+ <svg aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1"
+ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <symbol id="icon-aoc-cancel" viewBox="0 0 24 24">
+ <title>aoc-cancel</title>
+ <path
+ d="M12 2c-5.53 0-10 4.47-10 10s4.47 10 10 10c5.53 0 10-4.47 10-10s-4.47-10-10-10v0zM17 15.59l-1.41 1.41-3.59-3.59-3.59 3.59-1.41-1.41 3.59-3.59-3.59-3.59 1.41-1.41 3.59 3.59 3.59-3.59 1.41 1.41-3.59 3.59 3.59 3.59z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-video" viewBox="0 0 24 24">
+ <title>aoc-video</title>
+ <path
+ d="M10 16.5l6-4.5-6-4.5v9zM12 2c-5.52 0-10 4.48-10 10s4.48 10 10 10c5.52 0 10-4.48 10-10s-4.48-10-10-10v0zM12 20c-4.41 0-8-3.59-8-8s3.59-8 8-8c4.41 0 8 3.59 8 8s-3.59 8-8 8v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-building" viewBox="0 0 24 24">
+ <title>aoc-building</title>
+ <path
+ d="M16 8.050c-0.096 0.098-0.187 0.202-0.272 0.312-1.061 0.455-1.911 1.305-2.366 2.366-0.129 0.099-0.25 0.207-0.362 0.322v-0.050h-2v2h1.036c-0.024 0.164-0.036 0.331-0.036 0.5 0 1.883 1.487 3.419 3.351 3.497 0.2 0.177 0.418 0.334 0.649 0.468v4.535h-5v-6h-4v6h-5v-20h14v6.050zM5 11v2h2v-2h-2zM5 7v2h2v-2h-2zM8 7v2h2v-2h-2zM8 11v2h2v-2h-2zM11 7v2h2v-2h-2zM17 16.829c-0.485-0.171-0.913-0.464-1.247-0.842-0.083 0.008-0.167 0.013-0.253 0.013-1.381 0-2.5-1.119-2.5-2.5 0-0.898 0.474-1.686 1.185-2.127 0.349-1.027 1.161-1.839 2.188-2.188 0.441-0.711 1.228-1.185 2.127-1.185 1.381 0 2.5 1.119 2.5 2.5 0 0.185-0.020 0.365-0.058 0.539 1.17 0.209 2.058 1.231 2.058 2.461 0 1.381-1.119 2.5-2.5 2.5-0.085 0-0.17-0.004-0.253-0.013-0.334 0.378-0.762 0.67-1.247 0.842v5.171h-2v-5.171z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-clock" viewBox="0 0 24 24">
+ <title>aoc-clock</title>
+ <path
+ d="M11.99 2c5.53 0 10.010 4.48 10.010 10s-4.48 10-10.010 10c-5.52 0-9.99-4.48-9.99-10s4.47-10 9.99-10zM13 11.423v-5.423c0-0.552-0.448-1-1-1s-1 0.448-1 1v6.577l3.964 2.289c0.478 0.276 1.090 0.112 1.366-0.366s0.112-1.090-0.366-1.366l-2.964-1.711z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-clipboard" viewBox="0 0 19 24">
+ <title>aoc-clipboard</title>
+ <path
+ d="M12.984 2.4c-0.504-1.392-1.824-2.4-3.384-2.4s-2.88 1.008-3.384 2.4h-3.816c-1.32 0-2.4 1.080-2.4 2.4v16.8c0 1.32 1.080 2.4 2.4 2.4h14.4c1.32 0 2.4-1.080 2.4-2.4v-16.8c0-1.32-1.080-2.4-2.4-2.4h-3.816zM10.8 3.6c0 0.66-0.54 1.2-1.2 1.2s-1.2-0.54-1.2-1.2c0-0.66 0.54-1.2 1.2-1.2s1.2 0.54 1.2 1.2zM4.804 20.4c-0.665 0-1.204-0.533-1.204-1.2v0c0-0.663 0.525-1.2 1.204-1.2h5.993c0.665 0 1.204 0.533 1.204 1.2v0c0 0.663-0.525 1.2-1.204 1.2h-5.993zM4.794 15.6c-0.66 0-1.194-0.533-1.194-1.2v0c0-0.663 0.547-1.2 1.194-1.2h9.611c0.66 0 1.194 0.533 1.194 1.2v0c0 0.663-0.547 1.2-1.194 1.2h-9.611zM4.794 10.8c-0.66 0-1.194-0.533-1.194-1.2v0c0-0.663 0.547-1.2 1.194-1.2h9.611c0.66 0 1.194 0.533 1.194 1.2v0c0 0.663-0.547 1.2-1.194 1.2h-9.611z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-help" viewBox="0 0 24 24">
+ <title>aoc-help</title>
+ <path
+ d="M12 1c6.072 0 11 4.928 11 11s-4.928 11-11 11c-6.072 0-11-4.928-11-11s4.928-11 11-11zM12 19.5c0.69 0 1.25-0.56 1.25-1.25s-0.56-1.25-1.25-1.25c-0.69 0-1.25 0.56-1.25 1.25s0.56 1.25 1.25 1.25zM7.6 8.7c0 0.608 0.492 1.1 1.1 1.1s1.1-0.492 1.1-1.1c0-1.21 0.99-2.2 2.2-2.2s2.2 0.99 2.2 2.2c0 0.605-0.242 1.155-0.649 1.551l-1.364 1.386c-0.67 0.679-1.285 1.592-1.285 2.563h-0.002c0 0.608 0.492 1.1 1.1 1.1s1.1-0.492 1.1-1.1c0.067-1.003 0.7-1.417 1.287-2.013l0.99-1.012c0.627-0.627 1.023-1.507 1.023-2.475 0-2.431-1.969-4.4-4.4-4.4s-4.4 1.969-4.4 4.4z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-arrow-right" viewBox="0 0 24 24">
+ <title>aoc-arrow-right</title>
+ <path d="M9.295 7.115l1.41-1.41 6 6-6 6-1.41-1.41 4.58-4.59z"></path>
+ </symbol>
+ <symbol id="icon-aoc-arrow-left" viewBox="0 0 24 24">
+ <title>aoc-arrow-left</title>
+ <path d="M7.295 11.705l6-6 1.41 1.41-4.58 4.59 4.58 4.59-1.41 1.41z"></path>
+ </symbol>
+ <symbol id="icon-aoc-ticket" viewBox="0 0 24 24">
+ <title>aoc-ticket</title>
+ <path
+ d="M19.778 12.707l-8.485-8.485 2.828-2.828c0.391-0.391 1.024-0.391 1.414 0l2.311 2.311c-0.005 0.004-0.009 0.009-0.013 0.013-0.71 0.71-0.743 1.828-0.073 2.498s1.788 0.637 2.498-0.073c0.004-0.004 0.009-0.009 0.013-0.013l2.336 2.336c0.391 0.391 0.391 1.024 0 1.414l-2.828 2.828zM19.071 13.414l-9.192 9.192c-0.391 0.391-1.024 0.391-1.414 0l-2.336-2.336c0.697-0.711 0.725-1.819 0.060-2.484s-1.774-0.637-2.484 0.060l-2.311-2.311c-0.391-0.391-0.391-1.024 0-1.414l9.192-9.192 8.485 8.485zM5.636 14.121c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0l4.95-4.95c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-4.95 4.95zM8.464 16.95c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0l4.95-4.95c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-4.95 4.95z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-place-entry" viewBox="0 0 24 24">
+ <title>aoc-place-entry</title>
+ <path
+ d="M18 8c0-3.31-2.69-6-6-6s-6 2.69-6 6c0 4.5 6 11 6 11s6-6.5 6-11v0zM10 8c0-1.1 0.9-2 2-2s2 0.9 2 2c0 1.1-0.89 2-2 2-1.1 0-2-0.9-2-2v0zM5 20v2h14v-2h-14z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-facebook" viewBox="0 0 24 24">
+ <title>aoc-facebook</title>
+ <path
+ d="M20.895 2h-17.789c-0.61 0-1.105 0.495-1.105 1.105v17.789c0 0.61 0.495 1.105 1.105 1.105h9.579v-7.719h-2.614v-3.042h2.6v-2.221c0-2.586 1.575-3.993 3.881-3.993 0.783 0.002 1.566 0.047 2.344 0.133v2.698h-1.593c-1.253 0-1.495 0.593-1.495 1.467v1.93h3l-0.389 3.028h-2.611v7.719h5.088c0.61 0 1.105-0.495 1.105-1.105v-17.789c0-0.61-0.495-1.105-1.105-1.105z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-instagram" viewBox="0 0 24 24">
+ <title>aoc-instagram</title>
+ <path
+ d="M12 2c2.716 0 3.056 0.012 4.123 0.060 1.064 0.049 1.791 0.218 2.427 0.465 0.658 0.256 1.215 0.597 1.771 1.153s0.898 1.114 1.153 1.771c0.247 0.636 0.416 1.363 0.465 2.427 0.049 1.067 0.060 1.407 0.060 4.123s-0.012 3.056-0.060 4.123c-0.049 1.064-0.218 1.791-0.465 2.427-0.256 0.658-0.597 1.215-1.153 1.771s-1.114 0.898-1.771 1.153c-0.636 0.247-1.363 0.416-2.427 0.465-1.067 0.049-1.407 0.060-4.123 0.060s-3.056-0.012-4.123-0.060c-1.064-0.049-1.791-0.218-2.427-0.465-0.658-0.256-1.215-0.597-1.771-1.153s-0.898-1.114-1.153-1.771c-0.247-0.636-0.416-1.363-0.465-2.427-0.049-1.067-0.060-1.407-0.060-4.123s0.012-3.056 0.060-4.123c0.049-1.064 0.218-1.791 0.465-2.427 0.256-0.658 0.597-1.215 1.153-1.771s1.114-0.898 1.771-1.153c0.636-0.247 1.363-0.416 2.427-0.465 1.067-0.049 1.407-0.060 4.123-0.060zM12 3.802c-2.67 0-2.986 0.010-4.041 0.058-0.975 0.044-1.504 0.207-1.857 0.344-0.467 0.181-0.8 0.398-1.15 0.748s-0.567 0.683-0.748 1.15c-0.137 0.352-0.3 0.882-0.344 1.857-0.048 1.054-0.058 1.371-0.058 4.041s0.010 2.986 0.058 4.041c0.044 0.975 0.207 1.504 0.344 1.857 0.181 0.467 0.398 0.8 0.748 1.15s0.683 0.567 1.15 0.748c0.352 0.137 0.882 0.3 1.857 0.344 1.054 0.048 1.371 0.058 4.041 0.058s2.987-0.010 4.041-0.058c0.975-0.044 1.504-0.207 1.857-0.344 0.467-0.181 0.8-0.398 1.15-0.748s0.567-0.683 0.748-1.15c0.137-0.352 0.3-0.882 0.344-1.857 0.048-1.054 0.058-1.371 0.058-4.041s-0.010-2.986-0.058-4.041c-0.044-0.975-0.207-1.504-0.344-1.857-0.181-0.467-0.398-0.8-0.748-1.15s-0.683-0.567-1.15-0.748c-0.352-0.137-0.882-0.3-1.857-0.344-1.054-0.048-1.371-0.058-4.041-0.058zM12 6.865c2.836 0 5.135 2.299 5.135 5.135s-2.299 5.135-5.135 5.135c-2.836 0-5.135-2.299-5.135-5.135s2.299-5.135 5.135-5.135zM12 15.333c1.841 0 3.333-1.492 3.333-3.333s-1.492-3.333-3.333-3.333c-1.841 0-3.333 1.492-3.333 3.333s1.492 3.333 3.333 3.333zM18.538 6.662c0 0.663-0.537 1.2-1.2 1.2s-1.2-0.537-1.2-1.2 0.537-1.2 1.2-1.2c0.663 0 1.2 0.537 1.2 1.2z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-reddit" viewBox="0 0 24 24">
+ <title>aoc-reddit</title>
+ <path
+ d="M22.999 12.034c0-1.397-1.137-2.534-2.534-2.534-0.621 0-1.208 0.225-1.67 0.632-1.648-1.054-3.834-1.732-6.252-1.823l1.441-4.096 3.601 0.861c0.002 1.139 0.929 2.065 2.069 2.065 1.141 0 2.069-0.928 2.069-2.069s-0.929-2.069-2.069-2.069c-0.866 0-1.609 0.536-1.917 1.292l-4.265-1.020-1.771 5.030c-2.519 0.048-4.803 0.729-6.512 1.816-0.46-0.399-1.040-0.618-1.655-0.618-1.397 0-2.534 1.137-2.534 2.534 0 0.864 0.445 1.661 1.166 2.126-0.044 0.253-0.069 0.509-0.069 0.77 0 3.658 4.434 6.634 9.884 6.634s9.884-2.976 9.884-6.634c0-0.253-0.023-0.502-0.064-0.748 0.74-0.461 1.199-1.27 1.199-2.148l-0.001-0.001zM7.283 13.759c0-0.86 0.697-1.557 1.558-1.557 0.86 0 1.557 0.697 1.557 1.557 0 0.86-0.697 1.557-1.557 1.557-0.861 0-1.558-0.697-1.558-1.557zM15.652 17.971c-0.046 0.049-1.164 1.185-3.689 1.185-2.538 0-3.554-1.153-3.595-1.202-0.143-0.167-0.124-0.419 0.044-0.562 0.166-0.142 0.415-0.124 0.559 0.041 0.023 0.025 0.87 0.926 2.993 0.926 2.16 0 3.107-0.933 3.116-0.942 0.153-0.156 0.404-0.16 0.562-0.006s0.162 0.401 0.011 0.56l0.001-0.001zM15.342 15.316c-0.861 0-1.558-0.697-1.558-1.557s0.697-1.557 1.558-1.557c0.86 0 1.557 0.697 1.557 1.557 0 0.86-0.697 1.557-1.557 1.557z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-rss" viewBox="0 0 24 24">
+ <title>aoc-rss</title>
+ <path
+ d="M3 2c-0.552 0-1 0.448-1 1v18c0 0.552 0.448 1 1 1h18c0.552 0 1-0.448 1-1v-18c0-0.552-0.448-1-1-1h-18zM14.083 18.25h-2.381c0-3.282-2.671-5.952-5.953-5.952v-2.381c4.595 0 8.333 3.738 8.333 8.333zM18.25 18.25h-2.381c0-5.58-4.539-10.119-10.119-10.119v-2.381c6.892 0 12.5 5.608 12.5 12.5zM5.75 16.464c0-0.986 0.8-1.786 1.786-1.786s1.786 0.8 1.786 1.786c0 0.986-0.8 1.786-1.786 1.786s-1.786-0.8-1.786-1.786z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-twitter" viewBox="0 0 24 24">
+ <title>aoc-twitter</title>
+ <path
+ d="M23 6.072c-0.772 0.351-1.602 0.589-2.474 0.695 0.89-0.546 1.573-1.412 1.895-2.443-0.833 0.506-1.754 0.873-2.738 1.071-0.784-0.858-1.904-1.394-3.144-1.394-2.378 0-4.307 1.978-4.307 4.418 0 0.346 0.037 0.683 0.111 1.006-3.581-0.185-6.755-1.941-8.881-4.617-0.371 0.655-0.583 1.414-0.583 2.223 0 1.532 0.761 2.884 1.917 3.677-0.705-0.021-1.371-0.222-1.952-0.551v0.054c0 2.141 1.485 3.927 3.457 4.332-0.361 0.104-0.742 0.155-1.135 0.155-0.277 0-0.549-0.027-0.811-0.078 0.549 1.754 2.139 3.032 4.024 3.066-1.474 1.186-3.333 1.892-5.351 1.892-0.348 0-0.692-0.020-1.028-0.061 1.907 1.251 4.172 1.983 6.604 1.983 7.926 0 12.258-6.731 12.258-12.569 0-0.192-0.004-0.384-0.011-0.573 0.842-0.623 1.573-1.401 2.148-2.287z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-accommodation" viewBox="0 0 24 24">
+ <title>aoc-accommodation</title>
+ <path
+ d="M23 12v1h-17c-0.552 0-1-0.448-1-1s0.448-1 1-1h4v-2.834c0-0.552 0.448-1 1-1 0.051 0 0.102 0.004 0.152 0.012l11 1.692c0.488 0.075 0.848 0.495 0.848 0.988v2.142zM4 14h19v6h-3v-3h-16v3h-3v-15c0-0.552 0.448-1 1-1h1c0.552 0 1 0.448 1 1v9zM7 10c-1.105 0-2-0.895-2-2s0.895-2 2-2c1.105 0 2 0.895 2 2s-0.895 2-2 2z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-activity-level" viewBox="0 0 24 24">
+ <title>aoc-activity-level</title>
+ <path
+ d="M22.994 17.99h-7.239c-0.366 0-0.72-0.134-0.994-0.377l-11.172-9.883 3.409-4.016c0.422-0.557 0.839-0.788 1.251-0.694 0.619 0.141 0.619 2.349 2.249 3.47 0.909 0.625 1.601 0.94 5 0.5 0.889 1.249 2.099 5.976 3.050 6.747s4.447 1.234 4.447 3.753v0.5zM22.994 18.99v1c0 0.552-0.448 1-1 1l-6.864-0c-0.73 0-1.435-0.266-1.983-0.749l-10.808-9.522c-0.409-0.36-0.454-0.982-0.101-1.397l0.704-0.829 11.157 9.87c0.457 0.404 1.046 0.628 1.656 0.628h7.239z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-add-a-photo" viewBox="0 0 24 24">
+ <title>aoc-add-a-photo</title>
+ <path
+ d="M3 4v-3h2v3h3v2h-3v3h-2v-3h-3v-2h3zM6 10v-3h3v-3h7l1.83 2h3.17c1.1 0 2 0.9 2 2v12c0 1.1-0.9 2-2 2h-16c-1.1 0-2-0.9-2-2v-10h3zM13 19c2.76 0 5-2.24 5-5s-2.24-5-5-5c-2.76 0-5 2.24-5 5s2.24 5 5 5v0zM9.8 14c0 1.77 1.43 3.2 3.2 3.2s3.2-1.43 3.2-3.2c0-1.77-1.43-3.2-3.2-3.2s-3.2 1.43-3.2 3.2v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-add-box" viewBox="0 0 24 24">
+ <title>aoc-add-box</title>
+ <path
+ d="M19 3h-14c-1.11 0-2 0.9-2 2v14c0 1.1 0.89 2 2 2h14c1.1 0 2-0.9 2-2v-14c0-1.1-0.9-2-2-2v0zM17 13h-4v4h-2v-4h-4v-2h4v-4h2v4h4v2z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-add-shape" viewBox="0 0 24 24">
+ <title>aoc-add-shape</title>
+ <path d="M19 13h-6v6h-2v-6h-6v-2h6v-6h2v6h6z"></path>
+ </symbol>
+ <symbol id="icon-aoc-arrow-forward" viewBox="0 0 24 24">
+ <title>aoc-arrow-forward</title>
+ <path d="M12 4l-1.41 1.41 5.58 5.59h-12.17v2h12.17l-5.58 5.59 1.41 1.41 8-8z"></path>
+ </symbol>
+ <symbol id="icon-aoc-been-here" viewBox="0 0 24 24">
+ <title>aoc-been-here</title>
+ <path d="M7 20h-2l-1-16h2l1 16zM8.625 15l-0.625-10h12l-3 5 3 5h-11.375z"></path>
+ </symbol>
+ <symbol id="icon-aoc-chat-bubbles" viewBox="0 0 24 24">
+ <title>aoc-chat-bubbles</title>
+ <path
+ d="M21 6h-2v9h-13v2c0 0.55 0.45 1 1 1h11l4 4v-15c0-0.55-0.45-1-1-1v0zM17 12v-9c0-0.55-0.45-1-1-1h-13c-0.55 0-1 0.45-1 1v14l4-4h10c0.55 0 1-0.45 1-1v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-close" viewBox="0 0 24 24">
+ <title>aoc-close</title>
+ <path
+ d="M19 6.41l-1.41-1.41-5.59 5.59-5.59-5.59-1.41 1.41 5.59 5.59-5.59 5.59 1.41 1.41 5.59-5.59 5.59 5.59 1.41-1.41-5.59-5.59z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-expand-more" viewBox="0 0 24 24">
+ <title>aoc-expand-more</title>
+ <path d="M16.59 9l-4.59 4.58-4.59-4.58-1.41 1.41 6 6 6-6z"></path>
+ </symbol>
+ <symbol id="icon-aoc-expand-less" viewBox="0 0 24 24">
+ <title>aoc-expand-less</title>
+ <path d="M12 8l-6 6 1.41 1.41 4.59-4.58 4.59 4.58 1.41-1.41z"></path>
+ </symbol>
+ <symbol id="icon-aoc-forum-flag" viewBox="0 0 24 24">
+ <title>aoc-forum-flag</title>
+ <path
+ d="M6 15v6c0 0.552-0.448 1-1 1s-1-0.448-1-1v-18c0-0.552 0.448-1 1-1s1 0.448 1 1h14l-3 6 3 6h-14zM16.764 5h-10.764v8h10.764l-2-4 2-4z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-group-size" viewBox="0 0 24 24">
+ <title>aoc-group-size</title>
+ <path
+ d="M12 3c1.812 0 3.281 1.469 3.281 3.281s-1.469 3.281-3.281 3.281c-1.812 0-3.281-1.469-3.281-3.281s1.469-3.281 3.281-3.281zM6.292 10.178c-0.345 0.519-0.542 1.139-0.542 1.797v5.025h-4c-0.414 0-0.75-0.336-0.75-0.75v-5.266c0-0.688 0.468-1.288 1.136-1.455l0.71-0.178c0.582 0.63 1.416 1.024 2.341 1.024 0.388 0 0.76-0.069 1.104-0.197zM17.488 9.885c0.492 0.31 1.075 0.49 1.699 0.49 0.888 0 1.691-0.363 2.269-0.948l0.408 0.102c0.668 0.167 1.136 0.767 1.136 1.455v5.266c0 0.414-0.336 0.75-0.75 0.75h-4v-5.025c0-0.787-0.282-1.52-0.762-2.091zM19.188 9.375c-1.208 0-2.187-0.979-2.187-2.187s0.979-2.187 2.187-2.187c1.208 0 2.187 0.979 2.187 2.187s-0.979 2.187-2.187 2.187zM5.187 9.375c-1.208 0-2.187-0.979-2.187-2.187s0.979-2.187 2.187-2.187c1.208 0 2.187 0.979 2.187 2.187s-0.979 2.187-2.187 2.187zM9.279 9.587c0.74 0.61 1.688 0.976 2.721 0.976s1.981-0.366 2.721-0.976l0.824 0.206c1.002 0.25 1.704 1.15 1.704 2.183v6.9c0 0.621-0.504 1.125-1.125 1.125h-8.25c-0.621 0-1.125-0.504-1.125-1.125v-6.9c0-1.032 0.703-1.932 1.704-2.183l0.824-0.206z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-heart-outline" viewBox="0 0 24 24">
+ <title>aoc-heart-outline</title>
+ <path
+ d="M18.91 11.473c0.697-0.71 1.090-1.677 1.090-2.689s-0.394-1.979-1.091-2.689c-0.689-0.703-1.62-1.095-2.587-1.095s-1.897 0.393-2.587 1.095l-1.736 1.768-1.736-1.768c-1.433-1.46-3.741-1.46-5.174 0-1.454 1.481-1.454 3.897-0.031 5.347l6.941 6.765 6.91-6.734zM11.375 4.395l0.625 0.613 0.623-0.611c1.026-0.898 2.338-1.397 3.7-1.397 1.506 0 2.95 0.61 4.015 1.695s1.663 2.556 1.663 4.090-0.598 3.006-1.663 4.090l-8.337 8.125-8.337-8.125c-2.217-2.259-2.217-5.921 0-8.18 2.114-2.154 5.481-2.254 7.712-0.3z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-heart-solid" viewBox="0 0 24 24">
+ <title>aoc-heart-solid</title>
+ <path
+ d="M11.375 4.395l0.625 0.613 0.623-0.611c1.026-0.898 2.338-1.397 3.7-1.397 1.506 0 2.95 0.61 4.015 1.695s1.663 2.556 1.663 4.090-0.598 3.006-1.663 4.090l-8.337 8.125-8.337-8.125c-2.217-2.259-2.217-5.921 0-8.18 2.114-2.154 5.481-2.254 7.712-0.3z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-home" viewBox="0 0 24 24">
+ <title>aoc-home</title>
+ <path d="M15 22v-5c0-1.657-1.343-3-3-3s-3 1.343-3 3v5h-5v-11h-2v-1l9.995-8 10.005 8v1h-2v11h-5z"></path>
+ </symbol>
+ <symbol id="icon-aoc-important" viewBox="0 0 24 24">
+ <title>aoc-important</title>
+ <path
+ d="M21.775 18.469c0.641 1.125-0.164 2.531-1.444 2.531h-16.663c-1.283 0-2.083-1.408-1.444-2.531l8.331-14.626c0.641-1.125 2.247-1.123 2.887 0l8.331 14.626zM12 8c-0.552 0-1 0.448-1 1v5c0 0.552 0.448 1 1 1s1-0.448 1-1v-5c0-0.552-0.448-1-1-1zM12 19c0.552 0 1-0.448 1-1s-0.448-1-1-1c-0.552 0-1 0.448-1 1s0.448 1 1 1z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-knife-fork" viewBox="0 0 24 24">
+ <title>aoc-knife-fork</title>
+ <path
+ d="M9.25 2.75v4.625c0 0.345 0.28 0.625 0.625 0.625s0.625-0.28 0.625-0.625v-4.625c0-0.414 0.336-0.75 0.75-0.75s0.75 0.336 0.75 0.75v6.25c0 1.306-0.835 2.417-2 2.829v8.671c0 0.828-0.672 1.5-1.5 1.5s-1.5-0.672-1.5-1.5v-8.671c-1.165-0.412-2-1.523-2-2.829v-6.25c0-0.414 0.336-0.75 0.75-0.75s0.75 0.336 0.75 0.75v4.625c0 0.345 0.28 0.625 0.625 0.625s0.625-0.28 0.625-0.625v-4.625c0-0.414 0.336-0.75 0.75-0.75s0.75 0.336 0.75 0.75zM19 1v19.5c0 0.828-0.672 1.5-1.5 1.5s-1.5-0.672-1.5-1.5v-7.5h-1c-0.552 0-1-0.448-1-1v-6c0-2.761 2.239-5 5-5z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-library-books" viewBox="0 0 24 24">
+ <title>aoc-library-books</title>
+ <path
+ d="M4 6h-2v14c0 1.1 0.9 2 2 2h14v-2h-14v-14zM20 2h-12c-1.1 0-2 0.9-2 2v12c0 1.1 0.9 2 2 2h12c1.1 0 2-0.9 2-2v-12c0-1.1-0.9-2-2-2v0zM19 11h-10v-2h10v2zM15 15h-6v-2h6v2zM19 7h-10v-2h10v2z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-link" viewBox="0 0 24 24">
+ <title>aoc-link</title>
+ <path
+ d="M9.937 12.2c0.441-0.33 1.067-0.24 1.397 0.201 0.542 0.724 1.372 1.178 2.274 1.242s1.788-0.266 2.428-0.906l2.451-2.451c1.187-1.229 1.171-3.173-0.032-4.377-1.201-1.201-3.146-1.221-4.355-0.053l-1.421 1.413c-0.391 0.389-1.023 0.387-1.412-0.004s-0.387-1.023 0.004-1.412l1.431-1.423c2.002-1.934 5.192-1.904 7.164 0.067 1.975 1.975 1.998 5.165 0.044 7.187l-2.463 2.463c-1.049 1.049-2.502 1.591-3.982 1.485s-2.841-0.85-3.73-2.038c-0.33-0.441-0.24-1.067 0.201-1.397zM14.425 12.152c-0.441 0.33-1.067 0.24-1.397-0.201-0.542-0.724-1.372-1.178-2.274-1.242s-1.788 0.266-2.428 0.906l-2.451 2.451c-1.187 1.229-1.171 3.173 0.032 4.377 1.201 1.201 3.145 1.221 4.352 0.056l1.414-1.414c0.39-0.39 1.022-0.39 1.412 0s0.39 1.022-0 1.412l-1.426 1.426c-2.002 1.933-5.191 1.903-7.163-0.068-1.975-1.975-1.998-5.165-0.044-7.187l2.463-2.463c1.049-1.049 2.502-1.591 3.982-1.485s2.841 0.85 3.73 2.038c0.33 0.441 0.24 1.067-0.201 1.397z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-list-circle-bullets" viewBox="0 0 24 24">
+ <title>aoc-list-circle-bullets</title>
+ <path
+ d="M4 10.5c-0.83 0-1.5 0.67-1.5 1.5s0.67 1.5 1.5 1.5c0.83 0 1.5-0.67 1.5-1.5s-0.67-1.5-1.5-1.5v0zM4 4.5c-0.83 0-1.5 0.67-1.5 1.5s0.67 1.5 1.5 1.5c0.83 0 1.5-0.67 1.5-1.5s-0.67-1.5-1.5-1.5v0zM4 16.5c-0.835 0-1.5 0.677-1.5 1.5s0.677 1.5 1.5 1.5c0.823 0 1.5-0.677 1.5-1.5s-0.665-1.5-1.5-1.5v0zM7 19h14v-2h-14v2zM7 13h14v-2h-14v2zM7 5v2h14v-2h-14z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-list" viewBox="0 0 24 24">
+ <title>aoc-list</title>
+ <path d="M3 13h2v-2h-2v2zM3 17h2v-2h-2v2zM3 9h2v-2h-2v2zM7 13h14v-2h-14v2zM7 17h14v-2h-14v2zM7 7v2h14v-2h-14z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-location-add" viewBox="0 0 24 24">
+ <title>aoc-location-add</title>
+ <path
+ d="M12 2c-3.86 0-7 3.14-7 7 0 5.25 7 13 7 13s7-7.75 7-13c0-3.86-3.14-7-7-7v0zM16 10h-3v3h-2v-3h-3v-2h3v-3h2v3h3v2z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-location" viewBox="0 0 24 24">
+ <title>aoc-location</title>
+ <path
+ d="M12 2c-3.87 0-7 3.13-7 7 0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7v0zM12 11.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5c1.38 0 2.5 1.12 2.5 2.5s-1.12 2.5-2.5 2.5v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-mail" viewBox="0 0 24 24">
+ <title>aoc-mail</title>
+ <path
+ d="M20 4h-16c-1.1 0-1.99 0.9-1.99 2l-0.010 12c0 1.1 0.9 2 2 2h16c1.1 0 2-0.9 2-2v-12c0-1.1-0.9-2-2-2v0zM20 8l-8 5-8-5v-2l8 5 8-5v2z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-map" viewBox="0 0 24 24">
+ <title>aoc-map</title>
+ <path
+ d="M20.5 3l-0.16 0.030-5.34 2.070-6-2.1-5.64 1.9c-0.21 0.070-0.36 0.25-0.36 0.48v15.12c0 0.28 0.22 0.5 0.5 0.5l0.16-0.030 5.34-2.070 6 2.1 5.64-1.9c0.21-0.070 0.36-0.25 0.36-0.48v-15.12c0-0.28-0.22-0.5-0.5-0.5v0zM15 19l-6-2.11v-11.89l6 2.11v11.89z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-menu" viewBox="0 0 24 24">
+ <title>aoc-menu</title>
+ <path d="M3 5h18v2h-18v-2zM3 11h18v2h-18v-2zM3 17h18v2h-18v-2z"></path>
+ </symbol>
+ <symbol id="icon-aoc-more-horizontal" viewBox="0 0 24 24">
+ <title>aoc-more-horizontal</title>
+ <path
+ d="M6 10c-1.1 0-2 0.9-2 2s0.9 2 2 2c1.1 0 2-0.9 2-2s-0.9-2-2-2v0zM18 10c-1.1 0-2 0.9-2 2s0.9 2 2 2c1.1 0 2-0.9 2-2s-0.9-2-2-2v0zM12 10c-1.1 0-2 0.9-2 2s0.9 2 2 2c1.1 0 2-0.9 2-2s-0.9-2-2-2v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-my-location" viewBox="0 0 24 24">
+ <title>aoc-my-location</title>
+ <path
+ d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4c2.21 0 4-1.79 4-4s-1.79-4-4-4v0zM20.94 11c-0.46-4.17-3.77-7.48-7.94-7.94v-2.060h-2v2.060c-4.17 0.46-7.48 3.77-7.94 7.94h-2.060v2h2.060c0.46 4.17 3.77 7.48 7.94 7.94v2.060h2v-2.060c4.17-0.46 7.48-3.77 7.94-7.94h2.060v-2h-2.060zM12 19c-3.87 0-7-3.13-7-7s3.13-7 7-7c3.87 0 7 3.13 7 7s-3.13 7-7 7v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-near-me" viewBox="0 0 24 24">
+ <title>aoc-near-me</title>
+ <path d="M21 3l-18 7.53v0.98l6.84 2.65 2.64 6.84h0.98z"></path>
+ </symbol>
+ <symbol id="icon-aoc-notifications-alert" viewBox="0 0 24 24">
+ <title>aoc-notifications-alert</title>
+ <path
+ d="M18.988 10.589c0.008 0.136 0.012 0.273 0.012 0.411v5l2 1v2h-18v-2l2-1v-5c0-3.526 2.608-6.444 6-6.929v-1.071c0-0.552 0.448-1 1-1s1 0.447 1 1c-0.628 0.836-1 1.875-1 3 0 2.761 2.239 5 5 5 0.707 0 1.379-0.147 1.988-0.411zM10 20h4c0 1.105-0.895 2-2 2s-2-0.895-2-2zM17 9c-1.657 0-3-1.343-3-3s1.343-3 3-3c1.657 0 3 1.343 3 3s-1.343 3-3 3z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-notifications-mentions" viewBox="0 0 24 24">
+ <title>aoc-notifications-mentions</title>
+ <path
+ d="M15.52 15.487c-0.585 0.857-1.949 1.786-3.776 1.786-3.094 0-5.189-2.154-5.189-5.164 0-2.937 2.144-5.237 5.092-5.237 1.486 0 2.704 0.587 3.265 1.297v-1.126h2.12v6.534c0 1.126 0.39 1.835 1.535 1.835 1.34 0 2.388-1.786 2.388-4.601 0-4.943-3.411-7.978-8.771-7.978-5.677 0-9.039 4.185-9.039 9.201 0 5.555 3.898 9.103 9.623 9.103 2.68 0 4.848-1.003 6.383-2.349l1.121 1.37c-1.437 1.444-4.166 2.839-7.553 2.839-7.358 0-11.719-4.748-11.719-10.914 0-6.412 4.629-11.086 11.256-11.086 6.797 0 10.744 4.283 10.744 9.715 0 4.209-1.998 6.534-4.653 6.534-1.657 0-2.509-0.832-2.826-1.762zM8.75 12.011c0 1.747 1.19 2.989 2.929 2.989s3.071-1.149 3.071-2.989c0-1.839-1.357-3.011-3.095-3.011s-2.905 1.241-2.905 3.011z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-notifications-muted" viewBox="0 0 24 24">
+ <title>aoc-notifications-muted</title>
+ <path
+ d="M13 4.071c3.392 0.485 6 3.403 6 6.929v5l2 1v2h-18v-2l2-1v-5c0-3.526 2.608-6.444 6-6.929v-1.071c0-0.552 0.448-1 1-1s1 0.448 1 1v1.071zM10 20h4c0 1.105-0.895 2-2 2s-2-0.895-2-2zM13.407 11.993l2.828-2.828-1.414-1.414-2.828 2.828-2.828-2.828-1.414 1.414 2.828 2.828-2.828 2.828 1.414 1.414 2.828-2.828 2.828 2.828 1.414-1.414-2.828-2.828z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-notifications-tracking" viewBox="0 0 24 24">
+ <title>aoc-notifications-tracking</title>
+ <path
+ d="M19 9.9c0.323 0.066 0.658 0.1 1 0.1s0.677-0.034 1-0.1v10.1c0 1.105-0.895 2-2 2h-14c-1.105 0-2-0.895-2-2v-14c0-1.105 0.895-2 2-2h10.1c-0.066 0.323-0.1 0.658-0.1 1s0.034 0.677 0.1 1h-10.1v14h14v-10.1zM20 8c-1.657 0-3-1.343-3-3s1.343-3 3-3c1.657 0 3 1.343 3 3s-1.343 3-3 3z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-open-in-new" viewBox="0 0 24 24">
+ <title>aoc-open-in-new</title>
+ <path
+ d="M19 19h-14v-14h7v-2h-7c-1.11 0-2 0.9-2 2v14c0 1.1 0.89 2 2 2h14c1.1 0 2-0.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41 9.83-9.83v3.59h2v-7h-7z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-pencil" viewBox="0 0 24 24">
+ <title>aoc-pencil</title>
+ <path
+ d="M3 17.25v3.75h3.75l11.060-11.060-3.75-3.75-11.060 11.060zM20.71 7.040c0.39-0.39 0.39-1.020 0-1.41l-2.34-2.34c-0.39-0.39-1.020-0.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-person" viewBox="0 0 24 24">
+ <title>aoc-person</title>
+ <path
+ d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4c-2.21 0-4 1.79-4 4s1.79 4 4 4v0zM12 14c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-pinned" viewBox="0 0 24 24">
+ <title>aoc-pinned</title>
+ <path
+ d="M6.515 16.077l-4.223-4.223c1.774-1.774 4.266-2.391 6.541-1.853l5.516-4.667c-0.493-1.098-0.288-2.433 0.614-3.335l7.039 7.039c-0.902 0.902-2.236 1.106-3.335 0.614l-4.667 5.516c0.539 2.274-0.079 4.767-1.852 6.541l-4.223-4.223-4.223 4.223c-0.389 0.389-1.019 0.389-1.408 0s-0.389-1.019 0-1.408l4.223-4.223z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-plane-takeoff" viewBox="0 0 24 24">
+ <title>aoc-plane-takeoff</title>
+ <path
+ d="M2.5 19h19v2h-19v-2zM22.070 9.64c-0.21-0.8-1.040-1.28-1.84-1.060l-5.31 1.42-6.9-6.43-1.93 0.51 4.14 7.17-4.97 1.33-1.97-1.54-1.45 0.39 1.82 3.16 0.77 1.33 1.6-0.43 14.97-4c0.81-0.23 1.28-1.050 1.070-1.85v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-plane" viewBox="0 0 24 24">
+ <title>aoc-plane</title>
+ <path
+ d="M21 16v-2l-8-5v-5.5c0-0.83-0.67-1.5-1.5-1.5s-1.5 0.67-1.5 1.5v5.5l-8 5v2l8-2.5v5.5l-2 1.5v1.5l3.5-1 3.5 1v-1.5l-2-1.5v-5.5l8 2.5z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-print" viewBox="0 0 24 24">
+ <title>aoc-print</title>
+ <path
+ d="M19 8h-14c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3v0zM16 19h-8v-5h8v5zM19 12c-0.55 0-1-0.45-1-1s0.45-1 1-1c0.55 0 1 0.45 1 1s-0.45 1-1 1v0zM18 3h-12v4h12v-4z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-reply" viewBox="0 0 24 24">
+ <title>aoc-reply</title>
+ <path d="M10 9v-4l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11v0z"></path>
+ </symbol>
+ <symbol id="icon-aoc-search" viewBox="0 0 24 24">
+ <title>aoc-search</title>
+ <path
+ d="M15.5 14h-0.79l-0.28-0.27c0.98-1.14 1.57-2.62 1.57-4.23 0-3.59-2.91-6.5-6.5-6.5s-6.5 2.91-6.5 6.5c0 3.59 2.91 6.5 6.5 6.5 1.61 0 3.090-0.59 4.23-1.57l0.27 0.28v0.79l5 4.99 1.49-1.49-4.99-5zM9.5 14c-2.49 0-4.5-2.010-4.5-4.5s2.010-4.5 4.5-4.5c2.49 0 4.5 2.010 4.5 4.5s-2.010 4.5-4.5 4.5v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-shuffle" viewBox="0 0 24 24">
+ <title>aoc-shuffle</title>
+ <path
+ d="M10.59 9.17l-5.18-5.17-1.41 1.41 5.17 5.17 1.42-1.41zM14.5 4l2.040 2.040-12.54 12.55 1.41 1.41 12.55-12.54 2.040 2.040v-5.5h-5.5zM14.83 13.41l-1.41 1.41 3.13 3.13-2.050 2.050h5.5v-5.5l-2.040 2.040-3.13-3.13z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-star" viewBox="0 0 24 24">
+ <title>aoc-star</title>
+ <path
+ d="M18.18 21l-1.635-7.029 5.455-4.727-7.191-0.617-2.809-6.627-2.809 6.627-7.191 0.617 5.455 4.727-1.635 7.029 6.18-3.728z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-subject" viewBox="0 0 24 24">
+ <title>aoc-subject</title>
+ <path d="M14 17h-10v2h10v-2zM20 9h-16v2h16v-2zM4 15h16v-2h-16v2zM4 5v2h16v-2h-16z"></path>
+ </symbol>
+ <symbol id="icon-aoc-trip-style" viewBox="0 0 24 24">
+ <title>aoc-trip-style</title>
+ <path
+ d="M20 6c1.11 0 2 0.89 2 2v11c0 1.11-0.89 2-2 2h-16c-1.11 0-2-0.89-2-2l0.010-11c0-1.11 0.88-2 1.99-2h4v-2c0-1.11 0.89-2 2-2h4c1.11 0 2 0.89 2 2v2h4zM14 6v-2h-4v2h4zM6.5 14c0.828 0 1.5-0.672 1.5-1.5s-0.672-1.5-1.5-1.5c-0.828 0-1.5 0.672-1.5 1.5s0.672 1.5 1.5 1.5zM6 16.042l0.347 1.97 5.909-1.042-0.347-1.97-5.909 1.042z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-unpinned" viewBox="0 0 24 24">
+ <title>aoc-unpinned</title>
+ <path
+ d="M5.416 12.15l6.433 6.433c0.362-0.93 0.439-1.959 0.203-2.955l-0.233-0.982 5.94-7.020-1.386-1.386-7.020 5.94-0.982-0.233c-0.996-0.236-2.025-0.16-2.955 0.203zM6.515 16.077l-4.223-4.223c1.774-1.774 4.266-2.391 6.541-1.853l5.516-4.667c-0.493-1.098-0.288-2.433 0.614-3.335l7.039 7.039c-0.902 0.902-2.236 1.106-3.335 0.614l-4.667 5.516c0.539 2.274-0.079 4.767-1.852 6.541l-4.223-4.223-4.223 4.223c-0.389 0.389-1.019 0.389-1.408 0s-0.389-1.019 0-1.408l4.223-4.223z">
+ </path>
+ </symbol>
+ </defs>
+ </svg>
+</body>
+<svg aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1"
+ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <symbol id="icon-aoc-cancel" viewBox="0 0 24 24">
+ <title>aoc-cancel</title>
+ <path
+ d="M12 2c-5.53 0-10 4.47-10 10s4.47 10 10 10c5.53 0 10-4.47 10-10s-4.47-10-10-10v0zM17 15.59l-1.41 1.41-3.59-3.59-3.59 3.59-1.41-1.41 3.59-3.59-3.59-3.59 1.41-1.41 3.59 3.59 3.59-3.59 1.41 1.41-3.59 3.59 3.59 3.59z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-video" viewBox="0 0 24 24">
+ <title>aoc-video</title>
+ <path
+ d="M10 16.5l6-4.5-6-4.5v9zM12 2c-5.52 0-10 4.48-10 10s4.48 10 10 10c5.52 0 10-4.48 10-10s-4.48-10-10-10v0zM12 20c-4.41 0-8-3.59-8-8s3.59-8 8-8c4.41 0 8 3.59 8 8s-3.59 8-8 8v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-building" viewBox="0 0 24 24">
+ <title>aoc-building</title>
+ <path
+ d="M16 8.050c-0.096 0.098-0.187 0.202-0.272 0.312-1.061 0.455-1.911 1.305-2.366 2.366-0.129 0.099-0.25 0.207-0.362 0.322v-0.050h-2v2h1.036c-0.024 0.164-0.036 0.331-0.036 0.5 0 1.883 1.487 3.419 3.351 3.497 0.2 0.177 0.418 0.334 0.649 0.468v4.535h-5v-6h-4v6h-5v-20h14v6.050zM5 11v2h2v-2h-2zM5 7v2h2v-2h-2zM8 7v2h2v-2h-2zM8 11v2h2v-2h-2zM11 7v2h2v-2h-2zM17 16.829c-0.485-0.171-0.913-0.464-1.247-0.842-0.083 0.008-0.167 0.013-0.253 0.013-1.381 0-2.5-1.119-2.5-2.5 0-0.898 0.474-1.686 1.185-2.127 0.349-1.027 1.161-1.839 2.188-2.188 0.441-0.711 1.228-1.185 2.127-1.185 1.381 0 2.5 1.119 2.5 2.5 0 0.185-0.020 0.365-0.058 0.539 1.17 0.209 2.058 1.231 2.058 2.461 0 1.381-1.119 2.5-2.5 2.5-0.085 0-0.17-0.004-0.253-0.013-0.334 0.378-0.762 0.67-1.247 0.842v5.171h-2v-5.171z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-clock" viewBox="0 0 24 24">
+ <title>aoc-clock</title>
+ <path
+ d="M11.99 2c5.53 0 10.010 4.48 10.010 10s-4.48 10-10.010 10c-5.52 0-9.99-4.48-9.99-10s4.47-10 9.99-10zM13 11.423v-5.423c0-0.552-0.448-1-1-1s-1 0.448-1 1v6.577l3.964 2.289c0.478 0.276 1.090 0.112 1.366-0.366s0.112-1.090-0.366-1.366l-2.964-1.711z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-clipboard" viewBox="0 0 19 24">
+ <title>aoc-clipboard</title>
+ <path
+ d="M12.984 2.4c-0.504-1.392-1.824-2.4-3.384-2.4s-2.88 1.008-3.384 2.4h-3.816c-1.32 0-2.4 1.080-2.4 2.4v16.8c0 1.32 1.080 2.4 2.4 2.4h14.4c1.32 0 2.4-1.080 2.4-2.4v-16.8c0-1.32-1.080-2.4-2.4-2.4h-3.816zM10.8 3.6c0 0.66-0.54 1.2-1.2 1.2s-1.2-0.54-1.2-1.2c0-0.66 0.54-1.2 1.2-1.2s1.2 0.54 1.2 1.2zM4.804 20.4c-0.665 0-1.204-0.533-1.204-1.2v0c0-0.663 0.525-1.2 1.204-1.2h5.993c0.665 0 1.204 0.533 1.204 1.2v0c0 0.663-0.525 1.2-1.204 1.2h-5.993zM4.794 15.6c-0.66 0-1.194-0.533-1.194-1.2v0c0-0.663 0.547-1.2 1.194-1.2h9.611c0.66 0 1.194 0.533 1.194 1.2v0c0 0.663-0.547 1.2-1.194 1.2h-9.611zM4.794 10.8c-0.66 0-1.194-0.533-1.194-1.2v0c0-0.663 0.547-1.2 1.194-1.2h9.611c0.66 0 1.194 0.533 1.194 1.2v0c0 0.663-0.547 1.2-1.194 1.2h-9.611z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-help" viewBox="0 0 24 24">
+ <title>aoc-help</title>
+ <path
+ d="M12 1c6.072 0 11 4.928 11 11s-4.928 11-11 11c-6.072 0-11-4.928-11-11s4.928-11 11-11zM12 19.5c0.69 0 1.25-0.56 1.25-1.25s-0.56-1.25-1.25-1.25c-0.69 0-1.25 0.56-1.25 1.25s0.56 1.25 1.25 1.25zM7.6 8.7c0 0.608 0.492 1.1 1.1 1.1s1.1-0.492 1.1-1.1c0-1.21 0.99-2.2 2.2-2.2s2.2 0.99 2.2 2.2c0 0.605-0.242 1.155-0.649 1.551l-1.364 1.386c-0.67 0.679-1.285 1.592-1.285 2.563h-0.002c0 0.608 0.492 1.1 1.1 1.1s1.1-0.492 1.1-1.1c0.067-1.003 0.7-1.417 1.287-2.013l0.99-1.012c0.627-0.627 1.023-1.507 1.023-2.475 0-2.431-1.969-4.4-4.4-4.4s-4.4 1.969-4.4 4.4z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-arrow-right" viewBox="0 0 24 24">
+ <title>aoc-arrow-right</title>
+ <path d="M9.295 7.115l1.41-1.41 6 6-6 6-1.41-1.41 4.58-4.59z"></path>
+ </symbol>
+ <symbol id="icon-aoc-arrow-left" viewBox="0 0 24 24">
+ <title>aoc-arrow-left</title>
+ <path d="M7.295 11.705l6-6 1.41 1.41-4.58 4.59 4.58 4.59-1.41 1.41z"></path>
+ </symbol>
+ <symbol id="icon-aoc-ticket" viewBox="0 0 24 24">
+ <title>aoc-ticket</title>
+ <path
+ d="M19.778 12.707l-8.485-8.485 2.828-2.828c0.391-0.391 1.024-0.391 1.414 0l2.311 2.311c-0.005 0.004-0.009 0.009-0.013 0.013-0.71 0.71-0.743 1.828-0.073 2.498s1.788 0.637 2.498-0.073c0.004-0.004 0.009-0.009 0.013-0.013l2.336 2.336c0.391 0.391 0.391 1.024 0 1.414l-2.828 2.828zM19.071 13.414l-9.192 9.192c-0.391 0.391-1.024 0.391-1.414 0l-2.336-2.336c0.697-0.711 0.725-1.819 0.060-2.484s-1.774-0.637-2.484 0.060l-2.311-2.311c-0.391-0.391-0.391-1.024 0-1.414l9.192-9.192 8.485 8.485zM5.636 14.121c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0l4.95-4.95c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-4.95 4.95zM8.464 16.95c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0l4.95-4.95c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-4.95 4.95z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-place-entry" viewBox="0 0 24 24">
+ <title>aoc-place-entry</title>
+ <path
+ d="M18 8c0-3.31-2.69-6-6-6s-6 2.69-6 6c0 4.5 6 11 6 11s6-6.5 6-11v0zM10 8c0-1.1 0.9-2 2-2s2 0.9 2 2c0 1.1-0.89 2-2 2-1.1 0-2-0.9-2-2v0zM5 20v2h14v-2h-14z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-facebook" viewBox="0 0 24 24">
+ <title>aoc-facebook</title>
+ <path
+ d="M20.895 2h-17.789c-0.61 0-1.105 0.495-1.105 1.105v17.789c0 0.61 0.495 1.105 1.105 1.105h9.579v-7.719h-2.614v-3.042h2.6v-2.221c0-2.586 1.575-3.993 3.881-3.993 0.783 0.002 1.566 0.047 2.344 0.133v2.698h-1.593c-1.253 0-1.495 0.593-1.495 1.467v1.93h3l-0.389 3.028h-2.611v7.719h5.088c0.61 0 1.105-0.495 1.105-1.105v-17.789c0-0.61-0.495-1.105-1.105-1.105z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-instagram" viewBox="0 0 24 24">
+ <title>aoc-instagram</title>
+ <path
+ d="M12 2c2.716 0 3.056 0.012 4.123 0.060 1.064 0.049 1.791 0.218 2.427 0.465 0.658 0.256 1.215 0.597 1.771 1.153s0.898 1.114 1.153 1.771c0.247 0.636 0.416 1.363 0.465 2.427 0.049 1.067 0.060 1.407 0.060 4.123s-0.012 3.056-0.060 4.123c-0.049 1.064-0.218 1.791-0.465 2.427-0.256 0.658-0.597 1.215-1.153 1.771s-1.114 0.898-1.771 1.153c-0.636 0.247-1.363 0.416-2.427 0.465-1.067 0.049-1.407 0.060-4.123 0.060s-3.056-0.012-4.123-0.060c-1.064-0.049-1.791-0.218-2.427-0.465-0.658-0.256-1.215-0.597-1.771-1.153s-0.898-1.114-1.153-1.771c-0.247-0.636-0.416-1.363-0.465-2.427-0.049-1.067-0.060-1.407-0.060-4.123s0.012-3.056 0.060-4.123c0.049-1.064 0.218-1.791 0.465-2.427 0.256-0.658 0.597-1.215 1.153-1.771s1.114-0.898 1.771-1.153c0.636-0.247 1.363-0.416 2.427-0.465 1.067-0.049 1.407-0.060 4.123-0.060zM12 3.802c-2.67 0-2.986 0.010-4.041 0.058-0.975 0.044-1.504 0.207-1.857 0.344-0.467 0.181-0.8 0.398-1.15 0.748s-0.567 0.683-0.748 1.15c-0.137 0.352-0.3 0.882-0.344 1.857-0.048 1.054-0.058 1.371-0.058 4.041s0.010 2.986 0.058 4.041c0.044 0.975 0.207 1.504 0.344 1.857 0.181 0.467 0.398 0.8 0.748 1.15s0.683 0.567 1.15 0.748c0.352 0.137 0.882 0.3 1.857 0.344 1.054 0.048 1.371 0.058 4.041 0.058s2.987-0.010 4.041-0.058c0.975-0.044 1.504-0.207 1.857-0.344 0.467-0.181 0.8-0.398 1.15-0.748s0.567-0.683 0.748-1.15c0.137-0.352 0.3-0.882 0.344-1.857 0.048-1.054 0.058-1.371 0.058-4.041s-0.010-2.986-0.058-4.041c-0.044-0.975-0.207-1.504-0.344-1.857-0.181-0.467-0.398-0.8-0.748-1.15s-0.683-0.567-1.15-0.748c-0.352-0.137-0.882-0.3-1.857-0.344-1.054-0.048-1.371-0.058-4.041-0.058zM12 6.865c2.836 0 5.135 2.299 5.135 5.135s-2.299 5.135-5.135 5.135c-2.836 0-5.135-2.299-5.135-5.135s2.299-5.135 5.135-5.135zM12 15.333c1.841 0 3.333-1.492 3.333-3.333s-1.492-3.333-3.333-3.333c-1.841 0-3.333 1.492-3.333 3.333s1.492 3.333 3.333 3.333zM18.538 6.662c0 0.663-0.537 1.2-1.2 1.2s-1.2-0.537-1.2-1.2 0.537-1.2 1.2-1.2c0.663 0 1.2 0.537 1.2 1.2z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-reddit" viewBox="0 0 24 24">
+ <title>aoc-reddit</title>
+ <path
+ d="M22.999 12.034c0-1.397-1.137-2.534-2.534-2.534-0.621 0-1.208 0.225-1.67 0.632-1.648-1.054-3.834-1.732-6.252-1.823l1.441-4.096 3.601 0.861c0.002 1.139 0.929 2.065 2.069 2.065 1.141 0 2.069-0.928 2.069-2.069s-0.929-2.069-2.069-2.069c-0.866 0-1.609 0.536-1.917 1.292l-4.265-1.020-1.771 5.030c-2.519 0.048-4.803 0.729-6.512 1.816-0.46-0.399-1.040-0.618-1.655-0.618-1.397 0-2.534 1.137-2.534 2.534 0 0.864 0.445 1.661 1.166 2.126-0.044 0.253-0.069 0.509-0.069 0.77 0 3.658 4.434 6.634 9.884 6.634s9.884-2.976 9.884-6.634c0-0.253-0.023-0.502-0.064-0.748 0.74-0.461 1.199-1.27 1.199-2.148l-0.001-0.001zM7.283 13.759c0-0.86 0.697-1.557 1.558-1.557 0.86 0 1.557 0.697 1.557 1.557 0 0.86-0.697 1.557-1.557 1.557-0.861 0-1.558-0.697-1.558-1.557zM15.652 17.971c-0.046 0.049-1.164 1.185-3.689 1.185-2.538 0-3.554-1.153-3.595-1.202-0.143-0.167-0.124-0.419 0.044-0.562 0.166-0.142 0.415-0.124 0.559 0.041 0.023 0.025 0.87 0.926 2.993 0.926 2.16 0 3.107-0.933 3.116-0.942 0.153-0.156 0.404-0.16 0.562-0.006s0.162 0.401 0.011 0.56l0.001-0.001zM15.342 15.316c-0.861 0-1.558-0.697-1.558-1.557s0.697-1.557 1.558-1.557c0.86 0 1.557 0.697 1.557 1.557 0 0.86-0.697 1.557-1.557 1.557z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-rss" viewBox="0 0 24 24">
+ <title>aoc-rss</title>
+ <path
+ d="M3 2c-0.552 0-1 0.448-1 1v18c0 0.552 0.448 1 1 1h18c0.552 0 1-0.448 1-1v-18c0-0.552-0.448-1-1-1h-18zM14.083 18.25h-2.381c0-3.282-2.671-5.952-5.953-5.952v-2.381c4.595 0 8.333 3.738 8.333 8.333zM18.25 18.25h-2.381c0-5.58-4.539-10.119-10.119-10.119v-2.381c6.892 0 12.5 5.608 12.5 12.5zM5.75 16.464c0-0.986 0.8-1.786 1.786-1.786s1.786 0.8 1.786 1.786c0 0.986-0.8 1.786-1.786 1.786s-1.786-0.8-1.786-1.786z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-twitter" viewBox="0 0 24 24">
+ <title>aoc-twitter</title>
+ <path
+ d="M23 6.072c-0.772 0.351-1.602 0.589-2.474 0.695 0.89-0.546 1.573-1.412 1.895-2.443-0.833 0.506-1.754 0.873-2.738 1.071-0.784-0.858-1.904-1.394-3.144-1.394-2.378 0-4.307 1.978-4.307 4.418 0 0.346 0.037 0.683 0.111 1.006-3.581-0.185-6.755-1.941-8.881-4.617-0.371 0.655-0.583 1.414-0.583 2.223 0 1.532 0.761 2.884 1.917 3.677-0.705-0.021-1.371-0.222-1.952-0.551v0.054c0 2.141 1.485 3.927 3.457 4.332-0.361 0.104-0.742 0.155-1.135 0.155-0.277 0-0.549-0.027-0.811-0.078 0.549 1.754 2.139 3.032 4.024 3.066-1.474 1.186-3.333 1.892-5.351 1.892-0.348 0-0.692-0.020-1.028-0.061 1.907 1.251 4.172 1.983 6.604 1.983 7.926 0 12.258-6.731 12.258-12.569 0-0.192-0.004-0.384-0.011-0.573 0.842-0.623 1.573-1.401 2.148-2.287z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-accommodation" viewBox="0 0 24 24">
+ <title>aoc-accommodation</title>
+ <path
+ d="M23 12v1h-17c-0.552 0-1-0.448-1-1s0.448-1 1-1h4v-2.834c0-0.552 0.448-1 1-1 0.051 0 0.102 0.004 0.152 0.012l11 1.692c0.488 0.075 0.848 0.495 0.848 0.988v2.142zM4 14h19v6h-3v-3h-16v3h-3v-15c0-0.552 0.448-1 1-1h1c0.552 0 1 0.448 1 1v9zM7 10c-1.105 0-2-0.895-2-2s0.895-2 2-2c1.105 0 2 0.895 2 2s-0.895 2-2 2z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-activity-level" viewBox="0 0 24 24">
+ <title>aoc-activity-level</title>
+ <path
+ d="M22.994 17.99h-7.239c-0.366 0-0.72-0.134-0.994-0.377l-11.172-9.883 3.409-4.016c0.422-0.557 0.839-0.788 1.251-0.694 0.619 0.141 0.619 2.349 2.249 3.47 0.909 0.625 1.601 0.94 5 0.5 0.889 1.249 2.099 5.976 3.050 6.747s4.447 1.234 4.447 3.753v0.5zM22.994 18.99v1c0 0.552-0.448 1-1 1l-6.864-0c-0.73 0-1.435-0.266-1.983-0.749l-10.808-9.522c-0.409-0.36-0.454-0.982-0.101-1.397l0.704-0.829 11.157 9.87c0.457 0.404 1.046 0.628 1.656 0.628h7.239z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-add-a-photo" viewBox="0 0 24 24">
+ <title>aoc-add-a-photo</title>
+ <path
+ d="M3 4v-3h2v3h3v2h-3v3h-2v-3h-3v-2h3zM6 10v-3h3v-3h7l1.83 2h3.17c1.1 0 2 0.9 2 2v12c0 1.1-0.9 2-2 2h-16c-1.1 0-2-0.9-2-2v-10h3zM13 19c2.76 0 5-2.24 5-5s-2.24-5-5-5c-2.76 0-5 2.24-5 5s2.24 5 5 5v0zM9.8 14c0 1.77 1.43 3.2 3.2 3.2s3.2-1.43 3.2-3.2c0-1.77-1.43-3.2-3.2-3.2s-3.2 1.43-3.2 3.2v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-add-box" viewBox="0 0 24 24">
+ <title>aoc-add-box</title>
+ <path
+ d="M19 3h-14c-1.11 0-2 0.9-2 2v14c0 1.1 0.89 2 2 2h14c1.1 0 2-0.9 2-2v-14c0-1.1-0.9-2-2-2v0zM17 13h-4v4h-2v-4h-4v-2h4v-4h2v4h4v2z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-add-shape" viewBox="0 0 24 24">
+ <title>aoc-add-shape</title>
+ <path d="M19 13h-6v6h-2v-6h-6v-2h6v-6h2v6h6z"></path>
+ </symbol>
+ <symbol id="icon-aoc-arrow-forward" viewBox="0 0 24 24">
+ <title>aoc-arrow-forward</title>
+ <path d="M12 4l-1.41 1.41 5.58 5.59h-12.17v2h12.17l-5.58 5.59 1.41 1.41 8-8z"></path>
+ </symbol>
+ <symbol id="icon-aoc-been-here" viewBox="0 0 24 24">
+ <title>aoc-been-here</title>
+ <path d="M7 20h-2l-1-16h2l1 16zM8.625 15l-0.625-10h12l-3 5 3 5h-11.375z"></path>
+ </symbol>
+ <symbol id="icon-aoc-chat-bubbles" viewBox="0 0 24 24">
+ <title>aoc-chat-bubbles</title>
+ <path
+ d="M21 6h-2v9h-13v2c0 0.55 0.45 1 1 1h11l4 4v-15c0-0.55-0.45-1-1-1v0zM17 12v-9c0-0.55-0.45-1-1-1h-13c-0.55 0-1 0.45-1 1v14l4-4h10c0.55 0 1-0.45 1-1v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-close" viewBox="0 0 24 24">
+ <title>aoc-close</title>
+ <path
+ d="M19 6.41l-1.41-1.41-5.59 5.59-5.59-5.59-1.41 1.41 5.59 5.59-5.59 5.59 1.41 1.41 5.59-5.59 5.59 5.59 1.41-1.41-5.59-5.59z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-expand-more" viewBox="0 0 24 24">
+ <title>aoc-expand-more</title>
+ <path d="M16.59 9l-4.59 4.58-4.59-4.58-1.41 1.41 6 6 6-6z"></path>
+ </symbol>
+ <symbol id="icon-aoc-expand-less" viewBox="0 0 24 24">
+ <title>aoc-expand-less</title>
+ <path d="M12 8l-6 6 1.41 1.41 4.59-4.58 4.59 4.58 1.41-1.41z"></path>
+ </symbol>
+ <symbol id="icon-aoc-forum-flag" viewBox="0 0 24 24">
+ <title>aoc-forum-flag</title>
+ <path
+ d="M6 15v6c0 0.552-0.448 1-1 1s-1-0.448-1-1v-18c0-0.552 0.448-1 1-1s1 0.448 1 1h14l-3 6 3 6h-14zM16.764 5h-10.764v8h10.764l-2-4 2-4z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-group-size" viewBox="0 0 24 24">
+ <title>aoc-group-size</title>
+ <path
+ d="M12 3c1.812 0 3.281 1.469 3.281 3.281s-1.469 3.281-3.281 3.281c-1.812 0-3.281-1.469-3.281-3.281s1.469-3.281 3.281-3.281zM6.292 10.178c-0.345 0.519-0.542 1.139-0.542 1.797v5.025h-4c-0.414 0-0.75-0.336-0.75-0.75v-5.266c0-0.688 0.468-1.288 1.136-1.455l0.71-0.178c0.582 0.63 1.416 1.024 2.341 1.024 0.388 0 0.76-0.069 1.104-0.197zM17.488 9.885c0.492 0.31 1.075 0.49 1.699 0.49 0.888 0 1.691-0.363 2.269-0.948l0.408 0.102c0.668 0.167 1.136 0.767 1.136 1.455v5.266c0 0.414-0.336 0.75-0.75 0.75h-4v-5.025c0-0.787-0.282-1.52-0.762-2.091zM19.188 9.375c-1.208 0-2.187-0.979-2.187-2.187s0.979-2.187 2.187-2.187c1.208 0 2.187 0.979 2.187 2.187s-0.979 2.187-2.187 2.187zM5.187 9.375c-1.208 0-2.187-0.979-2.187-2.187s0.979-2.187 2.187-2.187c1.208 0 2.187 0.979 2.187 2.187s-0.979 2.187-2.187 2.187zM9.279 9.587c0.74 0.61 1.688 0.976 2.721 0.976s1.981-0.366 2.721-0.976l0.824 0.206c1.002 0.25 1.704 1.15 1.704 2.183v6.9c0 0.621-0.504 1.125-1.125 1.125h-8.25c-0.621 0-1.125-0.504-1.125-1.125v-6.9c0-1.032 0.703-1.932 1.704-2.183l0.824-0.206z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-heart-outline" viewBox="0 0 24 24">
+ <title>aoc-heart-outline</title>
+ <path
+ d="M18.91 11.473c0.697-0.71 1.090-1.677 1.090-2.689s-0.394-1.979-1.091-2.689c-0.689-0.703-1.62-1.095-2.587-1.095s-1.897 0.393-2.587 1.095l-1.736 1.768-1.736-1.768c-1.433-1.46-3.741-1.46-5.174 0-1.454 1.481-1.454 3.897-0.031 5.347l6.941 6.765 6.91-6.734zM11.375 4.395l0.625 0.613 0.623-0.611c1.026-0.898 2.338-1.397 3.7-1.397 1.506 0 2.95 0.61 4.015 1.695s1.663 2.556 1.663 4.090-0.598 3.006-1.663 4.090l-8.337 8.125-8.337-8.125c-2.217-2.259-2.217-5.921 0-8.18 2.114-2.154 5.481-2.254 7.712-0.3z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-heart-solid" viewBox="0 0 24 24">
+ <title>aoc-heart-solid</title>
+ <path
+ d="M11.375 4.395l0.625 0.613 0.623-0.611c1.026-0.898 2.338-1.397 3.7-1.397 1.506 0 2.95 0.61 4.015 1.695s1.663 2.556 1.663 4.090-0.598 3.006-1.663 4.090l-8.337 8.125-8.337-8.125c-2.217-2.259-2.217-5.921 0-8.18 2.114-2.154 5.481-2.254 7.712-0.3z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-home" viewBox="0 0 24 24">
+ <title>aoc-home</title>
+ <path d="M15 22v-5c0-1.657-1.343-3-3-3s-3 1.343-3 3v5h-5v-11h-2v-1l9.995-8 10.005 8v1h-2v11h-5z"></path>
+ </symbol>
+ <symbol id="icon-aoc-important" viewBox="0 0 24 24">
+ <title>aoc-important</title>
+ <path
+ d="M21.775 18.469c0.641 1.125-0.164 2.531-1.444 2.531h-16.663c-1.283 0-2.083-1.408-1.444-2.531l8.331-14.626c0.641-1.125 2.247-1.123 2.887 0l8.331 14.626zM12 8c-0.552 0-1 0.448-1 1v5c0 0.552 0.448 1 1 1s1-0.448 1-1v-5c0-0.552-0.448-1-1-1zM12 19c0.552 0 1-0.448 1-1s-0.448-1-1-1c-0.552 0-1 0.448-1 1s0.448 1 1 1z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-knife-fork" viewBox="0 0 24 24">
+ <title>aoc-knife-fork</title>
+ <path
+ d="M9.25 2.75v4.625c0 0.345 0.28 0.625 0.625 0.625s0.625-0.28 0.625-0.625v-4.625c0-0.414 0.336-0.75 0.75-0.75s0.75 0.336 0.75 0.75v6.25c0 1.306-0.835 2.417-2 2.829v8.671c0 0.828-0.672 1.5-1.5 1.5s-1.5-0.672-1.5-1.5v-8.671c-1.165-0.412-2-1.523-2-2.829v-6.25c0-0.414 0.336-0.75 0.75-0.75s0.75 0.336 0.75 0.75v4.625c0 0.345 0.28 0.625 0.625 0.625s0.625-0.28 0.625-0.625v-4.625c0-0.414 0.336-0.75 0.75-0.75s0.75 0.336 0.75 0.75zM19 1v19.5c0 0.828-0.672 1.5-1.5 1.5s-1.5-0.672-1.5-1.5v-7.5h-1c-0.552 0-1-0.448-1-1v-6c0-2.761 2.239-5 5-5z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-library-books" viewBox="0 0 24 24">
+ <title>aoc-library-books</title>
+ <path
+ d="M4 6h-2v14c0 1.1 0.9 2 2 2h14v-2h-14v-14zM20 2h-12c-1.1 0-2 0.9-2 2v12c0 1.1 0.9 2 2 2h12c1.1 0 2-0.9 2-2v-12c0-1.1-0.9-2-2-2v0zM19 11h-10v-2h10v2zM15 15h-6v-2h6v2zM19 7h-10v-2h10v2z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-link" viewBox="0 0 24 24">
+ <title>aoc-link</title>
+ <path
+ d="M9.937 12.2c0.441-0.33 1.067-0.24 1.397 0.201 0.542 0.724 1.372 1.178 2.274 1.242s1.788-0.266 2.428-0.906l2.451-2.451c1.187-1.229 1.171-3.173-0.032-4.377-1.201-1.201-3.146-1.221-4.355-0.053l-1.421 1.413c-0.391 0.389-1.023 0.387-1.412-0.004s-0.387-1.023 0.004-1.412l1.431-1.423c2.002-1.934 5.192-1.904 7.164 0.067 1.975 1.975 1.998 5.165 0.044 7.187l-2.463 2.463c-1.049 1.049-2.502 1.591-3.982 1.485s-2.841-0.85-3.73-2.038c-0.33-0.441-0.24-1.067 0.201-1.397zM14.425 12.152c-0.441 0.33-1.067 0.24-1.397-0.201-0.542-0.724-1.372-1.178-2.274-1.242s-1.788 0.266-2.428 0.906l-2.451 2.451c-1.187 1.229-1.171 3.173 0.032 4.377 1.201 1.201 3.145 1.221 4.352 0.056l1.414-1.414c0.39-0.39 1.022-0.39 1.412 0s0.39 1.022-0 1.412l-1.426 1.426c-2.002 1.933-5.191 1.903-7.163-0.068-1.975-1.975-1.998-5.165-0.044-7.187l2.463-2.463c1.049-1.049 2.502-1.591 3.982-1.485s2.841 0.85 3.73 2.038c0.33 0.441 0.24 1.067-0.201 1.397z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-list-circle-bullets" viewBox="0 0 24 24">
+ <title>aoc-list-circle-bullets</title>
+ <path
+ d="M4 10.5c-0.83 0-1.5 0.67-1.5 1.5s0.67 1.5 1.5 1.5c0.83 0 1.5-0.67 1.5-1.5s-0.67-1.5-1.5-1.5v0zM4 4.5c-0.83 0-1.5 0.67-1.5 1.5s0.67 1.5 1.5 1.5c0.83 0 1.5-0.67 1.5-1.5s-0.67-1.5-1.5-1.5v0zM4 16.5c-0.835 0-1.5 0.677-1.5 1.5s0.677 1.5 1.5 1.5c0.823 0 1.5-0.677 1.5-1.5s-0.665-1.5-1.5-1.5v0zM7 19h14v-2h-14v2zM7 13h14v-2h-14v2zM7 5v2h14v-2h-14z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-list" viewBox="0 0 24 24">
+ <title>aoc-list</title>
+ <path d="M3 13h2v-2h-2v2zM3 17h2v-2h-2v2zM3 9h2v-2h-2v2zM7 13h14v-2h-14v2zM7 17h14v-2h-14v2zM7 7v2h14v-2h-14z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-location-add" viewBox="0 0 24 24">
+ <title>aoc-location-add</title>
+ <path
+ d="M12 2c-3.86 0-7 3.14-7 7 0 5.25 7 13 7 13s7-7.75 7-13c0-3.86-3.14-7-7-7v0zM16 10h-3v3h-2v-3h-3v-2h3v-3h2v3h3v2z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-location" viewBox="0 0 24 24">
+ <title>aoc-location</title>
+ <path
+ d="M12 2c-3.87 0-7 3.13-7 7 0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7v0zM12 11.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5c1.38 0 2.5 1.12 2.5 2.5s-1.12 2.5-2.5 2.5v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-mail" viewBox="0 0 24 24">
+ <title>aoc-mail</title>
+ <path
+ d="M20 4h-16c-1.1 0-1.99 0.9-1.99 2l-0.010 12c0 1.1 0.9 2 2 2h16c1.1 0 2-0.9 2-2v-12c0-1.1-0.9-2-2-2v0zM20 8l-8 5-8-5v-2l8 5 8-5v2z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-map" viewBox="0 0 24 24">
+ <title>aoc-map</title>
+ <path
+ d="M20.5 3l-0.16 0.030-5.34 2.070-6-2.1-5.64 1.9c-0.21 0.070-0.36 0.25-0.36 0.48v15.12c0 0.28 0.22 0.5 0.5 0.5l0.16-0.030 5.34-2.070 6 2.1 5.64-1.9c0.21-0.070 0.36-0.25 0.36-0.48v-15.12c0-0.28-0.22-0.5-0.5-0.5v0zM15 19l-6-2.11v-11.89l6 2.11v11.89z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-menu" viewBox="0 0 24 24">
+ <title>aoc-menu</title>
+ <path d="M3 5h18v2h-18v-2zM3 11h18v2h-18v-2zM3 17h18v2h-18v-2z"></path>
+ </symbol>
+ <symbol id="icon-aoc-more-horizontal" viewBox="0 0 24 24">
+ <title>aoc-more-horizontal</title>
+ <path
+ d="M6 10c-1.1 0-2 0.9-2 2s0.9 2 2 2c1.1 0 2-0.9 2-2s-0.9-2-2-2v0zM18 10c-1.1 0-2 0.9-2 2s0.9 2 2 2c1.1 0 2-0.9 2-2s-0.9-2-2-2v0zM12 10c-1.1 0-2 0.9-2 2s0.9 2 2 2c1.1 0 2-0.9 2-2s-0.9-2-2-2v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-my-location" viewBox="0 0 24 24">
+ <title>aoc-my-location</title>
+ <path
+ d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4c2.21 0 4-1.79 4-4s-1.79-4-4-4v0zM20.94 11c-0.46-4.17-3.77-7.48-7.94-7.94v-2.060h-2v2.060c-4.17 0.46-7.48 3.77-7.94 7.94h-2.060v2h2.060c0.46 4.17 3.77 7.48 7.94 7.94v2.060h2v-2.060c4.17-0.46 7.48-3.77 7.94-7.94h2.060v-2h-2.060zM12 19c-3.87 0-7-3.13-7-7s3.13-7 7-7c3.87 0 7 3.13 7 7s-3.13 7-7 7v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-near-me" viewBox="0 0 24 24">
+ <title>aoc-near-me</title>
+ <path d="M21 3l-18 7.53v0.98l6.84 2.65 2.64 6.84h0.98z"></path>
+ </symbol>
+ <symbol id="icon-aoc-notifications-alert" viewBox="0 0 24 24">
+ <title>aoc-notifications-alert</title>
+ <path
+ d="M18.988 10.589c0.008 0.136 0.012 0.273 0.012 0.411v5l2 1v2h-18v-2l2-1v-5c0-3.526 2.608-6.444 6-6.929v-1.071c0-0.552 0.448-1 1-1s1 0.447 1 1c-0.628 0.836-1 1.875-1 3 0 2.761 2.239 5 5 5 0.707 0 1.379-0.147 1.988-0.411zM10 20h4c0 1.105-0.895 2-2 2s-2-0.895-2-2zM17 9c-1.657 0-3-1.343-3-3s1.343-3 3-3c1.657 0 3 1.343 3 3s-1.343 3-3 3z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-notifications-mentions" viewBox="0 0 24 24">
+ <title>aoc-notifications-mentions</title>
+ <path
+ d="M15.52 15.487c-0.585 0.857-1.949 1.786-3.776 1.786-3.094 0-5.189-2.154-5.189-5.164 0-2.937 2.144-5.237 5.092-5.237 1.486 0 2.704 0.587 3.265 1.297v-1.126h2.12v6.534c0 1.126 0.39 1.835 1.535 1.835 1.34 0 2.388-1.786 2.388-4.601 0-4.943-3.411-7.978-8.771-7.978-5.677 0-9.039 4.185-9.039 9.201 0 5.555 3.898 9.103 9.623 9.103 2.68 0 4.848-1.003 6.383-2.349l1.121 1.37c-1.437 1.444-4.166 2.839-7.553 2.839-7.358 0-11.719-4.748-11.719-10.914 0-6.412 4.629-11.086 11.256-11.086 6.797 0 10.744 4.283 10.744 9.715 0 4.209-1.998 6.534-4.653 6.534-1.657 0-2.509-0.832-2.826-1.762zM8.75 12.011c0 1.747 1.19 2.989 2.929 2.989s3.071-1.149 3.071-2.989c0-1.839-1.357-3.011-3.095-3.011s-2.905 1.241-2.905 3.011z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-notifications-muted" viewBox="0 0 24 24">
+ <title>aoc-notifications-muted</title>
+ <path
+ d="M13 4.071c3.392 0.485 6 3.403 6 6.929v5l2 1v2h-18v-2l2-1v-5c0-3.526 2.608-6.444 6-6.929v-1.071c0-0.552 0.448-1 1-1s1 0.448 1 1v1.071zM10 20h4c0 1.105-0.895 2-2 2s-2-0.895-2-2zM13.407 11.993l2.828-2.828-1.414-1.414-2.828 2.828-2.828-2.828-1.414 1.414 2.828 2.828-2.828 2.828 1.414 1.414 2.828-2.828 2.828 2.828 1.414-1.414-2.828-2.828z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-notifications-tracking" viewBox="0 0 24 24">
+ <title>aoc-notifications-tracking</title>
+ <path
+ d="M19 9.9c0.323 0.066 0.658 0.1 1 0.1s0.677-0.034 1-0.1v10.1c0 1.105-0.895 2-2 2h-14c-1.105 0-2-0.895-2-2v-14c0-1.105 0.895-2 2-2h10.1c-0.066 0.323-0.1 0.658-0.1 1s0.034 0.677 0.1 1h-10.1v14h14v-10.1zM20 8c-1.657 0-3-1.343-3-3s1.343-3 3-3c1.657 0 3 1.343 3 3s-1.343 3-3 3z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-open-in-new" viewBox="0 0 24 24">
+ <title>aoc-open-in-new</title>
+ <path
+ d="M19 19h-14v-14h7v-2h-7c-1.11 0-2 0.9-2 2v14c0 1.1 0.89 2 2 2h14c1.1 0 2-0.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41 9.83-9.83v3.59h2v-7h-7z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-pencil" viewBox="0 0 24 24">
+ <title>aoc-pencil</title>
+ <path
+ d="M3 17.25v3.75h3.75l11.060-11.060-3.75-3.75-11.060 11.060zM20.71 7.040c0.39-0.39 0.39-1.020 0-1.41l-2.34-2.34c-0.39-0.39-1.020-0.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-person" viewBox="0 0 24 24">
+ <title>aoc-person</title>
+ <path
+ d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4c-2.21 0-4 1.79-4 4s1.79 4 4 4v0zM12 14c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-pinned" viewBox="0 0 24 24">
+ <title>aoc-pinned</title>
+ <path
+ d="M6.515 16.077l-4.223-4.223c1.774-1.774 4.266-2.391 6.541-1.853l5.516-4.667c-0.493-1.098-0.288-2.433 0.614-3.335l7.039 7.039c-0.902 0.902-2.236 1.106-3.335 0.614l-4.667 5.516c0.539 2.274-0.079 4.767-1.852 6.541l-4.223-4.223-4.223 4.223c-0.389 0.389-1.019 0.389-1.408 0s-0.389-1.019 0-1.408l4.223-4.223z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-plane-takeoff" viewBox="0 0 24 24">
+ <title>aoc-plane-takeoff</title>
+ <path
+ d="M2.5 19h19v2h-19v-2zM22.070 9.64c-0.21-0.8-1.040-1.28-1.84-1.060l-5.31 1.42-6.9-6.43-1.93 0.51 4.14 7.17-4.97 1.33-1.97-1.54-1.45 0.39 1.82 3.16 0.77 1.33 1.6-0.43 14.97-4c0.81-0.23 1.28-1.050 1.070-1.85v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-plane" viewBox="0 0 24 24">
+ <title>aoc-plane</title>
+ <path
+ d="M21 16v-2l-8-5v-5.5c0-0.83-0.67-1.5-1.5-1.5s-1.5 0.67-1.5 1.5v5.5l-8 5v2l8-2.5v5.5l-2 1.5v1.5l3.5-1 3.5 1v-1.5l-2-1.5v-5.5l8 2.5z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-print" viewBox="0 0 24 24">
+ <title>aoc-print</title>
+ <path
+ d="M19 8h-14c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3v0zM16 19h-8v-5h8v5zM19 12c-0.55 0-1-0.45-1-1s0.45-1 1-1c0.55 0 1 0.45 1 1s-0.45 1-1 1v0zM18 3h-12v4h12v-4z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-reply" viewBox="0 0 24 24">
+ <title>aoc-reply</title>
+ <path d="M10 9v-4l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11v0z"></path>
+ </symbol>
+ <symbol id="icon-aoc-search" viewBox="0 0 24 24">
+ <title>aoc-search</title>
+ <path
+ d="M15.5 14h-0.79l-0.28-0.27c0.98-1.14 1.57-2.62 1.57-4.23 0-3.59-2.91-6.5-6.5-6.5s-6.5 2.91-6.5 6.5c0 3.59 2.91 6.5 6.5 6.5 1.61 0 3.090-0.59 4.23-1.57l0.27 0.28v0.79l5 4.99 1.49-1.49-4.99-5zM9.5 14c-2.49 0-4.5-2.010-4.5-4.5s2.010-4.5 4.5-4.5c2.49 0 4.5 2.010 4.5 4.5s-2.010 4.5-4.5 4.5v0z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-shuffle" viewBox="0 0 24 24">
+ <title>aoc-shuffle</title>
+ <path
+ d="M10.59 9.17l-5.18-5.17-1.41 1.41 5.17 5.17 1.42-1.41zM14.5 4l2.040 2.040-12.54 12.55 1.41 1.41 12.55-12.54 2.040 2.040v-5.5h-5.5zM14.83 13.41l-1.41 1.41 3.13 3.13-2.050 2.050h5.5v-5.5l-2.040 2.040-3.13-3.13z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-star" viewBox="0 0 24 24">
+ <title>aoc-star</title>
+ <path
+ d="M18.18 21l-1.635-7.029 5.455-4.727-7.191-0.617-2.809-6.627-2.809 6.627-7.191 0.617 5.455 4.727-1.635 7.029 6.18-3.728z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-subject" viewBox="0 0 24 24">
+ <title>aoc-subject</title>
+ <path d="M14 17h-10v2h10v-2zM20 9h-16v2h16v-2zM4 15h16v-2h-16v2zM4 5v2h16v-2h-16z"></path>
+ </symbol>
+ <symbol id="icon-aoc-trip-style" viewBox="0 0 24 24">
+ <title>aoc-trip-style</title>
+ <path
+ d="M20 6c1.11 0 2 0.89 2 2v11c0 1.11-0.89 2-2 2h-16c-1.11 0-2-0.89-2-2l0.010-11c0-1.11 0.88-2 1.99-2h4v-2c0-1.11 0.89-2 2-2h4c1.11 0 2 0.89 2 2v2h4zM14 6v-2h-4v2h4zM6.5 14c0.828 0 1.5-0.672 1.5-1.5s-0.672-1.5-1.5-1.5c-0.828 0-1.5 0.672-1.5 1.5s0.672 1.5 1.5 1.5zM6 16.042l0.347 1.97 5.909-1.042-0.347-1.97-5.909 1.042z">
+ </path>
+ </symbol>
+ <symbol id="icon-aoc-unpinned" viewBox="0 0 24 24">
+ <title>aoc-unpinned</title>
+ <path
+ d="M5.416 12.15l6.433 6.433c0.362-0.93 0.439-1.959 0.203-2.955l-0.233-0.982 5.94-7.020-1.386-1.386-7.020 5.94-0.982-0.233c-0.996-0.236-2.025-0.16-2.955 0.203zM6.515 16.077l-4.223-4.223c1.774-1.774 4.266-2.391 6.541-1.853l5.516-4.667c-0.493-1.098-0.288-2.433 0.614-3.335l7.039 7.039c-0.902 0.902-2.236 1.106-3.335 0.614l-4.667 5.516c0.539 2.274-0.079 4.767-1.852 6.541l-4.223-4.223-4.223 4.223c-0.389 0.389-1.019 0.389-1.408 0s-0.389-1.019 0-1.408l4.223-4.223z">
+ </path>
+ </symbol>
+ </defs>
+</svg>
+
+</html> \ No newline at end of file
diff --git a/test/fixtures/mastodon-post-activity.json b/test/fixtures/mastodon-post-activity.json
index b91263431..5c3d22722 100644
--- a/test/fixtures/mastodon-post-activity.json
+++ b/test/fixtures/mastodon-post-activity.json
@@ -35,6 +35,19 @@
"inReplyTo": null,
"inReplyToAtomUri": null,
"published": "2018-02-12T14:08:20Z",
+ "replies": {
+ "id": "http://mastodon.example.org/users/admin/statuses/99512778738411822/replies",
+ "type": "Collection",
+ "first": {
+ "type": "CollectionPage",
+ "next": "http://mastodon.example.org/users/admin/statuses/99512778738411822/replies?min_id=99512778738411824&page=true",
+ "partOf": "http://mastodon.example.org/users/admin/statuses/99512778738411822/replies",
+ "items": [
+ "http://mastodon.example.org/users/admin/statuses/99512778738411823",
+ "http://mastodon.example.org/users/admin/statuses/99512778738411824"
+ ]
+ }
+ },
"sensitive": true,
"summary": "cw",
"tag": [
diff --git a/test/fixtures/modules/runtime_module.ex b/test/fixtures/modules/runtime_module.ex
new file mode 100644
index 000000000..f11032b57
--- /dev/null
+++ b/test/fixtures/modules/runtime_module.ex
@@ -0,0 +1,9 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule RuntimeModule do
+ @moduledoc """
+ This is a dummy module to test custom runtime modules.
+ """
+end
diff --git a/test/fixtures/nypd-facial-recognition-children-teenagers4.html b/test/fixtures/nypd-facial-recognition-children-teenagers4.html
new file mode 100644
index 000000000..9f15cc42e
--- /dev/null
+++ b/test/fixtures/nypd-facial-recognition-children-teenagers4.html
@@ -0,0 +1,228 @@
+<!DOCTYPE html>
+<html lang="en" itemId="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" itemType="http://schema.org/NewsArticle" itemScope="" class="story" xmlns:og="http://opengraphprotocol.org/schema/">
+ <head>
+ <title data-rh="true">She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times</title>
+ <title data-rh="true">She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times</title>
+ <meta data-rh="true" itemprop="inLanguage" content="en-US"/><meta data-rh="true" property="article:published" itemprop="datePublished dateCreated" content="2019-08-01T17:15:31.000Z"/><meta data-rh="true" property="article:modified" itemprop="dateModified" content="2019-08-02T09:30:23.000Z"/><meta data-rh="true" http-equiv="Content-Language" content="en"/><meta data-rh="true" name="robots" content="noarchive"/><meta data-rh="true" name="articleid" itemprop="identifier" content="100000006583622"/><meta data-rh="true" name="nyt_uri" itemprop="identifier" content="nyt://article/9da58246-2495-505f-9abd-b5fda8e67b56"/><meta data-rh="true" name="pubp_event_id" itemprop="identifier" content="pubp://event/47a657bafa8a476bb36832f90ee5ac6e"/><meta data-rh="true" name="description" itemprop="description" content="With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers."/><meta data-rh="true" name="image" itemprop="image" content="https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg"/><meta data-rh="true" name="byl" content="By Joseph Goldstein and Ali Watkins"/><meta data-rh="true" name="thumbnail" itemprop="thumbnailUrl" content="https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-thumbStandard.jpg"/><meta data-rh="true" name="news_keywords" content="NYPD,Juvenile delinquency,Facial Recognition,Privacy,Government Surveillance,Police,Civil Rights,NYC"/><meta data-rh="true" name="pdate" content="20190801"/><meta data-rh="true" property="og:url" content="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" property="og:type" content="article"/><meta data-rh="true" property="og:title" content="She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database."/><meta data-rh="true" property="og:image" content="https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg"/><meta data-rh="true" property="og:description" content="With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers."/><meta data-rh="true" property="article:section" itemprop="articleSection" content="New York"/><meta data-rh="true" property="article:tag" content="Police Department (NYC)"/><meta data-rh="true" property="article:tag" content="Juvenile Delinquency"/><meta data-rh="true" property="article:tag" content="Facial Recognition Software"/><meta data-rh="true" property="article:tag" content="Privacy"/><meta data-rh="true" property="article:tag" content="Surveillance of Citizens by Government"/><meta data-rh="true" property="article:tag" content="Police"/><meta data-rh="true" property="article:tag" content="Civil Rights and Liberties"/><meta data-rh="true" property="article:tag" content="New York City"/><meta data-rh="true" name="CG" content="nyregion"/><meta data-rh="true" name="SCG" content=""/><meta data-rh="true" name="CN" content="experience-tech-and-society"/><meta data-rh="true" name="CT" content="spotlight"/><meta data-rh="true" name="PT" content="article"/><meta data-rh="true" name="PST" content="News"/><meta data-rh="true" name="url" itemprop="url" content="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" name="msapplication-starturl" content="https://www.nytimes.com"/><meta data-rh="true" property="al:android:url" content="nytimes://reader/id/100000006583622"/><meta data-rh="true" property="al:android:package" content="com.nytimes.android"/><meta data-rh="true" property="al:android:app_name" content="NYTimes"/><meta data-rh="true" name="twitter:app:name:googleplay" content="NYTimes"/><meta data-rh="true" name="twitter:app:id:googleplay" content="com.nytimes.android"/><meta data-rh="true" name="twitter:app:url:googleplay" content="nytimes://reader/id/100000006583622"/><meta data-rh="true" property="al:iphone:url" content="nytimes://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" property="al:iphone:app_store_id" content="284862083"/><meta data-rh="true" property="al:iphone:app_name" content="NYTimes"/><meta data-rh="true" property="al:ipad:url" content="nytimes://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" property="al:ipad:app_store_id" content="357066198"/><meta data-rh="true" property="al:ipad:app_name" content="NYTimes"/>
+ <meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge" />
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
+<meta property="fb:app_id" content="9869919170" />
+<meta name="twitter:site" value="@nytimes" />
+
+
+ <script type="text/javascript">
+ // 20.585kB
+ window.viHeadScriptSize = 20.585;
+ (function () { var _f=function(e){window.vi=window.vi||{},window.vi.env=Object.freeze(e)};;_f.apply(null, [{"JKIDD_PATH":"https://a.nytimes.com/svc/nyt/data-layer","ET2_URL":"https://a.et.nytimes.com","WEDDINGS_PATH":"https://content.api.nytimes.com","GDPR_PATH":"https://us-central1-nyt-wfvi-prd.cloudfunctions.net/gdpr-email-form","RECAPTCHA_SITEKEY":"6LevSGcUAAAAAF-7fVZF05VTRiXvBDAY4vBSPaTF","ABRA_ET_URL":"//et.nytimes.com","NODE_ENV":"production","SENTRY_SAMPLE_RATE":"10","EXPERIMENTAL_ROUTE_PREFIX":"","ENVIRONMENT":"prd","RELEASE":"034494769d779a637c178f47c2096df69b7c07a4","AUTH_HOST":"https://myaccount.nytimes.com","SWG_PUBLICATION_ID":"nytimes.com","GQL_FETCH_TIMEOUT":"4000"}]); })();;
+ !function(){if('PerformanceLongTaskTiming' in window){var g=window.__tti={e:[]};
+ g.o=new PerformanceObserver(function(l){g.e=g.e.concat(l.getEntries())});
+ g.o.observe({entryTypes:['longtask']})}}();
+;
+ !function(n,e){var t,o,i,c=[],f={passive:!0,capture:!0},r=new Date,a="pointerup",u="pointercancel";function p(n,c){t||(t=c,o=n,i=new Date,w(e),s())}function s(){o>=0&&o<i-r&&(c.forEach(function(n){n(o,t)}),c=[])}function l(t){if(t.cancelable){var o=(t.timeStamp>1e12?new Date:performance.now())-t.timeStamp;"pointerdown"==t.type?function(t,o){function i(){p(t,o),r()}function c(){r()}function r(){e(a,i,f),e(u,c,f)}n(a,i,f),n(u,c,f)}(o,t):p(o,t)}}function w(n){["click","mousedown","keydown","touchstart","pointerdown"].forEach(function(e){n(e,l,f)})}w(n),self.perfMetrics=self.perfMetrics||{},self.perfMetrics.onFirstInputDelay=function(n){c.push(n),s()}}(addEventListener,removeEventListener);
+;try {
+ var observer = new window.PerformanceObserver(function (list) {
+ var entries = list.getEntries();
+
+ for (var i = 0; i < entries.length; i += 1) {
+ var entry = entries[i];
+ var performance = {};
+
+ performance[entry.name] = Math.round(entry.startTime + entry.duration);
+ (window.dataLayer = window.dataLayer || []).push({
+ event: "performance",
+ pageview: {
+ performance: performance
+ }
+ });
+ }
+ });
+ observer.observe({
+ entryTypes: ["paint"]
+ });
+} catch (e) {};
+!function(i,e){var a,s,c,p,u,g=[],
+l="object"==typeof i.navigator&&"string"==typeof i.navigator.userAgent&&/iP(ad|hone|od)/.test(
+i.navigator.userAgent),f="object"==typeof i.navigator&&i.navigator.sendBeacon,
+y=f?l?"xhr_ios":"beacon":"xhr";function d(){var e,t,n=i.crypto||i.msCrypto;if(n)t=n.getRandomValues(
+new Uint8Array(18));else for(t=[];t.length<18;)t.push(256*Math.random()^255&(e=e||+new Date)),
+e=Math.floor(e/256);return btoa(String.fromCharCode.apply(String,t)).replace(/\+/g,"-").replace(
+/\//g,"_")}if(i.nyt_et)try{console.warn("et2 snippet should only load once per page")}catch(e
+){}else i.nyt_et=function(){var e,t,n,o=arguments;function r(r){g.length&&(function(e,t,n){if(
+"beacon"===y||f&&r)return i.navigator.sendBeacon(e,t)
+;var o="undefined"!=typeof XMLHttpRequest?new XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP")
+;o.open("POST",e),o.withCredentials=!0,o.setRequestHeader("Accept","*/*"),
+"string"==typeof t?o.setRequestHeader("Content-Type","text/plain;charset=UTF-8"
+):"[object Blob]"==={}.toString.call(t)&&t.type&&o.setRequestHeader("Content-Type",t.type);try{
+o.send(t)}catch(e){}}(a+"/track",JSON.stringify(g)),g.length=0,clearTimeout(u),u=null)}if(
+"string"==typeof o[0]&&/init/.test(o[0])&&(c=d(),"init"==o[0]&&!s)){if(s=d(),
+"string"!=typeof o[1]||!/^http/.test(o[1]))throw new Error("init must include an et host url")
+;a=String(o[1]).replace(/\/$/,""),"string"==typeof o[2]&&(p=o[2])}n="page_exit"==(e=o[o.length-1]
+).subject||"ob_click"==(e.eventData||{}).type,a&&"object"==typeof e&&(t="page"==e.subject?c:d(),
+e.sourceApp&&(p=e.sourceApp),e.sourceApp=p,g.push({context_id:s,pageview_id:c,event_id:t,
+client_lib:"v1.0.5",sourceApp:p,how:n&&l&&f?"beacon_ios":y,client_ts:+new Date,data:JSON.parse(
+JSON.stringify(e))}),"send"==o[0]||t==c||n?r(n):u||(u=setTimeout(r,5500)))},
+i.nyt_et.get_pageview_id=function(){return c}}(window);
+;
+var NYTD=NYTD||{};NYTD.Abra=function(t){"use strict";function e(t){var e=r[t];return e&&e[1]||null}function n(t,e){if(t){var n,r,o=e[0],i=e[1],c=0,u=0;if(1!==i.length||4294967296!==i[0])for(n=a(t+" "+o)>>>0,c=0,u=0;r=i[c++];)if(n<(u+=r[0]))return r}}function a(t){for(var e,n,a,r,o,i,c,u=0,h=0,l=[],s=[e=1732584193,n=4023233417,~e,~n,3285377520],f=[],p=t.length;h<=p;)f[h>>2]|=(h<p?t.charCodeAt(h):128)<<8*(3-h++%4);for(f[c=p+8>>2|15]=p<<3;u<=c;u+=16){for(e=s,h=0;h<80;e=[0|[(i=((t=e[0])<<5|t>>>27)+e[4]+(l[h]=h<16?~~f[u+h]:i<<1|i>>>31)+1518500249)+((n=e[1])&(a=e[2])|~n&(r=e[3])),o=i+(n^a^r)+341275144,i+(n&a|n&r|a&r)+882459459,o+1535694389][0|h++/20],t,n<<30|n>>>2,a,r])i=l[h-3]^l[h-8]^l[h-14]^l[h-16];for(h=5;h;)s[--h]=s[h]+e[h]|0}return s[0]}var r,o={};return t.dataLayer=t.dataLayer||[],e.init=function(e){var a,o,i,c,u,h,l,s,f,p,d=[],v=[],m=(t.document.cookie.match(/(?:^|;) *nyt-a=([^;]*)/)||[])[1],b=(t.document.cookie.match(/(?:^|;) *ab7=([^;]*)/)||[])[1],g=(t.location.search.match(/(?:^\?|&)abra=([^&]*)/)||[])[1];if(r)throw new Error("can't init twice");for(r={},u=(decodeURIComponent(b||"")+"&"+decodeURIComponent(g||"")).split("&"),a=u.length-1;a>=0;a--)h=u[a].split("="),h.length<2||(l=h[0])&&!r[l]&&(s=h[1]||null,r[l]=[,s,1],s&&d.push(l+"="+s),v.push({test:l,variant:s||"0"}));for(a=0;a<e.length;a++)i=e[a],(o=i[0])in r||(c=n(m,i)||[],c[0],f=c[1],p=!!c[2],r[o]=c,f&&d.push(o.replace(/[^\w-]/g)+"="+(""+f).replace(/[^\w-]/g)),p&&v.push({test:o,variant:f||"0"}));d.length&&t.document.documentElement.setAttribute("data-nyt-ab",d.join(" ")),v.length&&t.dataLayer.push({event:"ab-alloc",abtest:{batch:v}})},e.reportExposure=function(e,n){if(!o[e]){o[e]=1;var a=r[e];if(a){var i=a[1];a[2]&&t.dataLayer.push({event:"ab-expose",abtest:{test:e,variant:i||"0"}})}n&&t.setTimeout(function(){n(null)},0)}},e}(this);
+;(function () { var NYTD=window.NYTD||{};function setupTimeZone(){var e='[data-timezone][data-timezone~="'+(new Date).getHours()+'"] { display: block }',t=document.createElement("style");t.innerHTML=e,document.head.appendChild(t)}function addNYTAppClass(){var e=window.navigator.userAgent||window.navigator.vendor||window.opera,t=-1!==e.indexOf("nyt_android"),n=-1!==e.indexOf("nytios");(t||n)&&document.documentElement.classList.add("NYTApp")}function setupPageViewId(){NYTD.PageViewId={},NYTD.PageViewId.update=function(){return"undefined"!=typeof nyt_et&&"function"==typeof window.nyt_et.get_pageview_id?(window.nyt_et("pageinit"),NYTD.PageViewId.current=window.nyt_et.get_pageview_id()):NYTD.PageViewId.current="xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){var t=16*Math.random()|0;return("x"===e?t:3&t|8).toString(16)}),NYTD.PageViewId.current}}var _f=function(e){try{document.domain="nytimes.com"}catch(e){}window.swgUserInfoXhrObject=new XMLHttpRequest,window.__emotion=e.emotionIds,setupPageViewId(),setupTimeZone(),addNYTAppClass(),window.nyt_et("init",vi.env.ET2_URL,"nyt-vi",{subject:"page",canonicalUrl:(document.querySelector("link[rel=canonical]")||{}).href,articleId:(document.querySelector("meta[name=articleid]")||{}).content,nyt_uri:(document.querySelector("meta[name=nyt_uri]")||{}).content,pubpEventId:(document.querySelector("meta[name=pubp_event_id]")||{}).content,url:location.href,referrer:document.referrer||void 0,client_tz_offset:(new Date).getTimezoneOffset()}),"undefined"!=typeof nyt_et&&"function"==typeof window.nyt_et.get_pageview_id?NYTD.PageViewId.current=window.nyt_et.get_pageview_id():NYTD.PageViewId.update(),NYTD.Abra.init(e.abraConfig,vi.env.ABRA_ET_URL)};;_f.apply(null, [{"emotionIds":["0","1dv1kvn","v89234","nuvmzp","1gz70xg","9e9ivx","2bwtzy","1hyfx7x","6n7j50","1kj7lfb","10m9xeu","vz7hjd","1fe7a5q","1rn5q1r","10488qs","1iruc8t","1ropbjl","uw59u","jxzr5i","oylsik","1otr2jl","1c8n994","qtw155","v0l3hm","g4gku8","1rr4qq7","6xhk3s","rxqrcl","tj0ten","ist4u3","1gprdgz","10t7hia","mzqdl","kwpx34","1k2cjfc","1vhk1ks","6td9kr","r5ic95","15uy5yv","1p8nkc0","5j8bii","1am0aiv","1g7m0tk","d8bdto","y8aj3r","60hakz","i29ckm","acwcvw","1baulvz","f8wsfj","mhvv8m","m6999o","i9gxme","1m9j9gf","1sy8kpn","19vbshk","l9onyx","79elbk","1q1yk17","g7rb99","k008qs","bsn42l","11cwn6f","ghw4n2","1c5cfvc","htgkrt","e64et","9zaqp9","16fq4rz","1kjk1j2","88g286","12yx39b","4hu8jm","1wqz2f4","yl3z84","1q3gjvc","nc39ev","amd09y","ru1vxe","ajnadh","1ri25x2","12fr9lp","1hfdzay","4g4cvq","m6xlts","1ahhg7f","fwqvlz","17xtcya","x15j1o","1705lsu","1iwv8en","b7n1on","1b9egsl","1rj8to8","4w91ra","wg1cha","1ubp8k9","1egl8em","vdv0al","1i2y565","o6xoe7","1fanzo5","53u6y8","1m50asq","z3e15g","uwwqev","1ly73wi","7y3qfv","l72opv","4skfbu","1fcn4th","13zu7ev","f7l8cz","16ogagc","17ai7jg","8i9d0s","1nwzsjy","10698na","nhjhh0","1nuro5j","1w5cs23","4brsb6","uhuo44","exrw3m","1a48zt4","1xdhyk6","vuqh7u","1l44abu","jcw7oy","10raysz","ar1l6a","1ede5it","mn5hq9","1qmnftd","1ho5u4o","13o0c9t","1yo489b","ulr03x","1bymuyk","1waixk9","1f7ibof","l2ztic","19lv58h","mgtjo2","1wr3we4","y3sf94","1bnxwmn","1i8g3m4","3qijnq","uqyvli","1uqjmks","1bvtpon","1vxca1d","1vkm6nb","1ox9jel","1riqqik","2fg4z9","11n4cex","1ifw933","1rjmmt7","rqb9bm","19hdyf3","15g2oxy","2b3w4o","14b9hti","1j8dw05","1vm5oi9","32rbo2","llk6mt","1s4ffep","pdw9fk","1txwxcy","1soubk3"],"abraConfig":[["vi-ads-et",[[257698038,"2_remainder",1],[4037269258,null,0]]],["messaging-optimizely",[[4294967296,"1",0]]],["dfp_adslot4v2",[[4294967296,"1_external",1]]],["DFP_als",[[4294967296,"1_als",1]]],["DFP_als_home",[[214748365,"1_als",1],[214748365,"1_als",1],[429496730,"1_als",1],[429496729,"1_als",1],[858993459,"1_als",1],[1073741824,"1_als",1],[1073741824,null,0]]],["medianet_toggle",[[4294967296,"0_default",0]]],["amazon_toggle",[[4294967296,null,0]]],["index_toggle",[[4294967296,"1_block",0]]],["dfp_home_toggle",[[4294967296,null,0]]],["dfp_story_toggle",[[4294967296,null,0]]],["dfp_interactive_toggle",[[4294967296,null,0]]],["FREEX_Best_In_Show",[[2147483648,"0_Control",1],[2147483648,"1_Best",1]]],["MKT_dfp_ocean_language",[[2147483648,"0_control",1],[2147483648,"1_language",1]]],["MC_magnolia_0519",[[4294967296,"1_magnolia",1]]],["STORY_topical_recirc",[[2147483648,"0_control",1],[2147483648,"1_variant",1]]],["HOME_timesExclusive",[[2147483648,"0_control",1],[2147483648,"1_variant",1]]],["ON_daily_digest_NL_0719",[[644245095,"0_control",1],[644245094,"1_daily_digest",1],[3006477107,null,0]]],["HOME_discovery_automation",[[2147483648,"0_control",1],[2147483648,"1_automation",1]]],["MKT_GateDockMsgTap",[[1431655766,"0_control",1],[1431655765,"2_BAUDockTapGate",1],[1431655765,"4_BAUDockRBGate",1]]],["FREEX_RegiWall_Messaging",[[214748365,"0_Control",1],[214748365,"1_Continue_Reading",1],[214748365,"2_For_Free",1],[214748365,"3_Keep_Reading",1],[214748364,"4_Continue_Reading_NoHeader",1],[214748365,"5_For_Free_NoHeader",1],[214748365,"6_Keep_Reading_NoHeader",1],[858993459,"0_Control",1],[858993459,"6_Keep_Reading_NoHeader",1],[1073741824,null,0]]],["MC_briefing_bar_anon_test_0519",[[1431655766,"0_control",1],[1431655765,"1_subscribe",1],[1431655765,"2_regi",1]]],["MC_briefing_bar_regi_test_0519",[[2147483648,"0_control",1],[2147483648,"1_subscribe",1]]],["SEARCH_FACET_DROPDOWN",[[2147483648,"0_FACET_MULTI_SELECT",1],[2147483648,"1_DYNAMIC_FACET_SELECT",1]]],["VG_gift_upsell_x_only",[[429496730,"0_control",1],[3865470566,"1_upsell",1]]],["ON_allocator_0719",[[356482286,"ON_login_interrupt_0819-0_control",0],[356482286,"ON_login_interrupt_0819-1_app_experience",0],[356482285,"ON_login_interrupt_0819-2_login_value",0],[356482286,"ON_login_interrupt_0819-3_login_return",0],[356482285,"ON_app_dl_getstarted_0819-0_control",0],[356482286,"ON_app_dl_getstarted_0819-1_appExperience",0],[356482285,"ON_app_dl_getstarted_0819-2_bestApp",0],[356482286,"ON_app_dl_getstarted_0819-3_magicLink",0],[236223201,"ON_app_dl_mc4-6_0819-0_control",0],[236223202,"ON_app_dl_mc4-6_0819-1_dockTrunc",0],[236223201,"ON_app_dl_mc4-6_0819-2_newDock",0],[236223201,"ON_app_dl_mc4-6_0819-3_stdNew",0],[236223201,"ON_app_dl_mc4-6_0819-4_stdDockTrunc",0],[236223202,"ON_app_dl_mc4-6_0819-5_truncator",0],[25769803,null,0]]],["MKT_dfp_ocean_bundle_light",[[1431655766,"0_control",1],[1431655765,"1_design",1],[1431655765,"2_design_light",1]]],["MKT_dfp_ocean_bundle_family",[[1431655766,"0_control",1],[1431655765,"1_design",1],[1431655765,"2_family",1]]],["HL_sample",[[2147483648,"0",1],[2147483648,"1",1]]],["HL_100000006614214",[[2147483648,"0",1],[2147483648,"1",1]]],["HL_100000006641840",[[2147483648,"0",1],[2147483648,"1",1]]]]}]); })();;(function () { var _f=function(e){var r=function(){var r=e.url;try{r+=window.location.search.slice(1).split("&").reduce(function(e,r){return"ip-override"===r.split("=")[0]?"?"+r:e},"")}catch(e){console.warn(e)}var n=new XMLHttpRequest;for(var t in n.withCredentials=!0,n.open("POST",r,!0),n.setRequestHeader("Content-Type","application/json"),e.headers)n.setRequestHeader(t,e.headers[t]);return n.send(e.body),n};window.userXhrObject=r(),window.userXhrRefresh=function(){return window.userXhrObject=r(),window.userXhrObject}};;_f.apply(null, [{"url":"https://samizdat-graphql.nytimes.com/graphql/v2","body":"{\"operationName\":\"UserQuery\",\"variables\":{},\"query\":\" query UserQuery { user { __typename profile { displayName } userInfo { regiId entitlements demographics { emailSubscriptions wat bundleSubscriptions { bundle inGrace promotion source } } } subscriptionDetails { graceStartDate graceEndDate isFreeTrial hasQueuedSub startDate endDate status entitlements } } } \"}","headers":{"nyt-app-type":"project-vi","nyt-app-version":"0.0.5","nyt-token":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs+/oUCTBmD/cLdmcecrnBMHiU/pxQCn2DDyaPKUOXxi4p0uUSZQzsuq1pJ1m5z1i0YGPd1U1OeGHAChWtqoxC7bFMCXcwnE1oyui9G1uobgpm1GdhtwkR7ta7akVTcsF8zxiXx7DNXIPd2nIJFH83rmkZueKrC4JVaNzjvD+Z03piLn5bHWU6+w+rA+kyJtGgZNTXKyPh6EC6o5N+rknNMG5+CdTq35p8f99WjFawSvYgP9V64kgckbTbtdJ6YhVP58TnuYgr12urtwnIqWP9KSJ1e5vmgf3tunMqWNm6+AnsqNj8mCLdCuc5cEB74CwUeQcP2HQQmbCddBy2y0mEwIDAQAB"}}]); })();;100*Math.random()<=vi.env.SENTRY_SAMPLE_RATE?(window.INSTALL_RAVEN=!0,window.nyt_errors={ravenInstalled:!1,list:[],tags:[]},window.onerror=function(n,r,o,w,i){if(!window.nyt_errors.ravenInstalled){var t={err:i,data:{}};window.nyt_errors.list.push(t)}}):window.INSTALL_RAVEN=!1;;(function () { var _f=function(t,e,n){var a=window,A=document,o=function(t){var e=A.createElement("style");e.appendChild(A.createTextNode(t)),A.querySelector("head").appendChild(e)},r=function(t,e,n,a,A){var r=new XMLHttpRequest;r.open("GET",t,!0),r.onreadystatechange=function(){if(4===r.readyState&&200===r.status){o(r.responseText);try{localStorage.setItem("nyt-fontFormat",e),localStorage.setItem(a,n)}catch(t){return}localStorage.setItem(A,r.responseText)}return!0},r.send(null)},c=function(e,n){var A;try{A=localStorage.getItem("nyt-fontFormat")}catch(t){}A||(A=function(){if(!("FontFace"in a))return!1;var t=new FontFace("t",'url("data:application/font-woff2;base64,d09GMgABAAAAAADcAAoAAAAAAggAAACWAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABk4ALAoUNAE2AiQDCAsGAAQgBSAHIBtvAcieB3aD8wURQ+TZazbRE9HvF5vde4KCYGhiCgq/NKPF0i6UIsZynbP+Xi9Ng+XLbNlmNz/xIBBqq61FIQRJhC/+QA/08PJQJ3sK5TZFMlWzC/iK5GUN40psgqvxwBjBOg6JUSJ7ewyKE2AAaXZrfUB4v+hze37ugJ9d+DeYqiDwVgCawviwVFGnuttkLqIMGivmDg") format("woff2")',{});return t.load().catch(function(){}),"loading"==t.status||"loaded"==t.status}()?"woff2":"woff");for(var c=0;c<e.length;c++){var i=e[c],l="shared"!==i?"-"+i:"",d="nyt-fontHash"+l,s="nyt-fontFace"+l,f=t[i][A],u=localStorage.getItem(d),g=localStorage.getItem(s);if(u===f.hash&&g)o(g);else{var h=function(t,e,n,a,A){return function(){r(t,e,n,a,A)}}(f.url,A,f.hash,d,s);n?h():document.addEventListener("DOMContentLoaded",h)}}};c(e),window.addEventListener("load",function(){c(n,!0)})};;_f.apply(null, [{"shared":{"woff":{"hash":"f2adc73415c5bbb437e993c14559e70e","url":"/vi-assets/static-assets/shared-woff.fonts-f2adc73415c5bbb437e993c14559e70e.css"},"woff2":{"hash":"22b34a6a6fd840943496b658184afdd3","url":"/vi-assets/static-assets/shared-woff2.fonts-22b34a6a6fd840943496b658184afdd3.css"}},"story":{"woff":{"hash":"3c668927c32fbefb440b4024d5da6351","url":"/vi-assets/static-assets/story-woff.fonts-3c668927c32fbefb440b4024d5da6351.css"},"woff2":{"hash":"acec1a902e1795b20a0204af82726cd2","url":"/vi-assets/static-assets/story-woff2.fonts-acec1a902e1795b20a0204af82726cd2.css"}},"opinion":{"woff":{"hash":"dfc5106c9c0aaa76688687e664474b04","url":"/vi-assets/static-assets/opinion-woff.fonts-dfc5106c9c0aaa76688687e664474b04.css"},"woff2":{"hash":"e2b27ff317927dfd77bdd429409627e0","url":"/vi-assets/static-assets/opinion-woff2.fonts-e2b27ff317927dfd77bdd429409627e0.css"}},"tmag":{"woff":{"hash":"4634f3c7ddebb9113b69d4578d9a0ba0","url":"/vi-assets/static-assets/tmag-woff.fonts-4634f3c7ddebb9113b69d4578d9a0ba0.css"},"woff2":{"hash":"8622c93c260fa93b229b7249df708fb1","url":"/vi-assets/static-assets/tmag-woff2.fonts-8622c93c260fa93b229b7249df708fb1.css"}},"mag":{"woff":{"hash":"109e6d301ed49c8078086b5892696adf","url":"/vi-assets/static-assets/mag-woff.fonts-109e6d301ed49c8078086b5892696adf.css"},"woff2":{"hash":"fb42c728dc70cc4ef6010a60cb10b0bd","url":"/vi-assets/static-assets/mag-woff2.fonts-fb42c728dc70cc4ef6010a60cb10b0bd.css"}},"well":{"woff":{"hash":"f0e613b89006e99b4622d88aa5563a81","url":"/vi-assets/static-assets/well-woff.fonts-f0e613b89006e99b4622d88aa5563a81.css"},"woff2":{"hash":"77806b85de524283fe742b916c9d0ee4","url":"/vi-assets/static-assets/well-woff2.fonts-77806b85de524283fe742b916c9d0ee4.css"}}},["shared","story"],["opinion","tmag","mag","well"]]); })();;(function () { function swgDataLayer(e){return!!window.dataLayer&&((window.dataLayer=window.dataLayer||[]).push({event:"impression",module:e}),!0)}function checkSwgOptOut(){if(!window.localStorage)return!1;var e=window.localStorage.getItem("nyt-swgOptOut");if(!e)return!1;var t=parseInt(e,10);return((new Date).getTime()-t)/864e5<1||(window.localStorage.removeItem("nyt-swgOptOut"),!1)}function swgDeferredAccount(e,t){return e.completeDeferredAccountCreation({entitlements:t,consent:!1}).then(function(e){var t=vi.env.AUTH_HOST+"/svc/account/auth/v1/swg-dal-web",n=e.purchaseData.raw.data?e.purchaseData.raw.data:e.purchaseData.raw,o=JSON.parse(n),a={package_name:o.packageName,product_id:o.productId,purchase_token:o.purchaseToken,google_id_token:e.userData.idToken,google_user_email:e.userData.email,google_user_id:e.userData.id,google_user_name:e.userData.name},r=new XMLHttpRequest;r.withCredentials=!0,r.open("POST",t,!0),r.setRequestHeader("Content-Type","application/json"),r.send(JSON.stringify(a)),r.onload=function(){200===r.status?(swgDataLayer({name:"swg",context:"Deferred",label:"Seamless Signin",region:"swg-modal"}),e.complete().then(function(){window.location.reload(!0)})):(e.complete(),window.location=encodeURI(vi.env.AUTH_HOST+"/get-started/swg-link?redirect="+window.location.href))}}).catch(function(){return!!window.localStorage&&(!window.localStorage.getItem("nyt-swgOptOut")&&(window.localStorage.setItem("nyt-swgOptOut",(new Date).getTime()),!0))}),!0}function loginWithGoogle(){return"undefined"!=typeof window&&(-1===document.cookie.indexOf("NYT-S")&&(!0!==checkSwgOptOut()&&(!!window.SWG&&((window.SWG=window.SWG||[]).push(function(e){return e.init(vi.env.SWG_PUBLICATION_ID),e.getEntitlements().then(function(t){if(void 0===t||!t.raw)return!1;var n={entitlements_token:t.raw};return window.swgUserInfoXhrObject.withCredentials=!0,window.swgUserInfoXhrObject.open("POST",vi.env.AUTH_HOST+"/svc/account/auth/v1/login-swg-web",!0),window.swgUserInfoXhrObject.setRequestHeader("Content-Type","application/json"),window.swgUserInfoXhrObject.send(JSON.stringify(n)),window.swgUserInfoXhrObject.onload=function(){switch(window.swgUserInfoXhrObject.status){case 200:return swgDataLayer({name:"swg",context:"Seamless",label:"Seamless Signin",region:"login"}),window.location.reload(!0),!0;case 412:return swgDeferredAccount(e,t);default:return!1}},t}).catch(function(){return!1}),!0}),!0))))}var _f=function(){if(window.swgUserInfoXhrObject.checkSwgResponse=!1,-1===document.cookie.indexOf("NYT-S")){var e=document.createElement("script");e.src="https://news.google.com/swg/js/v1/swg.js",e.setAttribute("subscriptions-control","manual"),e.setAttribute("async",!0),e.onload=function(){loginWithGoogle()},document.getElementsByTagName("head")[0].appendChild(e)}};;_f.apply(null, []); })();
+ </script>
+
+ <link data-rh="true" rel="shortcut icon" href="/vi-assets/static-assets/favicon-4bf96cb6a1093748bf5b3c429accb9b4.ico"/><link data-rh="true" rel="apple-touch-icon" href="/vi-assets/static-assets/apple-touch-icon-319373aaf4524d94d38aa599c56b8655.png"/><link data-rh="true" rel="apple-touch-icon-precomposed" sizes="144×144" href="/vi-assets/static-assets/ios-ipad-144x144-319373aaf4524d94d38aa599c56b8655.png"/><link data-rh="true" rel="apple-touch-icon-precomposed" sizes="114×114" href="/vi-assets/static-assets/ios-iphone-114x144-61d373c43aa8365d3940c5f1135f4597.png"/><link data-rh="true" rel="apple-touch-icon-precomposed" href="/vi-assets/static-assets/ios-default-homescreen-57x57-7cccbfb151c7db793e92ea58c30b9e72.png"/><link data-rh="true" rel="alternate" itemprop="mainEntityOfPage" hrefLang="en-US" href="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><link data-rh="true" rel="canonical" href="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><link data-rh="true" rel="alternate" href="android-app://com.nytimes.android/nytimes/reader/id/100000006583622"/><link data-rh="true" rel="amphtml" href="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.amp.html"/><link data-rh="true" rel="alternate" type="application/json+oembed" href="https://www.nytimes.com/svc/oembed/json/?url=https%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html" title="She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database."/>
+ <script data-rh="true" >
+ if (typeof testCookie === 'undefined') {
+ var testCookie = function (name) {
+ var match = document.cookie.match(new RegExp(name + '=([^;]+)'));
+ if (match) return match[1];
+ }
+ }
+</script><script data-rh="true" >if (window.NYTD.Abra('dfp_story_toggle') !== '1_block') {
+
+ if (testCookie('nyt-gdpr') !== '1') {
+ var gptScript = document.createElement('script');
+ gptScript.async = 'async';
+ gptScript.src = '//securepubads.g.doubleclick.net/tag/js/gpt.js';
+ document.head.appendChild(gptScript);
+ }
+ }</script><script data-rh="true" >if (window.NYTD.Abra('dfp_story_toggle') !== '1_block') {
+
+ var googletag = googletag || {};
+ googletag.cmd = googletag.cmd || [];
+
+ if (testCookie('nyt-gdpr') == '1') {
+ googletag.cmd.push(function() {
+ googletag.pubads().setRequestNonPersonalizedAds(1);
+ });
+ }
+ }</script><script data-rh="true" >if (window.NYTD.Abra('dfp_story_toggle') !== '1_block') {
+ (function () { var _f=function(){var t,e,o=50,n=50;function i(t){if(!document.getElementById("3pCheckIframeId")){if(t||(t=1),!document.body){if(t>o)return;return t+=1,setTimeout(i.bind(null,t),n)}var e,a,r;e="https://static01.nyt.com/ads/tpc-check.html",a=document.body,(r=document.createElement("iframe")).src=e,r.id="3pCheckIframeId",r.style="display:none;",r.height=0,r.width=0,a.insertBefore(r,a.firstChild)}}function a(t){if("https://static01.nyt.com"===t.origin)try{"3PCookieSupported"===t.data&&googletag.cmd.push(function(){googletag.pubads().setTargeting("cookie","true")}),"3PCookieNotSupported"===t.data&&googletag.cmd.push(function(){googletag.pubads().setTargeting("cookie","false")})}catch(t){}}function r(){if(function(){if(Object.prototype.toString.call(window.HTMLElement).indexOf("Constructor")>0)return!0;if("[object SafariRemoteNotification]"===(!window.safari||safari.pushNotification).toString())return!0;try{return window.localStorage&&/Safari/.test(window.navigator.userAgent)}catch(t){return!1}}()){try{window.openDatabase(null,null,null,null)}catch(e){return t(),!0}try{localStorage.length?e():(localStorage.x=1,localStorage.removeItem("x"),e())}catch(o){navigator.cookieEnabled?t():e()}return!0}}!function(){try{googletag.cmd.push(function(){googletag.pubads().setTargeting("cookie","unknown")})}catch(t){}}(),t=function(){try{googletag.cmd.push(function(){googletag.pubads().setTargeting("cookie","private")})}catch(t){}}||function(){},e=function(){window.addEventListener("message",a,!1),i(0)}||function(){},function(){if(window.webkitRequestFileSystem)return window.webkitRequestFileSystem(window.TEMPORARY,1,e,t),!0}()||r()||function(){if(!window.indexedDB&&(window.PointerEvent||window.MSPointerEvent))return t(),!0}()||e()};;_f.apply(null, []); })();
+ }</script><script data-rh="true" >(function() {
+ var AdSlot4=function(){"use strict";function D(n,i,o){var t=document.getElementsByTagName("head")[0],e=document.createElement("script");i&&(e.onload=i),o&&(e.onerror=o),e.src=n,e.async=!0,t.appendChild(e)}return function(){var A=window.AdSlot4||{};A.cmd=A.cmd||[];var b=!1;if(A.loadScripts)return A;function z(t){"art, oak"!==t&&"art,oak"!==t||(t="art"),A.cmd.push(function(){A.events.subscribe({name:"AdDefined",scope:"all",callback:function(n){var o,i=[-1];n.sizes.forEach(function(n){n[0]<window.innerWidth&&n[0]>i[0]&&(i=[]).push(n)}),i[0][1]&&window.apstag.fetchBids({slots:[{slotID:n.id,slotName:"".concat(n.id,"_").concat(t,"_web"),sizes:(o=i[0][1],Array.isArray(o)?[[300,250],[728,90],[970,90],[970,250]].filter(function(i){return o.some(function(n){return n[0]===i[0]&&n[1]===i[1]})}):(console.warn("filterSizes() did not receive an array"),[]))}]},function(){window.googletag.cmd.push(function(){window.apstag.setDisplayBids()})})}})})}return A.loadScripts=function(n){var i,o,t,e,d,a,c,s,r=n||{},w=r.loadMnet,u=void 0===w||w,l=r.loadAmazon,p=void 0===l||l,f=r.loadBait,m=void 0===f||f,v=r.section,g=void 0===v?"none":v,h=r.pageViewId,y=void 0===h?"":h,B=r.pageType,x=void 0===B?"":B;b||("1"===(c="nyt-gdpr",(s=document.cookie.match(new RegExp("".concat(c,"=([^;]+)"))))?s[1]:"")||(d=document.referrer||"",(a=/([a-zA-Z0-9_\-.]+)(@|%40)([a-zA-Z0-9_\-.]+).([a-zA-Z]{2,5})/).test(d)||a.test(window.location.href))||(!u||window.advBidxc&&window.advBidxc.isLoaded||(t=y,e="8CU2553YN",window.innerWidth<740&&(e="8CULO58R6"),D("https://contextual.media.net/bidexchange.js?cid=".concat(e,"&dn=").concat("www.nytimes.com","&https=1"),function(){window.advBidxc&&window.advBidxc.isLoaded||console.warn("Media.net not loading properly")},function(){A.cmd.push(function(){A.events.publish({name:"BidderError",value:{type:"Mnet"}})})}),window.advBidxc=window.advBidxc||{},window.advBidxc.renderAd=function(){},window.advBidxc.startTime=(new Date).getTime(),window.advBidxc.customerId={mediaNetCID:e},window.advBidxc.misc={isGptDisabled:1},t&&(window.advBidxc.misc.keywords=t)),p&&!window.apstag&&(i=g,o=x,function(o,t){function n(n,i){t[o]._Q.push([n,i])}t[o]||(t[o]={init:function(){n("i",arguments)},fetchBids:function(){n("f",arguments)},setDisplayBids:function(){},targetingKeys:function(){return[]},_Q:[]})}("apstag",window),D("//c.amazon-adsystem.com/aax2/apstag.js",function(){window.apstag||console.warn("A9 not loading properly")},function(){A.cmd.push(function(){A.events.publish({name:"BidderError",value:{type:"A9"}})})}),window.apstag.init({pubID:"3030",adServer:"googletag",params:{si_section:i}}),z(o))),m&&D("https://static01.nyt.com/ads/google/adsbygoogle.js",function(){},function(){A.cmd.push(function(){A.events.publish({name:"AdEmpty",value:{type:"AdBlockOn"}})})}),b=!0)},window.AdSlot4=A}()}();
+ AdSlot4.loadScripts({
+ loadMnet: window.NYTD.Abra('medianet_toggle') !== '1_block',
+ loadAmazon: window.NYTD.Abra('amazon_toggle') !== '1_block',
+ section: 'nyregion',
+ pageType: 'art,oak',
+ pageViewId: window.NYTD.PageViewId.current,
+ });
+ (function () { var _f=function(e){var o=performance.navigation&&1===performance.navigation.type;function t(){return window.matchMedia("(max-width: 739px)").matches}function n(e){var n,r,i,d,a,p,u=function(){var e=window.userXhrObject&&""!==window.userXhrObject.responseText&&JSON.parse(window.userXhrObject.responseText).data||null,o=null;return e&&e.user&&e.user.userInfo&&(o=e.user.userInfo.demographics),o}();return u?(r=e,d=(n=u)&&n.emailSubscriptions,(a=n&&n.bundleSubscriptions)&&r&&(r.sub="reg",d&&d.length&&(r.em=d.toString().toLowerCase()),n.wat&&(r.wat=n.wat.toLowerCase()),a&&a.length&&a[0].bundle&&(i=a[0],r.sub=i.bundle.toLowerCase(),i.source&&(r.subsrc=i.source.toLowerCase()),i.promotion&&(r.subprm=i.promotion),i.in_grace&&(r.grace=i.in_grace.toString()))),e=r):e.sub="anon",t()?(e.prop="mnyt",e.plat="mweb",e.ver="mvi"):(e.prop="nyt",e.plat="web",e.ver="vi"),"hp"===e.typ&&(document.referrer&&(e.topref=document.referrer),o&&(e.refresh="manual")),e.abra_dfp=(p=document.documentElement.getAttribute("data-nyt-ab"))?p.split(" ").reduce(function(e,o){var t=o.split("="),n=t[0].toLowerCase(),r=t[1];return(n.indexOf("dfp")>-1||n.indexOf("redbird")>-1)&&e.push(n+"_"+r),e},[]):"",e.page_view_id=window.NYTD.PageViewId&&window.NYTD.PageViewId.current,e}var r=e||{},i=r.adTargeting||{},d=r.adUnitPath||"/29390238/nyt/homepage",a=r.offset||400,p=r.hideTopAd||t(),u=r.lockdownAds||!1,s=r.sizeMapping||{top:[[970,["fluid",[728,90],[970,90],[970,250],[1605,300]]],[728,["fluid",[728,90],[1605,300]]],[0,["fluid",[300,250],[300,420]]]],fp1:[[0,[195,250]]],fp2:[[0,[195,250]]],fp3:[[0,[195,250]]],interstitial:[[0,[[1,1],[640,480]]]],mktg:[[1020,[300,250]],[0,[]]],pencil:[[728,[[336,46]],[0,[]]]],pp_edpick:[[0,["fluid"]]],pp_morein:[[0,["fluid"],[210,218]]],ribbon:[[0,["fluid"]]],sponsor:[[765,[150,50]],[0,[320,25]]],supplemental:[[1020,[[300,250],[300,600]]],[0,[]]],default:[[970,["fluid",[728,90],[970,90],[970,250],[1605,300]]],[728,["fluid",[728,90],[300,250],[1605,300]]],[0,["fluid",[300,250],[300,420]]]]},l=r.dfpToggleName||"dfp_home_toggle";window.AdSlot4=window.AdSlot4||{},window.AdSlot4.cmd=window.AdSlot4.cmd||[],window.AdSlot4.cmd.push(function(){window.AdSlot4.init({adTargeting:n(i),adUnitPath:d,sizeMapping:s,offset:a,haltDFP:"1_block"===window.NYTD.Abra(l),hideTopAd:p,lockdownAds:u}),window.NYTD.Abra.reportExposure("dfp_adslot4v2")})};;_f.apply(null, [{"adTargeting":{"edn":"us","sov":"3","test":"projectvi","ver":"vi","hasVideo":false,"template":"article","als_test":"1565027040168","prop":"nyt","plat":"web","brandsensitive":"false","org":"policedepartmentnyc","geo":"newyorkcity","des":"juveniledelinquency,facialrecognitionsoftware,privacy,surveillanceofcitizensbygovern,police,civilrightsandliberties","auth":"aliwatkins,josephgoldstein","coll":"newyork,usnews,technology,techandsociety","artlen":"medium","ledemedsz":"none","typ":"art,oak","section":"nyregion","si_section":"nyregion","id":"100000006583622","pt":"nt10,nt15,nt16,nt18,nt3,nt4,nt9","gscat":"neg_mastercard,gs_law_misc,neg_chanel,gv_crime,neg_hearts,gs_tech,gs_law,gs_tech_computing,neg_ibmtest,gs_tech_phones,neg_samsung,gs_education"},"adUnitPath":"/29390238/nyt/nyregion/","dfpToggleName":"dfp_story_toggle"}]); })();
+ })();</script><script data-rh="true" id="als-svc">var alsVariant = window.NYTD.Abra('DFP_als');
+ if (alsVariant != null && alsVariant.match(/(0_control|1_als)/)) {
+ window.NYTD.Abra.reportExposure('DFP_als');
+ }
+ if (window.NYTD.Abra('DFP_als') === '1_als') {
+ (function () { var _f=function(){window.googletag=window.googletag||{},googletag.cmd=googletag.cmd||[];var e=new XMLHttpRequest,t="prd"===window.vi.env.ENVIRONMENT?"als-svc.nytimes.com":"als-svc.dev.nytimes.com",n=document.querySelector('[name="nyt_uri"]'),o=null==n?"":encodeURIComponent(n.content),l=document.querySelector('[name="template"]'),s=document.querySelector('[name="prop"]'),a=document.querySelector('[name="plat"]'),i=null==l||null==l.content?"":l.content,c=null==s||null==s.content?"nyt":s.content,r=null==a||null==a.content?"web":a.content;window.innerWidth<740&&(c="mnyt",r="mweb"),"/"===location.pathname&&(o=encodeURIComponent("https://www.nytimes.com/pages/index.html"));var d=window.localStorage.getItem("als_test_clientside");void 0!==d&&window.googletag.cmd.push(function(){googletag.pubads().setTargeting("als_test_clientside",d)}),e.open("GET","https://"+t+"/als?uri="+o+"&typ="+i+"&prop="+c+"&plat="+r),e.withCredentials=!0,e.send(),e.onreadystatechange=function(){if(4===e.readyState)if(200===e.status){var t=JSON.parse(e.responseText);window.googletag.cmd.push(function(){void 0!==t.als_test_clientside&&(googletag.pubads().setTargeting("als_test_clientside",t.als_test_clientside),window.localStorage.setItem("als_test_clientside","ls-"+t.als_test_clientside)),Object.keys(t).forEach(function(e){"User"===e&&void 0!==t[e]&&window.localStorage.setItem("UTS_User",JSON.stringify(t[e]))})})}else{console.error("Error "+e.responseText);(window.dataLayer=window.dataLayer||[]).push({event:"impression",module:{name:"timing",context:"script-load",label:"alsService-als-error"}})}}};;_f.apply(null, []); })();
+ }
+ </script>
+ <link rel="stylesheet" href="/vi-assets/static-assets/global-42db6c8821fec0e2b3837b2ea2ece8fe.css" />
+ <style>.css-1dv1kvn{border:0;-webkit-clip:rect(0 0 0 0);clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;}.css-v89234{overflow:hidden;height:100%;}.css-nuvmzp{font-size:14.25px;font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:700;text-transform:uppercase;-webkit-letter-spacing:0.7px;-moz-letter-spacing:0.7px;-ms-letter-spacing:0.7px;letter-spacing:0.7px;line-height:19px;}.css-nuvmzp:hover{-webkit-text-decoration:underline;text-decoration:underline;}.css-1gz70xg{border-left:1px solid #ccc;color:#326891;height:12px;margin-left:8px;padding-left:8px;}.css-9e9ivx{display:none;font-size:10px;margin-left:auto;text-transform:uppercase;}.hasLinks .css-9e9ivx{display:block;}@media (min-width:740px){.hasLinks .css-9e9ivx{margin:none;position:absolute;right:20px;}}@media (min-width:1024px){.hasLinks .css-9e9ivx{display:none;}}.css-2bwtzy{display:inline-block;padding:6px 4px 4px;margin-bottom:12px;font-size:12px;border-radius:3px;-webkit-transition:background 0.6s ease;transition:background 0.6s ease;}.css-2bwtzy:hover{background-color:#f7f7f7;}.css-1hyfx7x{display:none;}.css-6n7j50{display:inline;}.css-1kj7lfb{display:none;}@media (min-width:1024px){.css-1kj7lfb{display:inline-block;margin-right:7px;}}.css-10m9xeu{display:block;width:16px;height:16px;}.css-vz7hjd{border:0;-webkit-clip:rect(0 0 0 0);clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;}.css-1fe7a5q{display:inline-block;height:16px;vertical-align:sub;width:16px;}.css-1rn5q1r{border:0;-webkit-clip:rect(0 0 0 0);clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:transparent;color:#000;font-size:11px;font-weight:700;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;padding:7px 9px 9px;background:#fff;display:inline-block;left:44px;text-transform:uppercase;-webkit-transition:none;transition:none;}.css-1rn5q1r:active,.css-1rn5q1r:focus{-webkit-clip:auto;clip:auto;overflow:visible;width:auto;height:auto;}.css-1rn5q1r::-moz-focus-inner{padding:0;border:0;}.css-1rn5q1r:-moz-focusring{outline:1px dotted;}.css-1rn5q1r:disabled,.css-1rn5q1r.disabled{opacity:0.5;cursor:default;}.css-1rn5q1r:active,.css-1rn5q1r.active{background-color:#f7f7f7;}@media (min-width:740px){.css-1rn5q1r:hover{background-color:#f7f7f7;}}.css-1rn5q1r:focus{margin-top:3px;padding:8px 8px 6px;}@media (min-width:1024px){.css-1rn5q1r{left:112px;}}.css-10488qs{display:none;}@media (min-width:1024px){.css-10488qs{display:inline-block;position:relative;}}.css-1iruc8t{list-style:none;margin:0;padding:0;}.css-1ropbjl::before{background-color:$white;border-bottom:1px solid #e2e2e2;border-top:2px solid #e2e2e2;content:'';display:block;height:1px;margin-top:0;}@media (min-width:1150px){.css-1ropbjl{margin:0 auto;max-width:1200px;padding:0 3% 9px;}}.NYTApp .css-1ropbjl{display:none;}@media print{.css-1ropbjl{display:none;}}.css-uw59u{padding:0 20px;}@media (min-width:740px){.css-uw59u{padding:0 3%;}}@media (min-width:1150px){.css-uw59u{padding:0;}}.css-jxzr5i{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row;-ms-flex-flow:row;flex-flow:row;}.css-oylsik{display:block;height:44px;vertical-align:middle;width:184px;}.css-1otr2jl{margin:18px 0 0 auto;}.css-1c8n994{color:#6288a5;font-family:nyt-franklin;font-size:11px;font-style:normal;font-weight:400;line-height:11px;-webkit-text-decoration:none;text-decoration:none;}.css-qtw155{display:block;}@media (min-width:1150px){.css-qtw155{display:none;}}.css-v0l3hm{display:none;}@media (min-width:1150px){.css-v0l3hm{display:block;}}.css-g4gku8{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-top:10px;min-width:600px;}.css-1rr4qq7{-webkit-flex:1;-ms-flex:1;flex:1;}.css-6xhk3s{border-left:1px solid #e2e2e2;-webkit-flex:1;-ms-flex:1;flex:1;padding-left:15px;}.css-rxqrcl{color:#333;font-size:13px;font-weight:700;font-family:nyt-franklin;height:25px;line-height:15px;margin:0;text-transform:uppercase;width:150px;}.css-tj0ten{margin-bottom:5px;white-space:nowrap;}.css-tj0ten:last-child{margin-bottom:10px;}.css-ist4u3.desktop{display:none;}@media (min-width:740px){.css-ist4u3.desktop{display:block;}.css-ist4u3.smartphone{display:none;}}.css-1gprdgz{list-style:none;margin:0;padding:0;-webkit-columns:2;columns:2;padding:0 0 15px;}.css-10t7hia{height:34px;line-height:34px;list-style-type:none;}.css-10t7hia.desktop{display:none;}@media (min-width:740px){.css-10t7hia.desktop{display:block;}.css-10t7hia.smartphone{display:none;}}.css-mzqdl{color:#333;display:block;font-family:nyt-franklin;font-size:15px;font-weight:500;height:34px;line-height:34px;-webkit-text-decoration:none;text-decoration:none;text-transform:capitalize;}.css-kwpx34{color:#000;display:inline-block;font-family:nyt-franklin;-webkit-text-decoration:none;text-decoration:none;text-transform:capitalize;width:150px;font-size:14px;font-weight:500;height:23px;line-height:16px;}.css-kwpx34:hover{cursor:pointer;-webkit-text-decoration:underline;text-decoration:underline;}body.dark .css-kwpx34{color:#fff;}.css-1k2cjfc{color:#000;display:inline-block;font-family:nyt-franklin;-webkit-text-decoration:none;text-decoration:none;text-transform:capitalize;width:150px;font-size:16px;font-weight:700;height:25px;line-height:15px;padding-bottom:0;}.css-1k2cjfc:hover{cursor:pointer;-webkit-text-decoration:underline;text-decoration:underline;}body.dark .css-1k2cjfc{color:#fff;}.css-1vhk1ks{color:#000;display:inline-block;font-family:nyt-franklin;-webkit-text-decoration:none;text-decoration:none;text-transform:capitalize;width:150px;font-size:11px;font-weight:500;height:23px;line-height:21px;}.css-1vhk1ks:hover{cursor:pointer;-webkit-text-decoration:underline;text-decoration:underline;}body.dark .css-1vhk1ks{color:#fff;}.css-6td9kr{list-style:none;margin:0;padding:0;border-top:1px solid #e2e2e2;margin-top:2px;padding-top:10px;}.css-r5ic95{display:inline-block;height:13px;width:13px;margin-right:7px;vertical-align:middle;}.css-15uy5yv{border-top:1px solid #ebebeb;padding-top:9px;}.css-1p8nkc0{color:#999;font-family:nyt-franklin,helvetica,arial,sans-serif;padding:10px 0;-webkit-text-decoration:none;text-decoration:none;white-space:nowrap;}.css-1p8nkc0:hover{-webkit-text-decoration:underline;text-decoration:underline;}@-webkit-keyframes animation-5j8bii{from{opacity:0;}to{opacity:1;}}@keyframes animation-5j8bii{from{opacity:0;}to{opacity:1;}}@-webkit-keyframes animation-1am0aiv{from{visibility:visible;opacity:1;}to{visibility:visible;opacity:0;}}@keyframes animation-1am0aiv{from{visibility:visible;opacity:1;}to{visibility:visible;opacity:0;}}.css-1g7m0tk{color:#326891;}.css-1g7m0tk:visited{color:#326891;}.css-d8bdto{color:#999;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:17px;margin-bottom:5px;}@media print{.css-d8bdto{display:none;}}.css-y8aj3r{padding:0;}.css-60hakz{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;}.css-60hakz a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-60hakz:nth-of-type(3),.css-60hakz:nth-of-type(4){display:none;}}.css-60hakz:last-of-type{margin-right:0;}.css-i29ckm{width:calc(100% - 40px);max-width:600px;margin:1.5rem auto 2rem;}@media (min-width:1440px){.css-i29ckm{width:600px;max-width:600px;}}@media (min-width:600px){.css-i29ckm{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}}@media (max-width:600px){.css-i29ckm .facebook,.css-i29ckm .twitter,.css-i29ckm .email{display:inline-block;}}.css-acwcvw{margin-bottom:1rem;}.css-1baulvz{display:inline-block;}@-webkit-keyframes animation-f8wsfj{0%{opacity:1;}50%{opacity:0;}100%{opacity:0;}}@keyframes animation-f8wsfj{0%{opacity:1;}50%{opacity:0;}100%{opacity:0;}}@-webkit-keyframes animation-mhvv8m{0%{opacity:0;}50%{opacity:0;}100%{opacity:1;}}@keyframes animation-mhvv8m{0%{opacity:0;}50%{opacity:0;}100%{opacity:1;}}@-webkit-keyframes animation-m6999o{100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);-webkit-transform-origin:center;-ms-transform-origin:center;transform-origin:center;}}@keyframes animation-m6999o{100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);-webkit-transform-origin:center;-ms-transform-origin:center;transform-origin:center;}}.css-i9gxme{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;}@-webkit-keyframes animation-1m9j9gf{from{background-color:#f7f7f5;}to{background-color:transparent;}}@keyframes animation-1m9j9gf{from{background-color:#f7f7f5;}to{background-color:transparent;}}.css-1sy8kpn{display:none;}@media (min-width:765px){.css-1sy8kpn{background-color:#f7f7f7;border-bottom:1px solid #f3f3f3;display:block;padding-bottom:15px;padding-top:15px;margin:0;min-height:90px;}}@media print{.css-1sy8kpn{display:none;}}.css-19vbshk{color:#ccc;display:none;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:0.5625rem;font-weight:300;-webkit-letter-spacing:0.05rem;-moz-letter-spacing:0.05rem;-ms-letter-spacing:0.05rem;letter-spacing:0.05rem;line-height:0.5625rem;margin-left:auto;text-align:center;text-transform:uppercase;}@media (min-width:600px){.css-19vbshk{display:inline-block;}}.css-19vbshk p{margin-bottom:auto;margin-right:7px;margin-top:auto;text-transform:none;}.css-l9onyx{color:#ccc;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:0.5625rem;font-weight:300;-webkit-letter-spacing:0.05rem;-moz-letter-spacing:0.05rem;-ms-letter-spacing:0.05rem;letter-spacing:0.05rem;line-height:0.5625rem;margin-bottom:9px;text-align:center;text-transform:uppercase;}.css-79elbk{position:relative;}@-webkit-keyframes animation-1q1yk17{to{width:11px;}}@keyframes animation-1q1yk17{to{width:11px;}}@-webkit-keyframes animation-g7rb99{0%{-webkit-transform:scale(1) rotate(0);-ms-transform:scale(1) rotate(0);transform:scale(1) rotate(0);}100%{-webkit-transform:scale(1.05) rotate(-90deg);-ms-transform:scale(1.05) rotate(-90deg);transform:scale(1.05) rotate(-90deg);}}@keyframes animation-g7rb99{0%{-webkit-transform:scale(1) rotate(0);-ms-transform:scale(1) rotate(0);transform:scale(1) rotate(0);}100%{-webkit-transform:scale(1.05) rotate(-90deg);-ms-transform:scale(1.05) rotate(-90deg);transform:scale(1.05) rotate(-90deg);}}.css-k008qs{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}.sizeSmall .css-bsn42l{width:50%;}@media (min-width:600px){.sizeSmall .css-bsn42l{width:300px;}}@media (min-width:1440px){.sizeSmall .css-bsn42l{width:300px;}}@media (max-width:600px){.sizeSmall .css-bsn42l{width:50%;}}.sizeSmall.sizeSmallNoCaption .css-bsn42l{margin-left:auto;margin-right:auto;}@media (min-width:740px){.sizeSmall.layoutVertical .css-bsn42l{max-width:250px;}}@media (min-width:1024px){.sizeSmall.layoutVertical .css-bsn42l{width:250px;}}@media (max-width:740px){.sizeSmall.layoutVertical .css-bsn42l{max-width:250px;}}@media (min-width:740px){.sizeSmall.layoutHorizontal .css-bsn42l{max-width:300px;}}@media (min-width:1024px){.sizeSmall.layoutHorizontal .css-bsn42l{width:300px;}}@media (max-width:740px){.sizeSmall.layoutHorizontal .css-bsn42l{max-width:300px;}}@media (min-width:600px){.sizeMedium.layoutVertical.verticalVideo .css-bsn42l{width:310px;}}.css-11cwn6f{width:100%;vertical-align:top;}.css-11cwn6f img{width:100%;vertical-align:top;}@-webkit-keyframes animation-ghw4n2{0%{opacity:0;-webkit-transform:translateY(100px);-ms-transform:translateY(100px);transform:translateY(100px);}100%{opacity:1;-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0);}}@keyframes animation-ghw4n2{0%{opacity:0;-webkit-transform:translateY(100px);-ms-transform:translateY(100px);transform:translateY(100px);}100%{opacity:1;-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0);}}@-webkit-keyframes animation-1c5cfvc{0%{-webkit-transform:translate(0px,0px) scale(1,1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,1) translate(0px,0px);transform:translate(0px,0px) scale(1,1) translate(0px,0px);}25%{-webkit-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);}50%{-webkit-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);}75%{-webkit-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);}100%{-webkit-transform:translate(0px,0px) scale(1,1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,1) translate(0px,0px);transform:translate(0px,0px) scale(1,1) translate(0px,0px);}}@keyframes animation-1c5cfvc{0%{-webkit-transform:translate(0px,0px) scale(1,1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,1) translate(0px,0px);transform:translate(0px,0px) scale(1,1) translate(0px,0px);}25%{-webkit-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);}50%{-webkit-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);}75%{-webkit-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);}100%{-webkit-transform:translate(0px,0px) scale(1,1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,1) translate(0px,0px);transform:translate(0px,0px) scale(1,1) translate(0px,0px);}}@-webkit-keyframes animation-htgkrt{0%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);}25%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);}50%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);}75%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);}100%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);}}@keyframes animation-htgkrt{0%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);}25%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);}50%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);}75%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);}100%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);}}@-webkit-keyframes animation-e64et{0%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}25%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);}50%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}75%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);}100%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}}@keyframes animation-e64et{0%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}25%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);}50%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}75%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);}100%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}}@-webkit-keyframes animation-9zaqp9{0%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}25%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);}50%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}75%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);}100%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}}@keyframes animation-9zaqp9{0%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}25%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);}50%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}75%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);}100%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}}@-webkit-keyframes animation-16fq4rz{0%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}25%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);}50%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);}75%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);}100%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}}@keyframes animation-16fq4rz{0%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}25%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);}50%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);}75%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);}100%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}}@-webkit-keyframes animation-1kjk1j2{0%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}25%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);}50%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);}75%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);}100%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}}@keyframes animation-1kjk1j2{0%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}25%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);}50%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);}75%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);}100%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}}@-webkit-keyframes animation-88g286{0%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}25%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);}50%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}75%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);}100%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}}@keyframes animation-88g286{0%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}25%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);}50%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}75%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);}100%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}}@-webkit-keyframes animation-12yx39b{0%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}25%{-webkit-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);}50%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}75%{-webkit-transform:translate(34px,0px) scale(1,1) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,1) translate(-34px,0px);transform:translate(34px,0px) scale(1,1) translate(-34px,0px);}100%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}}@keyframes animation-12yx39b{0%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}25%{-webkit-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);}50%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}75%{-webkit-transform:translate(34px,0px) scale(1,1) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,1) translate(-34px,0px);transform:translate(34px,0px) scale(1,1) translate(-34px,0px);}100%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}}@-webkit-keyframes animation-4hu8jm{0%{-webkit-transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);}33.33%{-webkit-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);}100%{-webkit-transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);}}@keyframes animation-4hu8jm{0%{-webkit-transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);}33.33%{-webkit-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);}100%{-webkit-transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);}}@-webkit-keyframes animation-1wqz2f4{0%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);}100%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);}}@keyframes animation-1wqz2f4{0%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);}100%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);}}@-webkit-keyframes animation-yl3z84{0%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);}33.33%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);}100%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);}}@keyframes animation-yl3z84{0%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);}33.33%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);}100%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);}}@-webkit-keyframes animation-1q3gjvc{0%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);}100%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);}}@keyframes animation-1q3gjvc{0%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);}100%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);}}@-webkit-keyframes animation-nc39ev{0%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);}33.33%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}100%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);}}@keyframes animation-nc39ev{0%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);}33.33%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}100%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);}}@-webkit-keyframes animation-amd09y{0%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}100%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);}}@keyframes animation-amd09y{0%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}100%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);}}@-webkit-keyframes animation-ru1vxe{0%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);}100%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);}}@keyframes animation-ru1vxe{0%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);}100%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);}}@-webkit-keyframes animation-ajnadh{0%{-webkit-transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);}33.33%{-webkit-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);}100%{-webkit-transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);}}@keyframes animation-ajnadh{0%{-webkit-transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);}33.33%{-webkit-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);}100%{-webkit-transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);}}.css-1ri25x2{display:none;}@media (min-width:740px){.css-1ri25x2{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:16px;height:31px;}}@media (min-width:1024px){.css-1ri25x2{display:none;}}.css-12fr9lp{height:23px;margin-top:6px;}.css-1hfdzay{display:none;}@media (min-width:1024px){.css-1hfdzay{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-top:0;}}.css-4g4cvq{display:none;}@media (min-width:740px){.css-4g4cvq{position:fixed;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;opacity:0;z-index:1;-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;width:100%;height:32.063px;background:white;padding:5px 0;top:0;text-align:center;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;box-shadow:rgba(0,0,0,0.08) 0 0 5px 1px;border-bottom:1px solid #e2e2e2;}}.css-m6xlts{margin-left:20px;margin-right:20px;max-width:1605px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;position:relative;width:100%;}@media (min-width:1360px){.css-m6xlts{margin-left:20px;margin-right:20px;}}@media (min-width:1780px){.css-m6xlts{margin-left:auto;margin-right:auto;}}.css-1ahhg7f{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;max-width:1605px;overflow:hidden;position:absolute;width:56%;margin-left:calc((100% - 56%) / 2);}@media (min-width:1024px){.css-1ahhg7f{width:56%;margin-left:calc((100% - 56%) / 2);}}@media (min-width:1024px){.css-1ahhg7f{width:53%;margin-left:calc((100% - 53%) / 2);}}.css-fwqvlz{font-family:nyt-cheltenham-small,georgia,'times new roman';font-weight:400;font-size:13px;-webkit-letter-spacing:0.015em;-moz-letter-spacing:0.015em;-ms-letter-spacing:0.015em;letter-spacing:0.015em;margin-top:10.5px;margin-right:auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.css-17xtcya{font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:700;font-size:12.5px;text-transform:uppercase;-webkit-letter-spacing:0;-moz-letter-spacing:0;-ms-letter-spacing:0;letter-spacing:0;margin-top:12.5px;margin-bottom:auto;margin-left:auto;white-space:nowrap;}.css-17xtcya:hover{-webkit-text-decoration:underline;text-decoration:underline;}.css-x15j1o{display:inline-block;padding-left:7px;padding-right:7px;font-size:13px;margin-top:10px;margin-bottom:auto;color:#ccc;}.css-1705lsu{margin-top:auto;margin-bottom:auto;margin-left:auto;background-color:#fff;z-index:50;box-shadow:-14px 2px 7px -2px rgba(255,255,255,0.7);}@media (min-width:740px){.css-1iwv8en{margin-top:1px;}}@media (min-width:1024px){.css-1iwv8en{margin-top:0;}}@-webkit-keyframes animation-b7n1on{0%{-webkit-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);}}@keyframes animation-b7n1on{0%{-webkit-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);}}@-webkit-keyframes animation-1b9egsl{0%{-webkit-transform:rotate(20deg);-ms-transform:rotate(20deg);transform:rotate(20deg);}100%{-webkit-transform:rotate(380deg);-ms-transform:rotate(380deg);transform:rotate(380deg);}}@keyframes animation-1b9egsl{0%{-webkit-transform:rotate(20deg);-ms-transform:rotate(20deg);transform:rotate(20deg);}100%{-webkit-transform:rotate(380deg);-ms-transform:rotate(380deg);transform:rotate(380deg);}}.css-1rj8to8{display:inline-block;line-height:1em;}.css-1rj8to8 .css-0{-webkit-text-decoration:none;text-decoration:none;display:inline-block;}.css-4w91ra{display:inline-block;padding-left:3px;}.css-wg1cha{margin-left:20px;margin-right:20px;}@media (min-width:600px){.css-wg1cha{width:calc(100% - 40px);max-width:600px;margin:1.5rem auto 1em;}}@media (min-width:1440px){.css-wg1cha{width:600px;max-width:600px;margin:1.5rem auto 1em;}}.css-1ubp8k9{font-family:nyt-imperial,georgia,'times new roman',times,serif;font-style:italic;font-size:1.0625rem;line-height:1.5rem;width:calc(100% - 40px);max-width:600px;margin:1rem auto 0.75rem;}@media (min-width:740px){.css-1ubp8k9{font-size:1.1875rem;line-height:1.75rem;margin-bottom:1.25rem;}}@media (min-width:1440px){.css-1ubp8k9{width:600px;max-width:600px;}}@media print{.css-1ubp8k9{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@-webkit-keyframes animation-1egl8em{from{opacity:0;-webkit-transform:translate3d(0,13%,0);-ms-transform:translate3d(0,13%,0);transform:translate3d(0,13%,0);}to{opacity:1;-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);transform:translate3d(0,0,0);}}@keyframes animation-1egl8em{from{opacity:0;-webkit-transform:translate3d(0,13%,0);-ms-transform:translate3d(0,13%,0);transform:translate3d(0,13%,0);}to{opacity:1;-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);transform:translate3d(0,0,0);}}.css-vdv0al{font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:0.75rem;line-height:1rem;width:calc(100% - 40px);max-width:600px;margin:0 auto 1em;color:#999;}.css-vdv0al a{color:#999;-webkit-text-decoration:none;text-decoration:none;}.css-vdv0al a:hover{-webkit-text-decoration:underline;text-decoration:underline;}@media (min-width:1440px){.css-vdv0al{width:600px;max-width:600px;}}@media print{.css-vdv0al{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@media print{.css-vdv0al span{display:none;}}.css-1i2y565 .e6idgb70 + .e1h9rw200{margin-top:0;}.css-1i2y565 .eoo0vm40 + .e1gnsphs0{margin-top:-0.3em;}.css-1i2y565 .e6idgb70 + .eoo0vm40{margin-top:0;}.css-1i2y565 .eoo0vm40 + figure{margin-top:1.2rem;}.css-1i2y565 .e1gnsphs0 + figure{margin-top:1.2rem;}.css-o6xoe7{display:none;}@media (min-width:1024px){.css-o6xoe7{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-right:0;margin-left:auto;width:130px;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}}@media (min-width:1150px){.css-o6xoe7{width:210px;}}@media print{.css-o6xoe7{display:none;}}.css-1fanzo5{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;margin-bottom:1rem;}@media (min-width:1024px){.css-1fanzo5{-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;height:100%;width:945px;margin-left:auto;margin-right:auto;}}@media (min-width:1150px){.css-1fanzo5{width:1110px;margin-left:auto;margin-right:auto;}}@media (min-width:1280px){.css-1fanzo5{width:1170px;}}@media (min-width:1440px){.css-1fanzo5{width:1200px;}}@media print{.css-1fanzo5{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@media print{.css-1fanzo5{margin-bottom:1em;display:block;}}.css-53u6y8{margin-left:auto;margin-right:auto;width:100%;}@media (min-width:1024px){.css-53u6y8{margin-left:calc((100% - 600px) / 2);margin-right:0;width:600px;}}@media (min-width:1440px){.css-53u6y8{max-width:600px;width:600px;margin-left:calc((100% - 600px) / 2);}}@media print{.css-53u6y8{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-1m50asq{width:100%;vertical-align:top;}.css-z3e15g{position:fixed;opacity:0;-webkit-scroll-events:none;-moz-scroll-events:none;-ms-scroll-events:none;scroll-events:none;top:0;left:0;bottom:0;right:0;-webkit-transition:opacity 0.2s;transition:opacity 0.2s;background-color:#fff;pointer-events:none;}.css-uwwqev{width:100%;height:100%;}.css-1ly73wi{position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;-webkit-clip:rect(0 0 0 0);clip:rect(0 0 0 0);overflow:hidden;}@-webkit-keyframes animation-7y3qfv{0%{opacity:0;}10%,90%{opacity:1;}100%{opacity:0;}}@keyframes animation-7y3qfv{0%{opacity:0;}10%,90%{opacity:1;}100%{opacity:0;}}.css-l72opv{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;position:relative;right:3px;}.css-l72opv a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-l72opv:nth-of-type(3),.css-l72opv:nth-of-type(4){display:none;}}.css-l72opv:last-of-type{margin-right:0;}.css-4skfbu{color:#999;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:17px;margin-bottom:5px;margin-bottom:0;}@media print{.css-4skfbu{display:none;}}.css-1fcn4th{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;}.css-1fcn4th a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-1fcn4th:nth-of-type(3),.css-1fcn4th:nth-of-type(4){display:none;}}.css-1fcn4th:last-of-type{margin-right:0;}@media (max-width:1150px){.css-1fcn4th:nth-of-type(1),.css-1fcn4th:nth-of-type(2),.css-1fcn4th:nth-of-type(3){display:none;}}.css-13zu7ev{display:inline-block;height:15px;vertical-align:middle;width:15px;background-color:#eee;border:1px #eee solid;border-radius:100%;padding:5px;width:14px;height:14px;}.css-13zu7ev.facebook{background-image:url(/vi-assets/static-assets/icon-fb-circle-2ec7780140bd9e8e8398bbcdf5661569.svg);}.css-13zu7ev.twitter{background-image:url(/vi-assets/static-assets/icon-twitter-circle-fc7c2748f5613c68963a0df203bffc05.svg);}.css-13zu7ev.email{background-image:url(/vi-assets/static-assets/icon-share-email-circle-a2acce7e23b21d47bb606b628a6c7e34.svg);}.css-13zu7ev.link{background-image:url(/vi-assets/static-assets/icon-share-permalink-circle-3ff3876a106221ee493d9542c0895863.svg);}.css-13zu7ev.linkedin{background-image:url(/vi-assets/static-assets/icon-share-linkedin-circle-4d7bcf236c5f3a086738746f41f46f7b.svg);}.css-13zu7ev.whatsapp{background-image:url(/vi-assets/static-assets/icon-whatsapp-video-a9503bf2b3c73111106c496d3ebc8c67.svg);}.css-13zu7ev.reddit{background-image:url(/vi-assets/static-assets/icon-share-reddit-f882338200fd1971b767627fa5f60124.svg);}.css-13zu7ev:hover{background-color:#fff;border:1px solid #ccc;}.css-f7l8cz{display:inline-block;height:15px;vertical-align:middle;width:15px;bottom:5px;position:relative;width:14px;height:14px;bottom:4px;}.css-f7l8cz.facebook{background-image:url(/vi-assets/static-assets/icon-fb-circle-2ec7780140bd9e8e8398bbcdf5661569.svg);}.css-f7l8cz.twitter{background-image:url(/vi-assets/static-assets/icon-twitter-circle-fc7c2748f5613c68963a0df203bffc05.svg);}.css-f7l8cz.email{background-image:url(/vi-assets/static-assets/icon-share-email-circle-a2acce7e23b21d47bb606b628a6c7e34.svg);}.css-f7l8cz.link{background-image:url(/vi-assets/static-assets/icon-share-permalink-circle-3ff3876a106221ee493d9542c0895863.svg);}.css-f7l8cz.linkedin{background-image:url(/vi-assets/static-assets/icon-share-linkedin-circle-4d7bcf236c5f3a086738746f41f46f7b.svg);}.css-f7l8cz.whatsapp{background-image:url(/vi-assets/static-assets/icon-whatsapp-video-a9503bf2b3c73111106c496d3ebc8c67.svg);}.css-f7l8cz.reddit{background-image:url(/vi-assets/static-assets/icon-share-reddit-f882338200fd1971b767627fa5f60124.svg);}.css-16ogagc{background:transparent;display:inline-block;height:20px;width:20px;background-color:#eee;border:1px #eee solid;border-radius:100%;padding:5px;width:27px;height:27px;}.css-16ogagc.hidden{opacity:0;visibility:hidden;}.css-16ogagc.hidden:focus{opacity:1;}.css-16ogagc:hover{background-color:#fff;border:1px solid #ccc;}.css-17ai7jg{color:#666;font-family:nyt-imperial,georgia,'times new roman',times,serif;margin:10px 20px 0;text-align:left;}.css-17ai7jg a{color:#326891;-webkit-text-decoration:none;text-decoration:none;}.css-17ai7jg a:hover,.css-17ai7jg a:focus{-webkit-text-decoration:underline;text-decoration:underline;}@media (min-width:600px){.css-17ai7jg{margin-left:0;}}.sizeSmall .css-17ai7jg{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;width:calc(50% - 15px);margin:auto 0 15px 15px;}@media (min-width:600px){.sizeSmall .css-17ai7jg{width:260px;margin-left:15px;}}@media (min-width:740px){.sizeSmall .css-17ai7jg{margin-left:15px;}}@media (min-width:1440px){.sizeSmall .css-17ai7jg{width:330px;margin-left:15px;}}@media (max-width:600px){.sizeSmall .css-17ai7jg{margin:auto 0 0 15px;}}.sizeSmall.sizeSmallNoCaption .css-17ai7jg{margin-left:auto;margin-right:auto;margin-top:10px;}.sizeMedium .css-17ai7jg{max-width:900px;}.sizeMedium.layoutVertical.verticalVideo .css-17ai7jg{margin-left:0;margin-right:0;}@media (min-width:600px){.sizeMedium.layoutVertical.verticalVideo .css-17ai7jg{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;width:255px;margin:auto 0 15px 15px;}}@media (min-width:1440px){.sizeMedium.layoutVertical.verticalVideo .css-17ai7jg{width:325px;}}.sizeLarge .css-17ai7jg{max-width:none;}@media (min-width:600px){.sizeLarge .css-17ai7jg{margin-left:20px;}}@media (min-width:740px){.sizeLarge .css-17ai7jg{margin-left:20px;max-width:900px;}}@media (min-width:1024px){.sizeLarge .css-17ai7jg{margin-left:0;max-width:720px;}}@media (min-width:1440px){.sizeLarge .css-17ai7jg{margin-left:0;max-width:900px;}}@media (min-width:740px){.sizeLarge.layoutVertical .css-17ai7jg{margin-left:0;}}.sizeFull .css-17ai7jg{margin-left:20px;}@media (min-width:600px){.sizeFull .css-17ai7jg{max-width:900px;}}@media (min-width:740px){.sizeFull .css-17ai7jg{max-width:900px;}}@media (min-width:1440px){.sizeFull .css-17ai7jg{max-width:900px;}}@media print{.css-17ai7jg{display:none;}}.css-8i9d0s{margin-right:7px;color:#666;font-family:nyt-imperial,georgia,'times new roman',times,serif;font-size:0.875rem;line-height:1.125rem;}@media (min-width:740px){.css-8i9d0s{font-size:0.9375rem;line-height:1.25rem;}}.css-8i9d0s strong{font-weight:700;}.css-8i9d0s em{font-style:italic;}.css-8i9d0s a{color:#326891;}.css-8i9d0s a:visited{color:#326891;}.css-1nwzsjy{display:inline-block;color:#888;font-family:nyt-imperial,georgia,'times new roman',times,serif;line-height:1.125rem;-webkit-letter-spacing:0.01em;-moz-letter-spacing:0.01em;-ms-letter-spacing:0.01em;letter-spacing:0.01em;font-size:0.75rem;}@media (min-width:740px){.css-1nwzsjy{font-size:0.75rem;}}@media (min-width:1150px){.css-1nwzsjy{font-size:0.8125rem;}}@media (min-width:600px){.sizeSmall.sizeSmallNoCaption .css-1nwzsjy{margin-left:5px;}}@media (min-width:1024px){.sizeSmall.sizeSmallNoCaption .css-1nwzsjy{margin-left:5px;}}@media (min-width:1440px){.sizeSmall.sizeSmallNoCaption .css-1nwzsjy{margin-left:40px;}}@media (max-width:600px){.sizeSmall.sizeSmallNoCaption .css-1nwzsjy{margin-left:-8px;}}@media print{.css-1nwzsjy{display:none;}}.css-10698na{text-align:center;}@media (min-width:740px){.css-10698na{padding-top:0;}}@media (min-width:1024px){}@media print{.css-10698na a[href]::after{content:'';}.css-10698na svg{fill:black;}}.css-nhjhh0{display:block;width:189px;height:26px;margin:5px auto 0;}@media (min-width:740px){.css-nhjhh0{width:225px;height:31px;margin:4px auto 0;}}@media (min-width:1024px){.css-nhjhh0{width:195px;height:26px;margin:6px auto 0;}}.css-1nuro5j{display:inline-block;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;font-size:0.875rem;line-height:1.125rem;margin:0;font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:700;color:#333;}@media (min-width:740px){.css-1nuro5j{font-size:0.9375rem;line-height:1.25rem;}}.css-1w5cs23{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:0;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;}.css-1w5cs23 li{list-style:none;}.css-4brsb6{display:inline-block;height:15px;vertical-align:middle;width:15px;background-color:#eee;border:1px #eee solid;border-radius:100%;padding:5px;width:15px;height:15px;}.css-4brsb6.facebook{background-image:url(/vi-assets/static-assets/icon-fb-circle-2ec7780140bd9e8e8398bbcdf5661569.svg);}.css-4brsb6.twitter{background-image:url(/vi-assets/static-assets/icon-twitter-circle-fc7c2748f5613c68963a0df203bffc05.svg);}.css-4brsb6.email{background-image:url(/vi-assets/static-assets/icon-share-email-circle-a2acce7e23b21d47bb606b628a6c7e34.svg);}.css-4brsb6.link{background-image:url(/vi-assets/static-assets/icon-share-permalink-circle-3ff3876a106221ee493d9542c0895863.svg);}.css-4brsb6.linkedin{background-image:url(/vi-assets/static-assets/icon-share-linkedin-circle-4d7bcf236c5f3a086738746f41f46f7b.svg);}.css-4brsb6.whatsapp{background-image:url(/vi-assets/static-assets/icon-whatsapp-video-a9503bf2b3c73111106c496d3ebc8c67.svg);}.css-4brsb6.reddit{background-image:url(/vi-assets/static-assets/icon-share-reddit-f882338200fd1971b767627fa5f60124.svg);}.css-4brsb6:hover{background-color:#fff;border:1px solid #ccc;}.css-uhuo44{display:inline-block;height:15px;vertical-align:middle;width:15px;bottom:5px;position:relative;width:15px;height:15px;bottom:4px;}.css-uhuo44.facebook{background-image:url(/vi-assets/static-assets/icon-fb-circle-2ec7780140bd9e8e8398bbcdf5661569.svg);}.css-uhuo44.twitter{background-image:url(/vi-assets/static-assets/icon-twitter-circle-fc7c2748f5613c68963a0df203bffc05.svg);}.css-uhuo44.email{background-image:url(/vi-assets/static-assets/icon-share-email-circle-a2acce7e23b21d47bb606b628a6c7e34.svg);}.css-uhuo44.link{background-image:url(/vi-assets/static-assets/icon-share-permalink-circle-3ff3876a106221ee493d9542c0895863.svg);}.css-uhuo44.linkedin{background-image:url(/vi-assets/static-assets/icon-share-linkedin-circle-4d7bcf236c5f3a086738746f41f46f7b.svg);}.css-uhuo44.whatsapp{background-image:url(/vi-assets/static-assets/icon-whatsapp-video-a9503bf2b3c73111106c496d3ebc8c67.svg);}.css-uhuo44.reddit{background-image:url(/vi-assets/static-assets/icon-share-reddit-f882338200fd1971b767627fa5f60124.svg);}.css-exrw3m{margin-bottom:0.78125rem;margin-top:0;font-family:nyt-imperial,georgia,'times new roman',times,serif;font-size:1.125rem;line-height:1.5625rem;margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;}@media (min-width:740px){.css-exrw3m{margin-bottom:0.9375rem;margin-top:0;}}.css-exrw3m .css-1g7m0tk{-webkit-text-decoration:underline;text-decoration:underline;}.css-exrw3m .css-1g7m0tk:hover,.css-exrw3m .css-1g7m0tk:focus{-webkit-text-decoration:none;text-decoration:none;}@media (min-width:740px){.css-exrw3m{font-size:1.25rem;line-height:1.875rem;}}.css-exrw3m:first-child{margin-top:0;}.css-exrw3m:last-child{margin-bottom:0;}.css-exrw3m.e1h9rw200:last-child{margin-bottom:0.75rem;}@media (min-width:600px){.css-exrw3m{margin-left:auto;margin-right:auto;}}@media (min-width:1024px){.css-exrw3m{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@media print{.css-exrw3m{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-1a48zt4{opacity:1;-webkit-transition:opacity 0.3s 0.2s;transition:opacity 0.3s 0.2s;}.css-vuqh7u{display:inline-block;color:#888;font-family:nyt-imperial,georgia,'times new roman',times,serif;line-height:1.125rem;-webkit-letter-spacing:0.01em;-moz-letter-spacing:0.01em;-ms-letter-spacing:0.01em;letter-spacing:0.01em;font-size:0.75rem;}@media (min-width:740px){.css-vuqh7u{font-size:0.75rem;}}@media (min-width:1150px){.css-vuqh7u{font-size:0.8125rem;}}.css-1l44abu{font-family:nyt-imperial,georgia,'times new roman',times,serif;color:#666;margin:10px 20px 0 20px;text-align:left;}.css-1l44abu a{color:#326891;-webkit-text-decoration:none;text-decoration:none;}.css-1l44abu a:hover,.css-1l44abu a:focus{-webkit-text-decoration:underline;text-decoration:underline;}@media (min-width:600px){.css-1l44abu{margin-left:0;margin-right:20px;}}@media (min-width:1440px){.css-1l44abu{max-width:px;}}.css-jcw7oy{width:100%;max-width:600px;margin:2.3125rem auto;}@media (min-width:600px){.css-jcw7oy{width:calc(100% - 40px);}}@media (min-width:740px){.css-jcw7oy{width:auto;max-width:600px;}}@media (min-width:1440px){.css-jcw7oy{max-width:720px;}}.css-jcw7oy strong{font-weight:700;}.css-jcw7oy em{font-style:italic;}@media (min-width:740px){.css-jcw7oy{margin:2.6875rem auto;}}.css-10raysz{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;width:calc(100% - 40px);max-width:600px;padding:0 1rem 0 0;}.css-10raysz a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-10raysz:nth-of-type(3),.css-10raysz:nth-of-type(4){display:none;}}.css-10raysz:last-of-type{margin-right:0;}.css-10raysz:nth-of-type(1),.css-10raysz:nth-of-type(2),.css-10raysz:nth-of-type(3),.css-10raysz:nth-of-type(4){display:inline;}@media (min-width:600px){.css-10raysz{width:330px;}}.css-ar1l6a{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;}.css-ar1l6a a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-ar1l6a:nth-of-type(3),.css-ar1l6a:nth-of-type(4){display:none;}}.css-ar1l6a:last-of-type{margin-right:0;}.css-ar1l6a:nth-of-type(1),.css-ar1l6a:nth-of-type(2),.css-ar1l6a:nth-of-type(3),.css-ar1l6a:nth-of-type(4){display:inline;}.css-1ede5it{background-color:#f7f7f7;border-bottom:1px solid #f3f3f3;border-top:1px solid #f3f3f3;margin:37px auto;padding-bottom:30px;padding-top:12px;text-align:center;margin-top:60px;}@media (min-width:740px){.css-1ede5it{margin:43px auto;}}@media print{.css-1ede5it{display:none;}}@media (min-width:740px){.css-1ede5it{margin-bottom:0;margin-top:0;}}.css-mn5hq9{cursor:pointer;margin:0;border-top:1px solid #ebebeb;color:#333;font-family:nyt-franklin;font-size:13px;font-weight:700;height:44px;-webkit-letter-spacing:0.04rem;-moz-letter-spacing:0.04rem;-ms-letter-spacing:0.04rem;letter-spacing:0.04rem;line-height:44px;text-transform:uppercase;}.accordionExpanded .css-mn5hq9{color:#b3b3b3;}.css-1qmnftd{font-size:11px;text-align:center;}@media (max-width:600px){.css-1qmnftd{padding-bottom:25px;}}@media (min-width:600px){.css-1qmnftd{padding-bottom:25px;}}@media (min-width:1024px){.css-1qmnftd{padding:0 3% 9px;}}@media (min-width:1150px){.css-1qmnftd{margin:0 auto;max-width:1200px;}}.NYTApp .css-1qmnftd{display:none;}@media print{.css-1qmnftd{display:none;}}.css-1ho5u4o{list-style:none;margin:0 0 15px;padding:0;}@media (min-width:600px){.css-1ho5u4o{display:inline-block;}}.css-13o0c9t{list-style:none;line-height:8px;margin:0 0 35px;padding:0;}@media (min-width:600px){.css-13o0c9t{display:inline-block;}}.css-1yo489b{display:inline-block;line-height:20px;padding:0 10px;}.css-1yo489b:first-child{border-left:none;}.css-1yo489b.desktop{display:none;}@media (min-width:740px){.css-1yo489b.smartphone{display:none;}.css-1yo489b.desktop{display:inline-block;}}.css-ulr03x{opacity:1;visibility:visible;-webkit-animation-name:animation-5j8bii;animation-name:animation-5j8bii;-webkit-animation-duration:300ms;animation-duration:300ms;-webkit-animation-delay:0ms;animation-delay:0ms;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out;}@media print{.css-ulr03x{margin-bottom:15px;}}@media (min-width:1024px){.css-ulr03x{position:fixed;width:100%;top:0;left:0;z-index:200;background-color:#fff;border-bottom:none;-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;}}@media (min-width:1024px){.css-1bymuyk{position:relative;border-bottom:1px solid #e2e2e2;}}.css-1waixk9{background:#fff;border-bottom:1px solid #e2e2e2;height:36px;padding:8px 15px 3px;position:relative;}@media (min-width:740px){.css-1waixk9{background:#fff;padding:10px 15px 6px;}}@media (min-width:1024px){.css-1waixk9{background:transparent;border-bottom:0;padding:4px 15px 2px;}}@media print{.css-1waixk9{background:transparent;}}@media (min-width:740px){}@media (min-width:1024px){.css-1waixk9{margin:0 auto;max-width:1605px;}}.css-1f7ibof{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:space-around;-webkit-justify-content:space-around;-ms-flex-pack:space-around;justify-content:space-around;left:10px;position:absolute;}@media (min-width:1024px){}@media print{.css-1f7ibof{display:none;}}.css-l2ztic{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:transparent;color:#000;font-size:11px;font-weight:700;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;padding:7px 9px 9px;border:0;padding:8px 9px;text-transform:uppercase;}.css-l2ztic.hidden{opacity:0;visibility:hidden;}.css-l2ztic.hidden:focus{opacity:1;}.css-l2ztic::-moz-focus-inner{padding:0;border:0;}.css-l2ztic:-moz-focusring{outline:1px dotted;}.css-l2ztic:disabled,.css-l2ztic.disabled{opacity:0.5;cursor:default;}.css-l2ztic:active,.css-l2ztic.active{background-color:#f7f7f7;}@media (min-width:740px){.css-l2ztic:hover{background-color:#f7f7f7;}}@media (min-width:1024px){.css-l2ztic{display:none;}}.css-19lv58h{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;-webkit-appearance:button;-moz-appearance:button;appearance:button;background-color:#fff;border:1px solid #ebebeb;color:#333;display:inline-block;font-size:11px;font-weight:500;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;line-height:13px;margin:0;padding:8px 9px;text-transform:uppercase;vertical-align:middle;display:none;}.css-19lv58h::-moz-focus-inner{padding:0;border:0;}.css-19lv58h:-moz-focusring{outline:1px dotted;}.css-19lv58h:disabled,.css-19lv58h.disabled{opacity:0.5;cursor:default;}.css-19lv58h:active,.css-19lv58h.active{background-color:#f7f7f7;}@media (min-width:740px){.css-19lv58h:hover{background-color:#f7f7f7;}}@media (min-width:1024px){.css-19lv58h{border:0;display:inline-block;margin-right:8px;}}.css-mgtjo2{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:transparent;color:#000;font-size:11px;font-weight:700;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;padding:7px 9px 9px;border:0;}.css-mgtjo2::-moz-focus-inner{padding:0;border:0;}.css-mgtjo2:-moz-focusring{outline:1px dotted;}.css-mgtjo2:disabled,.css-mgtjo2.disabled{opacity:0.5;cursor:default;}.css-mgtjo2:active,.css-mgtjo2.active{background-color:#f7f7f7;}@media (min-width:740px){.css-mgtjo2:hover{background-color:#f7f7f7;}}.css-mgtjo2.activeSearchButton{background-color:#f7f7f7;}@media (min-width:1024px){.css-mgtjo2{padding:8px 9px 9px;}}.css-1wr3we4{display:none;}@media (min-width:1024px){.css-1wr3we4{display:block;position:absolute;left:105px;line-height:19px;top:10px;}}.css-y3sf94{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:space-around;-webkit-justify-content:space-around;-ms-flex-pack:space-around;justify-content:space-around;position:absolute;right:10px;top:9px;}@media (min-width:1024px){.css-y3sf94{top:4px;}}@media print{.css-y3sf94{display:none;}}.css-1bnxwmn{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:#6288a5;border:1px solid #326891;color:#fff;font-size:11px;font-weight:700;-webkit-letter-spacing:0.05em;-moz-letter-spacing:0.05em;-ms-letter-spacing:0.05em;letter-spacing:0.05em;line-height:11px;padding:8px 9px 6px;text-transform:uppercase;}.css-1bnxwmn::-moz-focus-inner{padding:0;border:0;}.css-1bnxwmn:-moz-focusring{outline:1px dotted;}.css-1bnxwmn:disabled,.css-1bnxwmn.disabled{opacity:0.5;cursor:default;}@media (min-width:740px){.css-1bnxwmn:hover{background-color:#326891;}}@media (min-width:1024px){.css-1bnxwmn{padding:11px 12px 8px;}}.css-1bnxwmn:hover{border:1px solid #326891;}.css-1i8g3m4{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:transparent;color:#000;font-size:11px;font-weight:700;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;padding:7px 9px 9px;border:0;display:block;}.css-1i8g3m4.hidden{opacity:0;visibility:hidden;}.css-1i8g3m4.hidden:focus{opacity:1;}.css-1i8g3m4::-moz-focus-inner{padding:0;border:0;}.css-1i8g3m4:-moz-focusring{outline:1px dotted;}.css-1i8g3m4:disabled,.css-1i8g3m4.disabled{opacity:0.5;cursor:default;}.css-1i8g3m4:active,.css-1i8g3m4.active{background-color:#f7f7f7;}@media (min-width:740px){.css-1i8g3m4:hover{background-color:#f7f7f7;}}@media (min-width:740px){.css-1i8g3m4{border:none;line-height:13px;padding:9px 9px 12px;}}@media (min-width:1024px){.css-1i8g3m4{display:none;}}@media (min-width:1150px){}.css-3qijnq{-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:11px;-webkit-box-pack:space-around;-webkit-justify-content:space-around;-ms-flex-pack:space-around;justify-content:space-around;padding:13px 20px 12px;}@media (min-width:740px){.css-3qijnq{position:relative;}}@media (min-width:1024px){.css-3qijnq{-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;border:none;padding:0;height:0;-webkit-transform:translateY(42px);-ms-transform:translateY(42px);transform:translateY(42px);}}@media print{.css-3qijnq{display:none;}}.css-uqyvli{color:#121212;font-size:13px;font-family:nyt-franklin,helvetica,arial,sans-serif;display:none;width:auto;}@media (min-width:740px){.css-uqyvli{text-align:center;width:100%;}}@media (min-width:1024px){.css-uqyvli{font-size:12px;margin-bottom:10px;width:auto;}}.css-1uqjmks{color:#121212;font-size:12px;font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:500;display:none;}@media (min-width:740px){.css-1uqjmks{margin:0;position:absolute;left:20px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;top:0;bottom:0;}}@media (min-width:1024px){.css-1uqjmks{display:none;}}.css-1bvtpon{display:none;}@media (min-width:1024px){}.css-1vxca1d{position:relative;margin:0 auto;}@media (min-width:600px){.css-1vxca1d{margin:0 auto 20px;}}.css-1vxca1d .relatedcoverage + .recirculation{margin-top:20px;}.css-1vxca1d .wrap + .recirculation{margin-top:20px;}@media (min-width:1024px){.css-1vxca1d{padding-top:40px;}}.css-1ox9jel{margin:37px auto;margin-top:20px;margin-bottom:32px;}.css-1ox9jel strong{font-weight:700;}.css-1ox9jel em{font-style:italic;}.css-1ox9jel.sizeSmall{width:calc(100% - 40px);display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}@media (min-width:600px){.css-1ox9jel.sizeSmall{max-width:600px;margin-left:auto;margin-right:auto;}}@media (min-width:1024px){.css-1ox9jel.sizeSmall{width:100%;}}@media (min-width:1440px){.css-1ox9jel.sizeSmall{max-width:600px;}}.css-1ox9jel.sizeSmall.sizeSmallNoCaption{display:block;}@media print{.css-1ox9jel.sizeSmall.sizeSmallNoCaption{display:none;}}.css-1ox9jel.sizeMedium{width:100%;max-width:600px;margin-right:auto;margin-left:auto;}@media (min-width:600px){.css-1ox9jel.sizeMedium{width:calc(100% - 40px);}}@media (min-width:740px){.css-1ox9jel.sizeMedium{max-width:600px;}}@media (min-width:1440px){.css-1ox9jel.sizeMedium{max-width:720px;}}@media (min-width:600px){.css-1ox9jel.sizeMedium.layoutVertical{width:420px;}}@media (min-width:1440px){.css-1ox9jel.sizeMedium.layoutVertical{width:480px;}}.css-1ox9jel.sizeMedium.layoutVertical.verticalVideo{width:calc(100% - 40px);}@media (min-width:600px){.css-1ox9jel.sizeMedium.layoutVertical.verticalVideo{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:600px;}}@media (min-width:1440px){.css-1ox9jel.sizeMedium.layoutVertical.verticalVideo{width:600px;}}.css-1ox9jel.sizeLarge{width:100%;max-width:1200px;margin-left:auto;margin-right:auto;}@media (min-width:600px){.css-1ox9jel.sizeLarge{width:auto;}}@media (min-width:740px){.css-1ox9jel.sizeLarge.layoutVertical{width:600px;}.css-1ox9jel.sizeLarge.layoutVertical.verticalVideo{width:600px;}}@media (min-width:1024px){.css-1ox9jel.sizeLarge{width:945px;}}@media (min-width:1440px){.css-1ox9jel.sizeLarge{width:1200px;}.css-1ox9jel.sizeLarge.layoutVertical{width:720px;}.css-1ox9jel.sizeLarge.layoutVertical.verticalVideo{width:600px;}}@media (min-width:600px){.css-1ox9jel{margin:43px auto;}}@media print{.css-1ox9jel{display:none;}}@media (min-width:740px){.css-1ox9jel{margin-top:25px;}}.css-1riqqik{display:inline;color:#333;}.css-1riqqik span{-webkit-text-decoration:underline;text-decoration:underline;-webkit-text-decoration-color:#ccc;text-decoration-color:#ccc;}.css-1riqqik span:hover,.css-1riqqik span:focus{-webkit-text-decoration:none;text-decoration:none;}.css-2fg4z9{font-style:italic;}.css-11n4cex{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-bottom:15px;margin-top:4px;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;}@media (min-width:600px){.css-11n4cex{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-11n4cex{width:600px;}}.css-11n4cex .e6idgb70{font-size:14px;margin:0;}.css-1ifw933{font-style:normal;font-stretch:normal;margin-bottom:1.6rem;color:#333;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;font-weight:300;-webkit-letter-spacing:0.005em;-moz-letter-spacing:0.005em;-ms-letter-spacing:0.005em;letter-spacing:0.005em;font-size:1.3125rem;line-height:1.6875rem;}@media (min-width:740px){.css-1ifw933{font-size:1.5rem;line-height:1.9375rem;}}.css-1rjmmt7{width:50px;vertical-align:bottom;margin-right:10px;}.css-rqb9bm{font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:500;color:#333;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;font-size:0.875rem;line-height:1.125rem;margin-bottom:1rem;}.css-19hdyf3{font-family:nyt-franklin,helvetica,arial,sans-serif;color:#333;font-size:0.9375rem;line-height:1.25rem;font-family:nyt-franklin,helvetica,arial,sans-serif;color:#333;font-size:0.9375rem;line-height:1.25rem;}.css-19hdyf3 p{margin-bottom:0.75rem;}.css-19hdyf3 a,.css-19hdyf3 a:visited{color:#326891;-webkit-text-decoration:underline;text-decoration:underline;}.css-19hdyf3 a:hover,.css-19hdyf3 a:focus{color:#326891;-webkit-text-decoration:none;text-decoration:none;}@media print{.css-19hdyf3{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@media (min-width:740px){.css-19hdyf3{font-size:1rem;line-height:1.375rem;}}.css-19hdyf3 p{margin-bottom:0.75rem;}.css-19hdyf3 a,.css-19hdyf3 a:visited{color:#326891;-webkit-text-decoration:underline;text-decoration:underline;}.css-19hdyf3 a:hover,.css-19hdyf3 a:focus{color:#326891;-webkit-text-decoration:none;text-decoration:none;}@media print{.css-19hdyf3{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-15g2oxy{margin-top:1rem;}.css-2b3w4o{margin-bottom:1rem;}.css-2b3w4o .e16638kd0{margin-top:5px;margin-bottom:0;display:inline-block;color:#999;font-size:0.75rem;line-height:1.0625rem;}.css-2b3w4o:hover .e16ij5yr2,.css-2b3w4o:visited .e16ij5yr2{color:#666;}.css-2b3w4o .css-1g7m0tk{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;min-height:100px;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}.css-14b9hti{font-weight:500;color:#a19d9d;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;font-size:1.125rem;line-height:1.375rem;}@media (min-width:740px){.css-14b9hti{font-size:1.1875rem;line-height:1.4375rem;}}.css-1j8dw05{margin-right:10px;display:inline;font-weight:500;color:#121212;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;font-size:1.125rem;line-height:1.375rem;}@media (min-width:740px){.css-1j8dw05{font-size:1.1875rem;line-height:1.4375rem;}}.css-1vm5oi9{margin-left:10px;width:120px;min-width:120px;}@media (min-width:740px){.css-1vm5oi9{width:165px;min-width:165px;}}.css-32rbo2{width:100%;min-width:120px;}.css-llk6mt{margin-top:5px;}@media (min-width:740px){.css-llk6mt{margin-top:45px;margin-bottom:0;}}.css-llk6mt .e6idgb70{margin-top:1.875rem;color:#121212;font-weight:700;line-height:0.75rem;margin-bottom:0.625rem;}@media print{.css-llk6mt .e6idgb70{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-llk6mt .e1h9rw200{margin-bottom:16px;margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;margin-top:0;}@media (min-width:600px){.css-llk6mt .e1h9rw200{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-llk6mt .e1h9rw200{width:600px;}}@media (min-width:740px){.css-llk6mt .e1h9rw200{max-width:none;margin-left:calc((100% - 600px) / 2);margin-right:auto;position:relative;width:660px;}}.css-llk6mt .euiyums3 .e6idgb70{margin:0;}.css-llk6mt .e1wiw3jv0{color:#333;margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;}@media (min-width:600px){.css-llk6mt .e1wiw3jv0{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-llk6mt .e1wiw3jv0{width:600px;}}.css-llk6mt .e16638kd0{width:auto;margin-bottom:0;margin-left:0;display:inline-block;margin-top:0;margin-bottom:0;width:auto;}.css-llk6mt .eatfx1z0{margin-right:15px;font-weight:700;font-size:14px;line-height:1;}.css-llk6mt .section-kicker .opinion-bar{font-size:25px;}.css-llk6mt .epjyd6m0{margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;}@media (min-width:600px){.css-llk6mt .epjyd6m0{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-llk6mt .epjyd6m0{width:600px;}}.css-llk6mt .e1g7ppur0{margin-bottom:32px;margin-top:20px;}@media (min-width:740px){.css-llk6mt .e1g7ppur0{margin-top:25px;}}@media (min-width:1024px){.css-llk6mt .e1g7ppur0{margin-bottom:43px;}}.css-llk6mt .e1q76eii0{margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;max-width:600px;}@media (min-width:600px){.css-llk6mt .e1q76eii0{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-llk6mt .e1q76eii0{width:600px;}}.css-llk6mt .euiyums1{margin-bottom:20px;color:#121212;}@media print{.css-llk6mt .euiyums1{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-1s4ffep{color:#121212;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;font-weight:700;font-style:italic;font-size:1.9375rem;line-height:2.25rem;text-align:left;}@media (min-width:740px){.css-1s4ffep{font-size:2.5rem;line-height:3rem;}}.css-pdw9fk{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:15px;width:100%;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;}@media (min-width:1024px){.css-pdw9fk{-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;}}.css-pdw9fk > img,.css-pdw9fk a > img,.css-pdw9fk div > img{margin-right:10px;}.css-1txwxcy{min-width:100px;display:block;width:100%;margin-bottom:10px;margin-left:-3px;}@media (min-width:1024px){.css-1txwxcy{display:inline-block;width:auto;margin-bottom:0;}}.css-1soubk3{margin:0.5rem 0 1.5rem;padding-top:1rem;width:calc(100% - 40px);max-width:600px;margin-left:20px;margin-right:20px;}.css-1soubk3:before{content:'';display:block;width:100%;margin-bottom:0.5rem;border-bottom:1px solid #e2e2e2;}.css-1soubk3:after{content:'';display:block;width:100%;margin-top:0.5rem;border-bottom:1px solid #e2e2e2;}.css-1soubk3 .e16ij5yr6{border-top:none;}@media (min-width:600px){.css-1soubk3{margin-left:auto;margin-right:auto;}}@media (min-width:1024px){.css-1soubk3{width:600px;}}@media (min-width:1440px){.css-1soubk3{width:600px;max-width:600px;}}@media print{.css-1soubk3{margin-left:0;margin-right:0;width:100%;max-width:100%;}}</style>
+
+
+
+ <style>[data-timezone] { display: none }</style>
+
+ </head>
+ <body>
+ <div id="app"><div class="css-v89234" role="main"><div class=""><div><div class="css-ulr03x e1suatyy0"><header class="css-1bymuyk e1suatyy1"><section class="css-1waixk9 e1suatyy2"><div class="css-1f7ibof er09x8g0"><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="Sections Navigation &amp; Search" class="er09x8g1 css-l2ztic" data-testid="nav-button"><svg class="css-1fe7a5q" viewBox="0 0 16 16"><rect x="1" y="3" fill="#333333" width="14" height="2"></rect><rect x="1" y="7" fill="#333333" width="14" height="2"></rect><rect x="1" y="11" fill="#333333" width="14" height="2"></rect></svg></button></div><button id="desktop-sections-button" aria-label="Sections Navigation" class="css-19lv58h er09x8g2"><span class="css-vz7hjd">Sections</span><svg class="css-1fe7a5q" viewBox="0 0 16 16"><rect x="1" y="3" fill="#333333" width="14" height="2"></rect><rect x="1" y="7" fill="#333333" width="14" height="2"></rect><rect x="1" y="11" fill="#333333" width="14" height="2"></rect></svg></button><div class="css-10488qs"><button class="css-mgtjo2 ewfai8r0" data-test-id="search-button"><span class="css-vz7hjd">SEARCH</span><svg class="css-1fe7a5q" viewBox="0 0 16 16"><path fill="#333" d="M11.3,9.2C11.7,8.4,12,7.5,12,6.5C12,3.5,9.5,1,6.5,1S1,3.5,1,6.5S3.5,12,6.5,12c1,0,1.9-0.3,2.7-0.7l3.3,3.3c0.3,0.3,0.7,0.4,1.1,0.4s0.8-0.1,1.1-0.4c0.6-0.6,0.6-1.5,0-2.1L11.3,9.2zM6.5,10.3c-2.1,0-3.8-1.7-3.8-3.8c0-2.1,1.7-3.8,3.8-3.8c2.1,0,3.8,1.7,3.8,3.8C10.3,8.6,8.6,10.3,6.5,10.3z"></path></svg></button></div><a class="css-1rn5q1r" href="#site-content">Skip to content</a><a class="css-1rn5q1r" href="#site-index">Skip to site index</a></div><div class="css-1wr3we4 eaxe0e00"><a href="https://www.nytimes.com/section/nyregion" class="css-nuvmzp">New York</a></div><div class="css-10698na e1huz5gh0"><a aria-label="New York Times Logo. Click to visit the homepage" class="css-nhjhh0 e1huz5gh1" href="/"><svg xmlns="http://www.w3.org/2000/svg" class="" viewBox="0 0 184 25" fill="#000"><path d="M13.8 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8C6.2 1.4 5 1 4 1 2 1 .6 2.5.6 4.2c0 1.5 1.1 2 1.5 2.2l.1-.2c-.2-.2-.5-.4-.5-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8v3.1L9 10.2v.1l1.5 1.3v4.3c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2C3.6 6.9 4.7 6 5.8 5.4l-.1-.3c-3 .8-5.7 3.6-5.7 7 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1l-1.6-1.3V5.8c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7 0-1.5.2-2.1l2.1-.9v6.2zm10.6 2.3l-1.3 1 .2.2.6-.5 2.2 2 3-2-.1-.2-.8.5-1-1V9.4l.8-.6 1.7 1.4v6.1c0 3.8-.8 4.4-2.5 5v.3c2.8.1 5.4-.8 5.4-5.7V9.3l.9-.7-.2-.2-.8.6-2.5-2.1L18.5 9V.8h-.2l-3.5 2.4v.2c.4.2 1 .4 1 1.5l-.1 11.3zM34 15.1L31.5 17 29 15v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM53.1 2c0-.3-.1-.6-.2-.9h-.2c-.3.8-.7 1.2-1.7 1.2-.9 0-1.5-.5-1.9-.9l-2.9 3.3.2.2 1-.9c.6.5 1.1.9 2.5 1v8.3L44 3.2c-.5-.8-1.2-1.9-2.6-1.9-1.6 0-3 1.4-2.8 3.6h.3c.1-.6.4-1.3 1.1-1.3.5 0 1 .5 1.3 1v3.3c-1.8 0-3 .8-3 2.3 0 .8.4 2 1.6 2.3v-.2c-.2-.2-.3-.4-.3-.7 0-.5.4-.9 1.1-.9h.5v4.2c-2.1 0-3.8 1.2-3.8 3.2 0 1.9 1.6 2.8 3.4 2.7v-.2c-1.1-.1-1.6-.6-1.6-1.3 0-.9.6-1.3 1.4-1.3.8 0 1.5.5 2 1.1l2.9-3.2-.2-.2-.7.8c-1.1-1-1.7-1.3-3-1.5V5l8 14h.6V5c1.5-.1 2.9-1.3 2.9-3zm7.3 13.1L57.9 17l-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM76.7 8l-.7.5-1.9-1.6-2.2 2 .9.9v7.5l-2.4-1.5V9.6l.8-.5-2.3-2.2-2.2 2 .9.9V17l-.3.2-2.1-1.5v-6c0-1.4-.7-1.8-1.5-2.3-.7-.5-1.1-.8-1.1-1.5 0-.6.6-.9.9-1.1v-.2c-.8 0-2.9.8-2.9 2.7 0 1 .5 1.4 1 1.9s1 .9 1 1.8v5.8l-1.1.8.2.2 1-.8 2.3 2 2.5-1.7 2.8 1.7 5.3-3.1V9.2l1.3-1-.2-.2zm18.6-5.5l-1 .9-2.2-2-3.3 2.4V1.6h-.3l.1 16.2c-.3 0-1.2-.2-1.9-.4l-.2-13.5c0-1-.7-2.4-2.5-2.4s-3 1.4-3 2.8h.3c.1-.6.4-1.1 1-1.1s1.1.4 1.1 1.7v3.9c-1.8.1-2.9 1.1-2.9 2.4 0 .8.4 2 1.6 2V13c-.4-.2-.5-.5-.5-.7 0-.6.5-.8 1.3-.8h.4v6.2c-1.5.5-2.1 1.6-2.1 2.8 0 1.7 1.3 2.9 3.3 2.9 1.4 0 2.6-.2 3.8-.5 1-.2 2.3-.5 2.9-.5.8 0 1.1.4 1.1.9 0 .7-.3 1-.7 1.1v.2c1.6-.3 2.6-1.3 2.6-2.8s-1.5-2.4-3.1-2.4c-.8 0-2.5.3-3.7.5-1.4.3-2.8.5-3.2.5-.7 0-1.5-.3-1.5-1.3 0-.8.7-1.5 2.4-1.5.9 0 2 .1 3.1.4 1.2.3 2.3.6 3.3.6 1.5 0 2.8-.5 2.8-2.6V3.7l1.2-1-.2-.2zm-4.1 6.1c-.3.3-.7.6-1.2.6s-1-.3-1.2-.6V4.2l1-.7 1.4 1.3v3.8zm0 3c-.2-.2-.7-.5-1.2-.5s-1 .3-1.2.5V9c.2.2.7.5 1.2.5s1-.3 1.2-.5v2.6zm0 4.7c0 .8-.5 1.6-1.6 1.6h-.8V12c.2-.2.7-.5 1.2-.5s.9.3 1.2.5v4.3zm13.7-7.1l-3.2-2.3-4.9 2.8v6.5l-1 .8.1.2.8-.6 3.2 2.4 5-3V9.2zm-5.4 6.3V8.3l2.5 1.8v7.1l-2.5-1.7zm14.9-8.4h-.2c-.3.2-.6.4-.9.4-.4 0-.9-.2-1.1-.5h-.2l-1.7 1.9-1.7-1.9-3 2 .1.2.8-.5 1 1.1v6.3l-1.3 1 .2.2.6-.5 2.4 2 3.1-2.1-.1-.2-.9.5-1.2-1V9c.5.5 1.1 1 1.8 1 1.4.1 2.2-1.3 2.3-2.9zm12 9.6L123 19l-4.6-7 3.3-5.1h.2c.4.4 1 .8 1.7.8s1.2-.4 1.5-.8h.2c-.1 2-1.5 3.2-2.5 3.2s-1.5-.5-2.1-.8l-.3.5 5 7.4 1-.6v.1zm-11-.5l-1.3 1 .2.2.6-.5 2.2 2 3-2-.2-.2-.8.5-1-1V.8h-.1l-3.6 2.4v.2c.4.2 1 .3 1 1.5v11.3zM143 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8-1.3-.4-2.5-.8-3.5-.8-2 0-3.4 1.5-3.4 3.2 0 1.5 1.1 2 1.5 2.2l.1-.2c-.3-.2-.6-.4-.6-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8V9l-1.5 1.3v.1l1.5 1.3V16c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2c.5-1.3 1.6-2.2 2.6-2.9l-.1-.2c-3 .8-5.7 3.5-5.7 6.9 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1L140 8.8v-3c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7.1-1.5.3-2.1l2.1-.9-.1 6.2zm12.2-12h-.1l-2 1.7v.1l1.7 1.9h.2l2-1.7v-.1l-1.8-1.9zm3 14.8l-.8.5-1-1V9.3l1-.7-.2-.2-.7.6-1.8-2.1-2.9 2 .2.3.7-.5.9 1.1v6.5l-1.3 1 .1.2.7-.5 2.2 2 3-2-.1-.3zm16.7-.1l-.7.5-1.1-1V9.3l1-.8-.2-.2-.8.7-2.3-2.1-3 2.1-2.3-2.1L154 9l-1.8-2.1-2.9 2 .1.3.7-.5 1 1.1v6.5l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.9-.6 1.5 1.4v6l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.8-.5 1.6 1.4v6l-.7.7 2.3 2.1 3.1-2.1v-.3zm8.7-1.5l-2.5 1.9-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.8l3.5 2.5 4.5-3.6-.1-.3zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zm14.1-.9l-1.9-1.5c1.3-1.1 1.8-2.6 1.8-3.6v-.6h-.2c-.2.5-.6 1-1.4 1-.8 0-1.3-.4-1.8-1L176 9.3v3.6l1.7 1.3c-1.7 1.5-2 2.5-2 3.3 0 1 .5 1.7 1.3 2l.1-.2c-.2-.2-.4-.3-.4-.8 0-.3.4-.8 1.2-.8 1 0 1.6.7 1.9 1l4.3-2.6v-3.6h-.1zm-1.1-3c-.7 1.2-2.2 2.4-3.1 3l-1.1-.9V8.1c.4 1 1.5 1.8 2.6 1.8.7 0 1.1-.1 1.6-.4zm-1.7 8c-.5-1.1-1.7-1.9-2.9-1.9-.3 0-1.1 0-1.9.5.5-.8 1.8-2.2 3.5-3.2l1.2 1 .1 3.6z"></path></svg></a></div><div class="css-y3sf94 ez4a0qj1"><a href="https://myaccount.nytimes.com/auth/login?response_type=cookie&amp;client_id=vi" class="css-1kj7lfb"><button class="css-1bnxwmn ez4a0qj0" data-testid="login-button">Log In</button></a><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="User Settings" class="ez4a0qj4 css-1i8g3m4" data-testid="user-settings-button"><svg class="css-10m9xeu" viewBox="0 0 16 16" fill="#333"><path d="M8,10c-2.5,0-7,1.1-7,3.5V16h14v-2.5C15,11.1,10.5,10,8,10z"></path><circle cx="8" cy="4" r="4"></circle></svg></button></div></div></section><section class="hasLinks css-3qijnq e1csuq9d3"><div class="css-uqyvli e1csuq9d0"></div><div class="css-1uqjmks e1csuq9d1"></div><div class="css-9e9ivx"><a href="https://myaccount.nytimes.com/auth/login?response_type=cookie&amp;client_id=vi" class="css-1gz70xg">Log In</a></div><div class="css-1bvtpon e1csuq9d2"><a href="https://www.nytimes.com/section/todayspaper" class="css-2bwtzy">Today’s Paper</a></div></section></header></div></div><div aria-hidden="false"><main id="site-content"><div><div class="css-4g4cvq" style="opacity:0.000000001;z-index:-1;visibility:hidden"><div class="css-m6xlts"><div class="css-1ahhg7f"><span class="css-17xtcya"><a href="/section/nyregion">New York</a></span><span class="css-x15j1o">|</span><span class="css-fwqvlz">She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.</span></div><div class="css-k008qs"><div class="css-1iwv8en"><a href="/"><svg class="css-1ri25x2" viewBox="0 0 16 22"><path d="M15.863 13.08c-.687 1.818-1.923 3.147-3.64 3.916v-3.917l2.129-1.958-2.129-1.889V6.505c1.923-.14 3.228-1.609 3.228-3.358C15.45.84 13.32 0 12.086 0c-.275 0-.55 0-.962.14v.14h.481c.824 0 1.51.42 1.51 1.189 0 .63-.48 1.189-1.304 1.189-2.129 0-4.6-1.749-7.279-1.749C2.13.91.481 2.728.481 4.546c0 1.819 1.03 2.448 2.128 2.798v-.14c-.343-.21-.618-.63-.618-1.189 0-.84.756-1.469 1.648-1.469 2.267 0 5.906 1.959 8.172 1.959h.206v2.727l-2.129 1.889 2.13 1.958v3.987c-.894.35-1.786.49-2.748.49-3.502 0-5.768-2.169-5.768-5.806 0-.839.137-1.678.344-2.518l1.785-.769v7.973l3.57-1.608V6.575L3.984 8.953c.55-1.61 1.648-2.728 2.953-3.358v-.07C3.433 6.295 0 9.023 0 13.08c0 4.686 3.914 7.974 8.446 7.974 4.807 0 7.485-3.288 7.554-7.974h-.137z" fill="#000"></path></svg></a><span class="css-1hfdzay"><div><a href="/" aria-label="New York Times Logo. Click to visit the homepage"><svg xmlns="http://www.w3.org/2000/svg" class="css-12fr9lp" viewBox="0 0 184 25" fill="#000"><path d="M13.8 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8C6.2 1.4 5 1 4 1 2 1 .6 2.5.6 4.2c0 1.5 1.1 2 1.5 2.2l.1-.2c-.2-.2-.5-.4-.5-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8v3.1L9 10.2v.1l1.5 1.3v4.3c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2C3.6 6.9 4.7 6 5.8 5.4l-.1-.3c-3 .8-5.7 3.6-5.7 7 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1l-1.6-1.3V5.8c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7 0-1.5.2-2.1l2.1-.9v6.2zm10.6 2.3l-1.3 1 .2.2.6-.5 2.2 2 3-2-.1-.2-.8.5-1-1V9.4l.8-.6 1.7 1.4v6.1c0 3.8-.8 4.4-2.5 5v.3c2.8.1 5.4-.8 5.4-5.7V9.3l.9-.7-.2-.2-.8.6-2.5-2.1L18.5 9V.8h-.2l-3.5 2.4v.2c.4.2 1 .4 1 1.5l-.1 11.3zM34 15.1L31.5 17 29 15v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM53.1 2c0-.3-.1-.6-.2-.9h-.2c-.3.8-.7 1.2-1.7 1.2-.9 0-1.5-.5-1.9-.9l-2.9 3.3.2.2 1-.9c.6.5 1.1.9 2.5 1v8.3L44 3.2c-.5-.8-1.2-1.9-2.6-1.9-1.6 0-3 1.4-2.8 3.6h.3c.1-.6.4-1.3 1.1-1.3.5 0 1 .5 1.3 1v3.3c-1.8 0-3 .8-3 2.3 0 .8.4 2 1.6 2.3v-.2c-.2-.2-.3-.4-.3-.7 0-.5.4-.9 1.1-.9h.5v4.2c-2.1 0-3.8 1.2-3.8 3.2 0 1.9 1.6 2.8 3.4 2.7v-.2c-1.1-.1-1.6-.6-1.6-1.3 0-.9.6-1.3 1.4-1.3.8 0 1.5.5 2 1.1l2.9-3.2-.2-.2-.7.8c-1.1-1-1.7-1.3-3-1.5V5l8 14h.6V5c1.5-.1 2.9-1.3 2.9-3zm7.3 13.1L57.9 17l-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM76.7 8l-.7.5-1.9-1.6-2.2 2 .9.9v7.5l-2.4-1.5V9.6l.8-.5-2.3-2.2-2.2 2 .9.9V17l-.3.2-2.1-1.5v-6c0-1.4-.7-1.8-1.5-2.3-.7-.5-1.1-.8-1.1-1.5 0-.6.6-.9.9-1.1v-.2c-.8 0-2.9.8-2.9 2.7 0 1 .5 1.4 1 1.9s1 .9 1 1.8v5.8l-1.1.8.2.2 1-.8 2.3 2 2.5-1.7 2.8 1.7 5.3-3.1V9.2l1.3-1-.2-.2zm18.6-5.5l-1 .9-2.2-2-3.3 2.4V1.6h-.3l.1 16.2c-.3 0-1.2-.2-1.9-.4l-.2-13.5c0-1-.7-2.4-2.5-2.4s-3 1.4-3 2.8h.3c.1-.6.4-1.1 1-1.1s1.1.4 1.1 1.7v3.9c-1.8.1-2.9 1.1-2.9 2.4 0 .8.4 2 1.6 2V13c-.4-.2-.5-.5-.5-.7 0-.6.5-.8 1.3-.8h.4v6.2c-1.5.5-2.1 1.6-2.1 2.8 0 1.7 1.3 2.9 3.3 2.9 1.4 0 2.6-.2 3.8-.5 1-.2 2.3-.5 2.9-.5.8 0 1.1.4 1.1.9 0 .7-.3 1-.7 1.1v.2c1.6-.3 2.6-1.3 2.6-2.8s-1.5-2.4-3.1-2.4c-.8 0-2.5.3-3.7.5-1.4.3-2.8.5-3.2.5-.7 0-1.5-.3-1.5-1.3 0-.8.7-1.5 2.4-1.5.9 0 2 .1 3.1.4 1.2.3 2.3.6 3.3.6 1.5 0 2.8-.5 2.8-2.6V3.7l1.2-1-.2-.2zm-4.1 6.1c-.3.3-.7.6-1.2.6s-1-.3-1.2-.6V4.2l1-.7 1.4 1.3v3.8zm0 3c-.2-.2-.7-.5-1.2-.5s-1 .3-1.2.5V9c.2.2.7.5 1.2.5s1-.3 1.2-.5v2.6zm0 4.7c0 .8-.5 1.6-1.6 1.6h-.8V12c.2-.2.7-.5 1.2-.5s.9.3 1.2.5v4.3zm13.7-7.1l-3.2-2.3-4.9 2.8v6.5l-1 .8.1.2.8-.6 3.2 2.4 5-3V9.2zm-5.4 6.3V8.3l2.5 1.8v7.1l-2.5-1.7zm14.9-8.4h-.2c-.3.2-.6.4-.9.4-.4 0-.9-.2-1.1-.5h-.2l-1.7 1.9-1.7-1.9-3 2 .1.2.8-.5 1 1.1v6.3l-1.3 1 .2.2.6-.5 2.4 2 3.1-2.1-.1-.2-.9.5-1.2-1V9c.5.5 1.1 1 1.8 1 1.4.1 2.2-1.3 2.3-2.9zm12 9.6L123 19l-4.6-7 3.3-5.1h.2c.4.4 1 .8 1.7.8s1.2-.4 1.5-.8h.2c-.1 2-1.5 3.2-2.5 3.2s-1.5-.5-2.1-.8l-.3.5 5 7.4 1-.6v.1zm-11-.5l-1.3 1 .2.2.6-.5 2.2 2 3-2-.2-.2-.8.5-1-1V.8h-.1l-3.6 2.4v.2c.4.2 1 .3 1 1.5v11.3zM143 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8-1.3-.4-2.5-.8-3.5-.8-2 0-3.4 1.5-3.4 3.2 0 1.5 1.1 2 1.5 2.2l.1-.2c-.3-.2-.6-.4-.6-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8V9l-1.5 1.3v.1l1.5 1.3V16c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2c.5-1.3 1.6-2.2 2.6-2.9l-.1-.2c-3 .8-5.7 3.5-5.7 6.9 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1L140 8.8v-3c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7.1-1.5.3-2.1l2.1-.9-.1 6.2zm12.2-12h-.1l-2 1.7v.1l1.7 1.9h.2l2-1.7v-.1l-1.8-1.9zm3 14.8l-.8.5-1-1V9.3l1-.7-.2-.2-.7.6-1.8-2.1-2.9 2 .2.3.7-.5.9 1.1v6.5l-1.3 1 .1.2.7-.5 2.2 2 3-2-.1-.3zm16.7-.1l-.7.5-1.1-1V9.3l1-.8-.2-.2-.8.7-2.3-2.1-3 2.1-2.3-2.1L154 9l-1.8-2.1-2.9 2 .1.3.7-.5 1 1.1v6.5l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.9-.6 1.5 1.4v6l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.8-.5 1.6 1.4v6l-.7.7 2.3 2.1 3.1-2.1v-.3zm8.7-1.5l-2.5 1.9-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.8l3.5 2.5 4.5-3.6-.1-.3zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zm14.1-.9l-1.9-1.5c1.3-1.1 1.8-2.6 1.8-3.6v-.6h-.2c-.2.5-.6 1-1.4 1-.8 0-1.3-.4-1.8-1L176 9.3v3.6l1.7 1.3c-1.7 1.5-2 2.5-2 3.3 0 1 .5 1.7 1.3 2l.1-.2c-.2-.2-.4-.3-.4-.8 0-.3.4-.8 1.2-.8 1 0 1.6.7 1.9 1l4.3-2.6v-3.6h-.1zm-1.1-3c-.7 1.2-2.2 2.4-3.1 3l-1.1-.9V8.1c.4 1 1.5 1.8 2.6 1.8.7 0 1.1-.1 1.6-.4zm-1.7 8c-.5-1.1-1.7-1.9-2.9-1.9-.3 0-1.1 0-1.9.5.5-.8 1.8-2.2 3.5-3.2l1.2 1 .1 3.6z"></path></svg></a></div></span></div><div class="css-1705lsu"><div class=""><div role="toolbar" aria-label="Social Media Share buttons, Save button, and Comments Panel with current comment count" class="css-4skfbu" data-testid="share-tools"><ul class="css-y8aj3r"><li class="css-1fcn4th"><a href="https://www.facebook.com/dialog/feed?app_id=9869919170&amp;link=https%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html&amp;smid=fb-share&amp;name=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;redirect_uri=https%3A%2F%2Fwww.facebook.com%2F" target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook"><svg class="css-13zu7ev" viewBox="0 0 7 15" width="7" height="15"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.775 14.163V7.08h1.923l.255-2.441H4.775l.004-1.222c0-.636.06-.977.958-.977H6.94V0H5.016c-2.31 0-3.123 1.184-3.123 3.175V4.64H.453v2.44h1.44v7.083h2.882z" fill="#000"></path></svg></a></li><li class="css-1fcn4th"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fnyti.ms%2F2GEzuZ8&amp;text=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database." target="_blank" rel="noopener noreferrer" aria-label="Share on Twitter"><svg viewBox="0 0 13 10" class="css-13zu7ev" width="13" height="10"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.987 2.772l.025.425-.429-.052c-1.562-.2-2.927-.876-4.086-2.011L.93.571.784.987c-.309.927-.111 1.906.533 2.565.343.364.266.416-.327.2-.206-.07-.386-.122-.403-.096-.06.06.146.85.309 1.161.223.434.678.858 1.176 1.11l.42.199-.497.009c-.481 0-.498.008-.447.19.172.564.85 1.162 1.606 1.422l.532.182-.464.277a4.833 4.833 0 0 1-2.3.641c-.387.009-.704.044-.704.07 0 .086 1.047.572 1.657.762 1.828.564 4 .32 5.631-.641 1.159-.685 2.318-2.045 2.859-3.363.292-.702.583-1.984.583-2.6 0-.398.026-.45.507-.927.283-.277.55-.58.6-.667.087-.165.078-.165-.36-.018-.73.26-.832.226-.472-.164.266-.278.584-.78.584-.928 0-.026-.129.018-.275.096a4.79 4.79 0 0 1-.755.294l-.464.148-.42-.286C9.66.467 9.335.293 9.163.24 8.725.12 8.055.137 7.66.276c-1.074.39-1.752 1.395-1.674 2.496z" fill="#000"></path></svg></a></li><li class="css-1fcn4th"><a href="mailto:?subject=NYTimes.com%3A%20She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;body=From%20The%20New%20York%20Times%3A%0A%0AShe%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.%0A%0AWith%20little%20oversight%2C%20the%20N.Y.P.D.%20has%20been%20using%20powerful%20surveillance%20technology%20on%20photos%20of%20children%20and%20teenagers.%0A%0Ahttps%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html" target="_blank" rel="noopener noreferrer" aria-label="Email"><svg viewBox="0 0 15 9" class="css-13zu7ev" width="15" height="9"><path fill-rule="evenodd" clip-rule="evenodd" d="M.906 8.418V0L5.64 4.76.906 8.419zm13 0L9.174 4.761 13.906 0v8.418zM7.407 6.539l-1.13-1.137L.907 9h13l-5.37-3.598-1.13 1.137zM1.297 0h12.22l-6.11 5.095L1.297 0z" fill="#000"></path></svg></a></li><li class="css-1fcn4th"><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="More sharing options" class="css-16ogagc" data-testid=""><svg class="css-f7l8cz" viewBox="0 0 16 13" width="16" height="13"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.406 5.359L8.978 0v3.215C3.82 3.215.406 8.107.406 12.66 1.653 9.133 4.29 7.517 8.978 7.517v3.2l6.428-5.358z" fill="#000"></path></svg></button></div></li><li class="css-60hakz"></li><li class="css-l72opv"></li></ul></div></div></div></div></div></div><meta itemProp="isAccessibleForFree" content="false"/><span itemProp="isPartOf" itemscope="" itemType="http://schema.org/CreativeWork http://schema.org/Product"><meta itemProp="name" content="The New York Times"/><meta itemProp="productID" content="nytimes.com:basic"/></span><article id="story" class="css-1vxca1d e1qksbhf0"><div id="top-wrapper" class="css-1sy8kpn"><div id="top-slug" class="css-l9onyx"><p>Advertisement</p></div><div class="ad top-wrapper" style="text-align:center;height:100%;display:block;min-height:250px"><div id="top" class="place-ad" data-position="top"></div></div></div><span itemProp="hasPart" itemscope="" itemType="http://schema.org/WebPageElement"><meta itemProp="isAccessibleForFree" content="False"/><meta itemProp="cssSelector" content=".meteredContent"/></span><div><header class="css-llk6mt euiyums4"><div id="sponsor-wrapper" class="css-1hyfx7x"><div id="sponsor-slug" class="css-19vbshk"><p>Supported by</p></div><div class="ad sponsor-wrapper" style="text-align:center;height:100%;display:block"><div id="sponsor" class="" data-position="sponsor"></div></div></div><div class="css-11n4cex euiyums3"></div><div class="css-1vkm6nb ehdk2mb0"><h1 itemProp="headline" class="css-1s4ffep e1h9rw200" id="link-2df79d6c"><span>She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.</span></h1></div><p class="css-1ifw933 e1wiw3jv0">With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.</p><div class="css-79elbk" data-testid="photoviewer-wrapper"><div class="css-z3e15g" data-testid="photoviewer-wrapper-hidden"></div><div data-testid="photoviewer-children" class="css-1a48zt4 ehw59r15"><figure class="sizeMedium layoutVertical css-1ox9jel" aria-label="media" role="group" itemscope="" itemProp="associatedMedia" itemID="https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg?quality=90&amp;auto=webp" itemType="http://schema.org/ImageObject"><div class="css-bsn42l"><span class="css-1dv1kvn">Image</span><img alt="“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, who pleaded guilty to an assault that occurred when she was 14." class="css-11cwn6f" src="https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg?quality=75&amp;auto=webp&amp;disable=upscale" srcSet="https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg?quality=90&amp;auto=webp 600w,https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-jumbo.jpg?quality=90&amp;auto=webp 683w,https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-superJumbo.jpg?quality=90&amp;auto=webp 1366w" sizes="((min-width: 600px) and (max-width: 1004px)) 84vw, (min-width: 1005px) 60vw, 100vw" itemProp="url" itemID="https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg?quality=75&amp;auto=webp&amp;disable=upscale"/></div><figcaption itemProp="caption description" class="css-17ai7jg emkp2hg0"><span aria-hidden="true" class="css-8i9d0s e13ogyst0">“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, who pleaded guilty to an assault that occurred when she was 14.</span><span itemProp="copyrightHolder" class="emkp2hg2 css-1nwzsjy e1z0qqy90"><span class="css-1ly73wi e1tej78p0">Credit</span><span><span class="css-1dv1kvn">Credit</span><span>Sarah Blesener for The New York Times</span></span></span></figcaption></figure></div></div><div class="css-acwcvw epjyd6m0"><div class="css-pdw9fk epjyd6m1"><div class="css-1txwxcy ey68jwv0"><a href="https://www.nytimes.com/by/joseph-goldstein" class="css-uwwqev"><img alt="Joseph Goldstein" title="Joseph Goldstein" src="https://static01.nyt.com/images/2018/07/16/multimedia/author-joseph-goldstein/author-joseph-goldstein-thumbLarge.png" class="css-1rjmmt7 ey68jwv2"/></a><a href="https://www.nytimes.com/by/ali-watkins" class="css-uwwqev"><img alt="Ali Watkins" title="Ali Watkins" src="https://static01.nyt.com/images/2019/02/20/multimedia/author-ali-watkins/author-ali-watkins-thumbLarge.png" class="css-1rjmmt7 ey68jwv2"/></a></div><div class="css-1baulvz"><p class="css-1nuro5j e1jsehar1" itemProp="author" itemscope="" itemType="http://schema.org/Person">By<!-- --> <a href="https://www.nytimes.com/by/joseph-goldstein" class="css-1riqqik e1jsehar0"><span class="css-1baulvz" itemProp="name">Joseph Goldstein</span></a> and <a href="https://www.nytimes.com/by/ali-watkins" class="css-1riqqik e1jsehar0"><span class="css-1baulvz last-byline" itemProp="name">Ali Watkins</span></a></p></div></div><ul class="css-1w5cs23 epjyd6m2"><li><time class="css-rqb9bm e16638kd0" dateTime="2019-08-01">Aug 1, 2019</time></li><li class="css-6n7j50"><div class=""><div role="toolbar" aria-label="Social Media Share buttons, Save button, and Comments Panel with current comment count" class="css-d8bdto" data-testid="share-tools"><ul class="css-y8aj3r"><li class="css-60hakz"><a href="https://www.facebook.com/dialog/feed?app_id=9869919170&amp;link=https%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html&amp;smid=fb-share&amp;name=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;redirect_uri=https%3A%2F%2Fwww.facebook.com%2F" target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook"><svg class="css-4brsb6" viewBox="0 0 7 15" width="7" height="15"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.775 14.163V7.08h1.923l.255-2.441H4.775l.004-1.222c0-.636.06-.977.958-.977H6.94V0H5.016c-2.31 0-3.123 1.184-3.123 3.175V4.64H.453v2.44h1.44v7.083h2.882z" fill="#000"></path></svg></a></li><li class="css-60hakz"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fnyti.ms%2F2GEzuZ8&amp;text=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database." target="_blank" rel="noopener noreferrer" aria-label="Share on Twitter"><svg viewBox="0 0 13 10" class="css-4brsb6" width="13" height="10"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.987 2.772l.025.425-.429-.052c-1.562-.2-2.927-.876-4.086-2.011L.93.571.784.987c-.309.927-.111 1.906.533 2.565.343.364.266.416-.327.2-.206-.07-.386-.122-.403-.096-.06.06.146.85.309 1.161.223.434.678.858 1.176 1.11l.42.199-.497.009c-.481 0-.498.008-.447.19.172.564.85 1.162 1.606 1.422l.532.182-.464.277a4.833 4.833 0 0 1-2.3.641c-.387.009-.704.044-.704.07 0 .086 1.047.572 1.657.762 1.828.564 4 .32 5.631-.641 1.159-.685 2.318-2.045 2.859-3.363.292-.702.583-1.984.583-2.6 0-.398.026-.45.507-.927.283-.277.55-.58.6-.667.087-.165.078-.165-.36-.018-.73.26-.832.226-.472-.164.266-.278.584-.78.584-.928 0-.026-.129.018-.275.096a4.79 4.79 0 0 1-.755.294l-.464.148-.42-.286C9.66.467 9.335.293 9.163.24 8.725.12 8.055.137 7.66.276c-1.074.39-1.752 1.395-1.674 2.496z" fill="#000"></path></svg></a></li><li class="css-60hakz"><a href="mailto:?subject=NYTimes.com%3A%20She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;body=From%20The%20New%20York%20Times%3A%0A%0AShe%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.%0A%0AWith%20little%20oversight%2C%20the%20N.Y.P.D.%20has%20been%20using%20powerful%20surveillance%20technology%20on%20photos%20of%20children%20and%20teenagers.%0A%0Ahttps%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html" target="_blank" rel="noopener noreferrer" aria-label="Email"><svg viewBox="0 0 15 9" class="css-4brsb6" width="15" height="9"><path fill-rule="evenodd" clip-rule="evenodd" d="M.906 8.418V0L5.64 4.76.906 8.419zm13 0L9.174 4.761 13.906 0v8.418zM7.407 6.539l-1.13-1.137L.907 9h13l-5.37-3.598-1.13 1.137zM1.297 0h12.22l-6.11 5.095L1.297 0z" fill="#000"></path></svg></a></li><li class="css-60hakz"><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="More sharing options" class="css-16ogagc" data-testid=""><svg class="css-uhuo44" viewBox="0 0 16 13" width="16" height="13"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.406 5.359L8.978 0v3.215C3.82 3.215.406 8.107.406 12.66 1.653 9.133 4.29 7.517 8.978 7.517v3.2l6.428-5.358z" fill="#000"></path></svg></button></div></li><li class="css-60hakz"></li><li class="css-l72opv"></li></ul></div></div></li></ul></div></header></div><section name="articleBody" itemProp="articleBody" class="meteredContent css-1i2y565"><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0"><em class="css-2fg4z9 e1gzwzxm0">[What you need to know to start the day: </em><a class="css-1g7m0tk" href="https://www.nytimes.com/newsletters/newyorktoday?module=inline" title=""><em class="css-2fg4z9 e1gzwzxm0">Get New York Today in your inbox</em></a><em class="css-2fg4z9 e1gzwzxm0">.]</em></p><p class="css-exrw3m evys1bk0">The New York Police Department has been loading <!-- -->thousands of arrest photos of children and teenagers<!-- --> into a facial recognition database despite evidence the technology has a higher risk of false matches in younger faces. </p><p class="css-exrw3m evys1bk0">For about four years, internal records show, the department has used the technology to compare crime scene images with its collection of juvenile mug <!-- -->shots<!-- -->, the photos that are taken at an arrest. Most of the photos are of teenagers, largely 13 to 16 years old, but children as young as 11 have been included. </p><p class="css-exrw3m evys1bk0">Elected officials and civil rights groups said the disclosure that the city was deploying a powerful surveillance tool on adolescents — whose privacy seems sacrosanct and whose status is protected in the criminal justice system — was a striking example of the Police Department’s ability to adopt advancing technology with little public scrutiny.</p><p class="css-exrw3m evys1bk0">Several members of the City Council as well as a range of civil liberties groups said they were unaware of the policy until they were contacted by The New York Times. </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">Police <!-- -->Department officials defended the decision, <!-- -->saying it was just the latest evolution of a longstanding policing technique: using arrest photos to identify suspects.</p><p class="css-exrw3m evys1bk0">“I don’t think this is any secret decision that’s made behind closed doors,” the city’s chief of detectives, Dermot F. Shea, said in an interview. “This is just process, and making sure we’re doing everything to fight crime.” </p><p class="css-exrw3m evys1bk0">Other cities have begun to debate whether law enforcement should use facial recognition, which relies on an algorithm to quickly pore through images and suggest matches. In May, <a class="css-1g7m0tk" href="https://www.nytimes.com/2019/05/14/us/facial-recognition-ban-san-francisco.html?module=inline" title="">San Francisco blocked city agencies, including the police</a>, from using the tool amid unease about potential government <!-- -->abuse<!-- -->. <a class="css-1g7m0tk" href="https://www.nytimes.com/2019/07/08/us/detroit-facial-recognition-cameras.html?module=inline" title="">Detroit is facing public resistance to a technology </a>that has been shown to have lower accuracy with people with darker skin. </p><p class="css-exrw3m evys1bk0">In New York, the state Education Department recently told the Lockport, N.Y., <!-- -->school district to delay a plan to use facial recognition on students, citing privacy concerns. </p><p class="css-exrw3m evys1bk0">“At the end <!-- -->of the day, it should be banned — no young people,” said <!-- -->Councilman Donovan Richards<!-- -->, a Queens Democrat who heads the Public Safety Committee, which oversees the Police Department. </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">The department said its legal bureau had approved using facial recognition on juveniles. <a class="css-1g7m0tk" href="https://www.nytimes.com/2019/06/09/opinion/facial-recognition-police-new-york-city.html?module=inline" title="">The algorithm may suggest a lead, but detectives would not make an arrest based solely on </a><a class="css-1g7m0tk" href="https://www.nytimes.com/2019/06/09/opinion/facial-recognition-police-new-york-city.html?module=inline" title="">that</a>, Chief Shea said.</p></div><aside class="css-o6xoe7"></aside></div><div class="css-79elbk" data-testid="photoviewer-wrapper"><div class="css-z3e15g" data-testid="photoviewer-wrapper-hidden"></div><div data-testid="photoviewer-children" class="css-1a48zt4 ehw59r15"><figure class="css-jcw7oy e1g7ppur0" aria-label="media" role="group" itemProp="associatedMedia" itemscope="" itemID="https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg?quality=90&amp;auto=webp" itemType="http://schema.org/ImageObject"><div class="css-1xdhyk6 erfvjey0"><span class="css-1ly73wi e1tej78p0">Image</span><img alt="Dermot Shea, the city&amp;rsquo;s chief of detectives, said investigators would not arrest anyone based solely on a facial recognition match." class="css-1m50asq" src="https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg?quality=75&amp;auto=webp&amp;disable=upscale" srcSet="https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg?quality=90&amp;auto=webp 600w,https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-jumbo.jpg?quality=90&amp;auto=webp 1024w,https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-superJumbo.jpg?quality=90&amp;auto=webp 2048w" sizes="((min-width: 600px) and (max-width: 1004px)) 84vw, (min-width: 1005px) 60vw, 100vw" itemProp="url" itemID="https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg?quality=75&amp;auto=webp&amp;disable=upscale"/></div><figcaption itemProp="caption description" class="css-1l44abu e1xdpqjp0"><span aria-hidden="true" class="css-8i9d0s e13ogyst0">Dermot Shea, the city&rsquo;s chief of detectives, said investigators would not arrest anyone based solely on a facial recognition match.</span><span itemProp="copyrightHolder" class="css-vuqh7u e1z0qqy90"><span class="css-1ly73wi e1tej78p0">Credit</span><span>Chang W. Lee/The New York Times</span></span></figcaption></figure></div></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">Still, facial recognition has not been widely tested on children. Most algorithms are taught to “think” based on adult faces, and there is growing evidence that they do not work as well on children. </p><p class="css-exrw3m evys1bk0">The National Institute of Standards and Technology<!-- -->, which is part of the Commerce Department and <a class="css-1g7m0tk" href="https://nvlpubs.nist.gov/nistpubs/ir/2018/NIST.IR.8238.pdf" title="" rel="noopener noreferrer" target="_blank">evaluates facial recognition </a><a class="css-1g7m0tk" href="https://nvlpubs.nist.gov/nistpubs/ir/2018/NIST.IR.8238.pdf" title="" rel="noopener noreferrer" target="_blank">algorithms</a> for accuracy, recently found the <!-- -->vast majority of more than 100 <!-- -->facial recognition <!-- -->algorithms had a higher rate of mistaken matches among children. The <!-- -->e<!-- -->rror rate was most pronounced in young children but was also seen in those aged 10 to 16.</p><p class="css-exrw3m evys1bk0">Aging poses another problem:<!-- --> The appearance of children and adolescents can change <!-- --> drastically as bones stretch and shift, altering the underlying facial structure. </p><p class="css-exrw3m evys1bk0">“I would use extreme caution in using those algorithms,” said <!-- -->Karl Ricanek Jr.<!-- -->, a computer science professor and <!-- -->co-founder of the Face Aging Group at the University of North Carolina-<!-- -->Wilmington<!-- -->. </p><p class="css-exrw3m evys1bk0">Technology that can match an image of a younger teenager to a recent arrest photo may be less effective at finding the same person even one or two years later, he said. </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0"> “The systems do not have the capacity to understand the dynamic changes that occur to a child’s face,” Dr. Ricanek said. </p><p class="css-exrw3m evys1bk0">Idemia<!-- --> and <!-- -->DataWorks Plus<!-- -->, the two companies that provide facial recognition software to the Police Department, did not respond to requests for comment. </p><p class="css-exrw3m evys1bk0">The New York Police Department can take arrest photos of minors as young as <!-- -->11<!-- --> who are charged with a felony, depending on the severity of the charge. </p><p class="css-exrw3m evys1bk0">And in many cases, the department keeps the photos for years, making facial recognition comparisons to what may have effectively become outdated images. There are photos of 5,500 individuals in the juvenile database, 4,100 of whom are no longer 16 or under, the department said. Teenagers 17 and older are considered adults in the criminal justice system. </p><p class="css-exrw3m evys1bk0">Police officials declined to provide statistics on how often their facial recognition systems provide false matches, or to explain how they evaluate the system’s effectiveness.</p><p class="css-exrw3m evys1bk0">“We are comfortable with this technology because it has proved to be a valuable investigative method,” Chief Shea said. Facial recognition has helped lead to thousands of arrests of both adults and juveniles, the department has said. </p><p class="css-exrw3m evys1bk0">Mayor Bill de Blasio had been aware the department was using the technology on minors, said Freddi Goldstein, a spokeswoman for the mayor. </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">She said the Police Department followed “strict guidelines” in applying the technology and City Hall monitored the agency’s compliance with the policies. </p><p class="css-exrw3m evys1bk0">The Times learned details of the department’s use of facial recognition by reviewing documents posted online earlier this year by <a class="css-1g7m0tk" href="https://www.flawedfacedata.com/#footnote5" title="" rel="noopener noreferrer" target="_blank">Clare Garvie, a senior associate</a><a class="css-1g7m0tk" href="https://www.flawedfacedata.com/#footnote5" title="" rel="noopener noreferrer" target="_blank"> at the Center on Privacy and Technology at Georgetown Law</a>. Ms. Garvie received the documents as part of an open records lawsuit. </p><p class="css-exrw3m evys1bk0">It could not be determined whether other large police departments used facial recognition with juveniles because very few have written policies governing the use of the technology, Ms. Garvie said. </p><p class="css-exrw3m evys1bk0">New York detectives rely on a vast network<!-- --> of surveillance cameras throughout the city to provide images of people believed to have committed a crime. Since 2011, the department has had a dedicated unit of officers who use facial recognition to compare those images against millions of photos, usually mug shots. The software proposes matches, which have led to thousands of arrests, the department said. </p><p class="css-exrw3m evys1bk0">By 2013, top police officials were meeting to discuss including juveniles in the program, the documents reviewed by The Times showed. </p><p class="css-exrw3m evys1bk0">The documents showed that the juvenile database had been integrated into the system by 2015. </p><p class="css-exrw3m evys1bk0">“We have these photos. It makes sense,” Chief Shea said in the interview. </p><p class="css-exrw3m evys1bk0">State law requires that arrest photos be destroyed if the case is resolved in the juvenile’s favor, or if the child is found to have committed only a misdemeanor, rather than a felony. The photos also must be destroyed if a person reaches age 21 without a criminal record. <!-- --> </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">When children are charged with crimes, the court system usually takes some steps to prevent their acts from defining them in later years. Children who are 16 and under, for instance, are generally sent to Family Court, where records are not public. </p><p class="css-exrw3m evys1bk0">Yet including their photos in a facial recognition database runs the risk that an imperfect algorithm identifies them as possible suspects in later crimes, civil rights advocates said. A mistaken match could lead investigators to focus on the wrong person from the outset, they said. </p><p class="css-exrw3m evys1bk0">“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, a 17-year-old Brooklyn girl who had admitted guilt in Family Court to a group attack that happened when she was 14. She said she was present at the attack but did not participate. </p><p class="css-exrw3m evys1bk0">Bailey, who asked that she be identified only by her <!-- -->last name<!-- --> because she did not want her juvenile arrest to be public, has not been arrested again and is now a student at John Jay College of Criminal Justice. </p><p class="css-exrw3m evys1bk0">R<!-- -->ecent studies indicate that people of color, as well as children and women, have a greater risk of misidentification than their counterparts, sai<!-- -->d <!-- -->Joy Buolamwini<!-- -->, <!-- -->the founder of the Algorithmic Justice League and graduate researcher at the M.I.T. Media Lab<!-- -->, who has examined how human biases are built into artificial intelligence. </p><p class="css-exrw3m evys1bk0">The racial disparities in the juvenile justice system are stark: In New York, black and Latino juveniles were charged with crimes at far higher rates than whites in 2017, the most recent year for which numbers were available. Black juveniles outnumbered white juveniles <a class="css-1g7m0tk" href="https://www.criminaljustice.ny.gov/crimnet/ojsa/jj-reports/newyorkcity.pdf" title="" rel="noopener noreferrer" target="_blank">more than 15 to 1</a>.</p><p class="css-exrw3m evys1bk0">“If the facial recognition algorithm has a negative bias toward a black population, that will get magnified more toward children,” Dr. Ricanek said, adding that in terms of diminished accuracy, “you’re now putting yourself in unknown territory.”</p></div><aside class="css-o6xoe7"></aside></div><div class="css-1soubk3 epkadsg3"><div class="css-15g2oxy epkadsg2"><div class="css-2b3w4o e16ij5yr6"><a class="css-1g7m0tk" href="https://www.nytimes.com/2019/06/09/opinion/facial-recognition-police-new-york-city.html?action=click&amp;module=RelatedLinks&amp;pgtype=Article"><div class="css-i9gxme e16ij5yr4"><div class="css-14b9hti e16ij5yr5">Opinion | James O’Neill</div><div class="css-1j8dw05 e16ij5yr2">How Facial Recognition Makes You Safer</div><time class="css-rqb9bm e16638kd0" dateTime="2019-06-09">Jun 9, 2019</time></div><div class="css-1vm5oi9 e16ij5yr0"><img src="https://static01.nyt.com/images/2019/06/07/opinion/sunday/07Oneill/07Oneill-threeByTwoSmallAt2X.jpg" class="css-32rbo2 e16ij5yr1"/></div></a></div><div class="css-2b3w4o e16ij5yr6"><a class="css-1g7m0tk" href="https://www.nytimes.com/2019/06/07/opinion/lockport-facial-recognition-schools.html?action=click&amp;module=RelatedLinks&amp;pgtype=Article"><div class="css-i9gxme e16ij5yr4"><div class="css-14b9hti e16ij5yr5">Opinion | Jim Shultz</div><div class="css-1j8dw05 e16ij5yr2">Spying on Children Won’t Keep Them Safe</div><time class="css-rqb9bm e16638kd0" dateTime="2019-06-07">Jun 7, 2019</time></div><div class="css-1vm5oi9 e16ij5yr0"><img src="https://static01.nyt.com/images/2019/06/07/opinion/07shultz-privacy/07shultz-privacy-threeByTwoSmallAt2X.jpg" class="css-32rbo2 e16ij5yr1"/></div></a></div></div></div></section><div class="bottom-of-article"><div class="css-1ubp8k9"></div><div class="css-wg1cha"><div class="css-19hdyf3 e1e7j8ap0"><div><p>Joseph Goldstein writes about policing and the criminal justice system. He has been a reporter at The Times since 2011, and is based in New York. He also worked for a year in the Kabul bureau, reporting on Afghanistan. <span class="css-4w91ra"> <a href="https://twitter.com/JoeKGoldstein" class="css-1rj8to8" rel="noopener noreferrer" target="_blank"><span class="css-0">@</span>JoeKGoldstein</a> </span></p></div></div><div class="css-19hdyf3 e1e7j8ap0"><div><p>Ali Watkins is a reporter on the Metro Desk, covering courts and social services. Previously, she covered national security in Washington for The Times, BuzzFeed and McClatchy Newspapers. <span class="css-4w91ra"> <a href="https://twitter.com/AliWatkins" class="css-1rj8to8" rel="noopener noreferrer" target="_blank"><span class="css-0">@</span>AliWatkins</a> </span></p></div></div></div><div class="css-vdv0al">A version of this article appears in print on <!-- -->, Section <!-- -->A<!-- -->, Page <!-- -->1<!-- --> of the New York edition<!-- --> with the headline: <!-- -->In New York, Police Computers Scan Faces, Some as Young as 11<span>. <a href="http://www.nytreprints.com/">Order Reprints</a> | <a href="http://www.nytimes.com/pages/todayspaper/index.html">Today’s Paper</a> | <a href="https://www.nytimes.com/subscriptions/Multiproduct/lp8HYKU.html?campaignId=48JQY">Subscribe</a></span></div><div class="css-i29ckm"><div class="css-10raysz"></div><div role="toolbar" aria-label="Social Media Share buttons, Save button, and Comments Panel with current comment count" class="css-d8bdto" data-testid="share-tools"><ul class="css-y8aj3r"><li class="css-ar1l6a"><a href="https://www.facebook.com/dialog/feed?app_id=9869919170&amp;link=https%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html&amp;smid=fb-share&amp;name=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;redirect_uri=https%3A%2F%2Fwww.facebook.com%2F" target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook"><svg class="css-4brsb6" viewBox="0 0 7 15" width="7" height="15"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.775 14.163V7.08h1.923l.255-2.441H4.775l.004-1.222c0-.636.06-.977.958-.977H6.94V0H5.016c-2.31 0-3.123 1.184-3.123 3.175V4.64H.453v2.44h1.44v7.083h2.882z" fill="#000"></path></svg></a></li><li class="css-ar1l6a"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fnyti.ms%2F2GEzuZ8&amp;text=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database." target="_blank" rel="noopener noreferrer" aria-label="Share on Twitter"><svg viewBox="0 0 13 10" class="css-4brsb6" width="13" height="10"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.987 2.772l.025.425-.429-.052c-1.562-.2-2.927-.876-4.086-2.011L.93.571.784.987c-.309.927-.111 1.906.533 2.565.343.364.266.416-.327.2-.206-.07-.386-.122-.403-.096-.06.06.146.85.309 1.161.223.434.678.858 1.176 1.11l.42.199-.497.009c-.481 0-.498.008-.447.19.172.564.85 1.162 1.606 1.422l.532.182-.464.277a4.833 4.833 0 0 1-2.3.641c-.387.009-.704.044-.704.07 0 .086 1.047.572 1.657.762 1.828.564 4 .32 5.631-.641 1.159-.685 2.318-2.045 2.859-3.363.292-.702.583-1.984.583-2.6 0-.398.026-.45.507-.927.283-.277.55-.58.6-.667.087-.165.078-.165-.36-.018-.73.26-.832.226-.472-.164.266-.278.584-.78.584-.928 0-.026-.129.018-.275.096a4.79 4.79 0 0 1-.755.294l-.464.148-.42-.286C9.66.467 9.335.293 9.163.24 8.725.12 8.055.137 7.66.276c-1.074.39-1.752 1.395-1.674 2.496z" fill="#000"></path></svg></a></li><li class="css-ar1l6a"><a href="mailto:?subject=NYTimes.com%3A%20She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;body=From%20The%20New%20York%20Times%3A%0A%0AShe%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.%0A%0AWith%20little%20oversight%2C%20the%20N.Y.P.D.%20has%20been%20using%20powerful%20surveillance%20technology%20on%20photos%20of%20children%20and%20teenagers.%0A%0Ahttps%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html" target="_blank" rel="noopener noreferrer" aria-label="Email"><svg viewBox="0 0 15 9" class="css-4brsb6" width="15" height="9"><path fill-rule="evenodd" clip-rule="evenodd" d="M.906 8.418V0L5.64 4.76.906 8.419zm13 0L9.174 4.761 13.906 0v8.418zM7.407 6.539l-1.13-1.137L.907 9h13l-5.37-3.598-1.13 1.137zM1.297 0h12.22l-6.11 5.095L1.297 0z" fill="#000"></path></svg></a></li><li class="css-ar1l6a"><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="More sharing options" class="css-16ogagc" data-testid=""><svg class="css-uhuo44" viewBox="0 0 16 13" width="16" height="13"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.406 5.359L8.978 0v3.215C3.82 3.215.406 8.107.406 12.66 1.653 9.133 4.29 7.517 8.978 7.517v3.2l6.428-5.358z" fill="#000"></path></svg></button></div></li></ul></div></div></div><div></div><div><div id="bottom-wrapper" class="css-1ede5it"><div id="bottom-slug" class="css-l9onyx"><p>Advertisement</p></div><div class="ad bottom-wrapper" style="text-align:center;height:100%;display:block;min-height:90px"><div id="bottom" class="" data-position="bottom"></div></div></div></div></article></div></main><nav class="css-1ropbjl" id="site-index" aria-labelledby="site-index-label" data-testid="site-index"><div class="css-uw59u"><header class="css-jxzr5i" data-testid="site-index-header"><h2 class="css-vz7hjd" id="site-index-label">Site Index</h2><a href="/"><svg xmlns="http://www.w3.org/2000/svg" class="css-oylsik" viewBox="0 0 184 25" fill="#000"><path d="M13.8 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8C6.2 1.4 5 1 4 1 2 1 .6 2.5.6 4.2c0 1.5 1.1 2 1.5 2.2l.1-.2c-.2-.2-.5-.4-.5-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8v3.1L9 10.2v.1l1.5 1.3v4.3c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2C3.6 6.9 4.7 6 5.8 5.4l-.1-.3c-3 .8-5.7 3.6-5.7 7 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1l-1.6-1.3V5.8c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7 0-1.5.2-2.1l2.1-.9v6.2zm10.6 2.3l-1.3 1 .2.2.6-.5 2.2 2 3-2-.1-.2-.8.5-1-1V9.4l.8-.6 1.7 1.4v6.1c0 3.8-.8 4.4-2.5 5v.3c2.8.1 5.4-.8 5.4-5.7V9.3l.9-.7-.2-.2-.8.6-2.5-2.1L18.5 9V.8h-.2l-3.5 2.4v.2c.4.2 1 .4 1 1.5l-.1 11.3zM34 15.1L31.5 17 29 15v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM53.1 2c0-.3-.1-.6-.2-.9h-.2c-.3.8-.7 1.2-1.7 1.2-.9 0-1.5-.5-1.9-.9l-2.9 3.3.2.2 1-.9c.6.5 1.1.9 2.5 1v8.3L44 3.2c-.5-.8-1.2-1.9-2.6-1.9-1.6 0-3 1.4-2.8 3.6h.3c.1-.6.4-1.3 1.1-1.3.5 0 1 .5 1.3 1v3.3c-1.8 0-3 .8-3 2.3 0 .8.4 2 1.6 2.3v-.2c-.2-.2-.3-.4-.3-.7 0-.5.4-.9 1.1-.9h.5v4.2c-2.1 0-3.8 1.2-3.8 3.2 0 1.9 1.6 2.8 3.4 2.7v-.2c-1.1-.1-1.6-.6-1.6-1.3 0-.9.6-1.3 1.4-1.3.8 0 1.5.5 2 1.1l2.9-3.2-.2-.2-.7.8c-1.1-1-1.7-1.3-3-1.5V5l8 14h.6V5c1.5-.1 2.9-1.3 2.9-3zm7.3 13.1L57.9 17l-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM76.7 8l-.7.5-1.9-1.6-2.2 2 .9.9v7.5l-2.4-1.5V9.6l.8-.5-2.3-2.2-2.2 2 .9.9V17l-.3.2-2.1-1.5v-6c0-1.4-.7-1.8-1.5-2.3-.7-.5-1.1-.8-1.1-1.5 0-.6.6-.9.9-1.1v-.2c-.8 0-2.9.8-2.9 2.7 0 1 .5 1.4 1 1.9s1 .9 1 1.8v5.8l-1.1.8.2.2 1-.8 2.3 2 2.5-1.7 2.8 1.7 5.3-3.1V9.2l1.3-1-.2-.2zm18.6-5.5l-1 .9-2.2-2-3.3 2.4V1.6h-.3l.1 16.2c-.3 0-1.2-.2-1.9-.4l-.2-13.5c0-1-.7-2.4-2.5-2.4s-3 1.4-3 2.8h.3c.1-.6.4-1.1 1-1.1s1.1.4 1.1 1.7v3.9c-1.8.1-2.9 1.1-2.9 2.4 0 .8.4 2 1.6 2V13c-.4-.2-.5-.5-.5-.7 0-.6.5-.8 1.3-.8h.4v6.2c-1.5.5-2.1 1.6-2.1 2.8 0 1.7 1.3 2.9 3.3 2.9 1.4 0 2.6-.2 3.8-.5 1-.2 2.3-.5 2.9-.5.8 0 1.1.4 1.1.9 0 .7-.3 1-.7 1.1v.2c1.6-.3 2.6-1.3 2.6-2.8s-1.5-2.4-3.1-2.4c-.8 0-2.5.3-3.7.5-1.4.3-2.8.5-3.2.5-.7 0-1.5-.3-1.5-1.3 0-.8.7-1.5 2.4-1.5.9 0 2 .1 3.1.4 1.2.3 2.3.6 3.3.6 1.5 0 2.8-.5 2.8-2.6V3.7l1.2-1-.2-.2zm-4.1 6.1c-.3.3-.7.6-1.2.6s-1-.3-1.2-.6V4.2l1-.7 1.4 1.3v3.8zm0 3c-.2-.2-.7-.5-1.2-.5s-1 .3-1.2.5V9c.2.2.7.5 1.2.5s1-.3 1.2-.5v2.6zm0 4.7c0 .8-.5 1.6-1.6 1.6h-.8V12c.2-.2.7-.5 1.2-.5s.9.3 1.2.5v4.3zm13.7-7.1l-3.2-2.3-4.9 2.8v6.5l-1 .8.1.2.8-.6 3.2 2.4 5-3V9.2zm-5.4 6.3V8.3l2.5 1.8v7.1l-2.5-1.7zm14.9-8.4h-.2c-.3.2-.6.4-.9.4-.4 0-.9-.2-1.1-.5h-.2l-1.7 1.9-1.7-1.9-3 2 .1.2.8-.5 1 1.1v6.3l-1.3 1 .2.2.6-.5 2.4 2 3.1-2.1-.1-.2-.9.5-1.2-1V9c.5.5 1.1 1 1.8 1 1.4.1 2.2-1.3 2.3-2.9zm12 9.6L123 19l-4.6-7 3.3-5.1h.2c.4.4 1 .8 1.7.8s1.2-.4 1.5-.8h.2c-.1 2-1.5 3.2-2.5 3.2s-1.5-.5-2.1-.8l-.3.5 5 7.4 1-.6v.1zm-11-.5l-1.3 1 .2.2.6-.5 2.2 2 3-2-.2-.2-.8.5-1-1V.8h-.1l-3.6 2.4v.2c.4.2 1 .3 1 1.5v11.3zM143 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8-1.3-.4-2.5-.8-3.5-.8-2 0-3.4 1.5-3.4 3.2 0 1.5 1.1 2 1.5 2.2l.1-.2c-.3-.2-.6-.4-.6-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8V9l-1.5 1.3v.1l1.5 1.3V16c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2c.5-1.3 1.6-2.2 2.6-2.9l-.1-.2c-3 .8-5.7 3.5-5.7 6.9 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1L140 8.8v-3c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7.1-1.5.3-2.1l2.1-.9-.1 6.2zm12.2-12h-.1l-2 1.7v.1l1.7 1.9h.2l2-1.7v-.1l-1.8-1.9zm3 14.8l-.8.5-1-1V9.3l1-.7-.2-.2-.7.6-1.8-2.1-2.9 2 .2.3.7-.5.9 1.1v6.5l-1.3 1 .1.2.7-.5 2.2 2 3-2-.1-.3zm16.7-.1l-.7.5-1.1-1V9.3l1-.8-.2-.2-.8.7-2.3-2.1-3 2.1-2.3-2.1L154 9l-1.8-2.1-2.9 2 .1.3.7-.5 1 1.1v6.5l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.9-.6 1.5 1.4v6l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.8-.5 1.6 1.4v6l-.7.7 2.3 2.1 3.1-2.1v-.3zm8.7-1.5l-2.5 1.9-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.8l3.5 2.5 4.5-3.6-.1-.3zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zm14.1-.9l-1.9-1.5c1.3-1.1 1.8-2.6 1.8-3.6v-.6h-.2c-.2.5-.6 1-1.4 1-.8 0-1.3-.4-1.8-1L176 9.3v3.6l1.7 1.3c-1.7 1.5-2 2.5-2 3.3 0 1 .5 1.7 1.3 2l.1-.2c-.2-.2-.4-.3-.4-.8 0-.3.4-.8 1.2-.8 1 0 1.6.7 1.9 1l4.3-2.6v-3.6h-.1zm-1.1-3c-.7 1.2-2.2 2.4-3.1 3l-1.1-.9V8.1c.4 1 1.5 1.8 2.6 1.8.7 0 1.1-.1 1.6-.4zm-1.7 8c-.5-1.1-1.7-1.9-2.9-1.9-.3 0-1.1 0-1.9.5.5-.8 1.8-2.2 3.5-3.2l1.2 1 .1 3.6z"></path></svg></a><div class="css-1otr2jl" data-testid="go-to-homepage"><a class="css-1c8n994" href="/">Go to Home Page »</a></div></header><div class="css-qtw155" data-testid="site-index-accordion"><div class=" " role="tablist" aria-multiselectable="true" data-testid="accordion"><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-0" id="item-siteindex-0" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">news</header><div class="css-1hyfx7x" id="body-siteindex-0" aria-labelledby="item-siteindex-0" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com" data-testid="accordion-item-list-link">home page</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/world" data-testid="accordion-item-list-link">world</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/us" data-testid="accordion-item-list-link">U.S.</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/politics" data-testid="accordion-item-list-link">politics</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/news-event/2020-election" data-testid="accordion-item-list-link">Election 2020</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/nyregion" data-testid="accordion-item-list-link">New York</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/business" data-testid="accordion-item-list-link">business</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/technology" data-testid="accordion-item-list-link">tech</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/science" data-testid="accordion-item-list-link">science</a></li><li class="css-10t7hia smartphone"><a class="css-mzqdl" href="https://www.nytimes.com/section/climate" data-testid="accordion-item-list-link">climate</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/sports" data-testid="accordion-item-list-link">sports</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/obituaries" data-testid="accordion-item-list-link">obituaries</a></li><li class="css-10t7hia smartphone"><a class="css-mzqdl" href="https://www.nytimes.com/section/upshot" data-testid="accordion-item-list-link">the upshot</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/todayspaper" data-testid="accordion-item-list-link">today&#x27;s paper</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/corrections" data-testid="accordion-item-list-link">corrections</a></li></ul></div></div><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-1" id="item-siteindex-1" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">opinion</header><div class="css-1hyfx7x" id="body-siteindex-1" aria-labelledby="item-siteindex-1" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion" data-testid="accordion-item-list-link">today&#x27;s opinion</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/columnists" data-testid="accordion-item-list-link">op-ed columnists</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/editorials" data-testid="accordion-item-list-link">editorials</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/contributors" data-testid="accordion-item-list-link">op-ed Contributors</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/letters" data-testid="accordion-item-list-link">letters</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/sunday" data-testid="accordion-item-list-link">sunday review</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/video/opinion" data-testid="accordion-item-list-link">video: opinion</a></li></ul></div></div><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-2" id="item-siteindex-2" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">arts</header><div class="css-1hyfx7x" id="body-siteindex-2" aria-labelledby="item-siteindex-2" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts" data-testid="accordion-item-list-link">today&#x27;s arts</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts/design" data-testid="accordion-item-list-link">art &amp; design</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/books" data-testid="accordion-item-list-link">books</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts/dance" data-testid="accordion-item-list-link">dance</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/movies" data-testid="accordion-item-list-link">movies</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts/music" data-testid="accordion-item-list-link">music</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/spotlight/pop-culture" data-testid="accordion-item-list-link">Pop Culture</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts/television" data-testid="accordion-item-list-link">television</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/theater" data-testid="accordion-item-list-link">theater</a></li><li class="css-10t7hia smartphone"><a class="css-mzqdl" href="https://www.nytimes.com/watching" data-testid="accordion-item-list-link">watching</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/video/arts" data-testid="accordion-item-list-link">video: arts</a></li></ul></div></div><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-3" id="item-siteindex-3" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">living</header><div class="css-1hyfx7x" id="body-siteindex-3" aria-labelledby="item-siteindex-3" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/automobiles" data-testid="accordion-item-list-link">automobiles</a></li><li class="css-10t7hia smartphone"><a class="css-mzqdl" href="https://cooking.nytimes.com/" data-testid="accordion-item-list-link">Cooking</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/crosswords" data-testid="accordion-item-list-link">crossword</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/education" data-testid="accordion-item-list-link">education</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/food" data-testid="accordion-item-list-link">food</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/health" data-testid="accordion-item-list-link">health</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/jobs" data-testid="accordion-item-list-link">jobs</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/magazine" data-testid="accordion-item-list-link">magazine</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://parenting.nytimes.com/" data-testid="accordion-item-list-link">parenting</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/realestate" data-testid="accordion-item-list-link">real estate</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/style" data-testid="accordion-item-list-link">style</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/t-magazine" data-testid="accordion-item-list-link">t magazine</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/travel" data-testid="accordion-item-list-link">travel</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/fashion/weddings" data-testid="accordion-item-list-link">love</a></li></ul></div></div><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-4" id="item-siteindex-4" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">listings &amp; more</header><div class="css-1hyfx7x" id="body-siteindex-4" aria-labelledby="item-siteindex-4" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/reader-center" data-testid="accordion-item-list-link">Reader Center</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://thewirecutter.com" data-testid="accordion-item-list-link">Wirecutter</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="http://nytconferences.com/" data-testid="accordion-item-list-link">Live Events</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/learning" data-testid="accordion-item-list-link">The Learning Network</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="http://www.nytimes.com/marketing/tools-and-services" data-testid="accordion-item-list-link">tools &amp; services</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/events" data-testid="accordion-item-list-link">N.Y.C. events guide</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/multimedia" data-testid="accordion-item-list-link">multimedia</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/lens" data-testid="accordion-item-list-link">photography</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/video" data-testid="accordion-item-list-link">video</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/newsletters" data-testid="accordion-item-list-link">Newsletters</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/store" data-testid="accordion-item-list-link">NYT store</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/times-journeys" data-testid="accordion-item-list-link">times journeys</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://myaccount.nytimes.com/membercenter/myaccount.html" data-testid="accordion-item-list-link">manage my account</a></li></ul></div></div></div></div><div class="css-v0l3hm" data-testid="site-index-sections"><div class="css-g4gku8" data-testid="site-index-section"><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-0"><h3 class="css-rxqrcl" id="site-index-section-label-0">news</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com" data-testid="site-index-section-list-link">home page</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/world" data-testid="site-index-section-list-link">world</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/us" data-testid="site-index-section-list-link">U.S.</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/politics" data-testid="site-index-section-list-link">politics</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/news-event/2020-election" data-testid="site-index-section-list-link">Election 2020</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/nyregion" data-testid="site-index-section-list-link">New York</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/business" data-testid="site-index-section-list-link">business</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/technology" data-testid="site-index-section-list-link">tech</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/science" data-testid="site-index-section-list-link">science</a></li><li class="css-ist4u3 smartphone"><a class="css-kwpx34" href="https://www.nytimes.com/section/climate" data-testid="site-index-section-list-link">climate</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/sports" data-testid="site-index-section-list-link">sports</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/obituaries" data-testid="site-index-section-list-link">obituaries</a></li><li class="css-ist4u3 smartphone"><a class="css-kwpx34" href="https://www.nytimes.com/section/upshot" data-testid="site-index-section-list-link">the upshot</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/todayspaper" data-testid="site-index-section-list-link">today&#x27;s paper</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/corrections" data-testid="site-index-section-list-link">corrections</a></li></ul></section><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-1"><h3 class="css-rxqrcl" id="site-index-section-label-1">opinion</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion" data-testid="site-index-section-list-link">today&#x27;s opinion</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/columnists" data-testid="site-index-section-list-link">op-ed columnists</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/editorials" data-testid="site-index-section-list-link">editorials</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/contributors" data-testid="site-index-section-list-link">op-ed Contributors</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/letters" data-testid="site-index-section-list-link">letters</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/sunday" data-testid="site-index-section-list-link">sunday review</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/video/opinion" data-testid="site-index-section-list-link">video: opinion</a></li></ul></section><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-2"><h3 class="css-rxqrcl" id="site-index-section-label-2">arts</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts" data-testid="site-index-section-list-link">today&#x27;s arts</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts/design" data-testid="site-index-section-list-link">art &amp; design</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/books" data-testid="site-index-section-list-link">books</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts/dance" data-testid="site-index-section-list-link">dance</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/movies" data-testid="site-index-section-list-link">movies</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts/music" data-testid="site-index-section-list-link">music</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/spotlight/pop-culture" data-testid="site-index-section-list-link">Pop Culture</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts/television" data-testid="site-index-section-list-link">television</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/theater" data-testid="site-index-section-list-link">theater</a></li><li class="css-ist4u3 smartphone"><a class="css-kwpx34" href="https://www.nytimes.com/watching" data-testid="site-index-section-list-link">watching</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/video/arts" data-testid="site-index-section-list-link">video: arts</a></li></ul></section><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-3"><h3 class="css-rxqrcl" id="site-index-section-label-3">living</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/automobiles" data-testid="site-index-section-list-link">automobiles</a></li><li class="css-ist4u3 smartphone"><a class="css-kwpx34" href="https://cooking.nytimes.com/" data-testid="site-index-section-list-link">Cooking</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/crosswords" data-testid="site-index-section-list-link">crossword</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/education" data-testid="site-index-section-list-link">education</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/food" data-testid="site-index-section-list-link">food</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/health" data-testid="site-index-section-list-link">health</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/jobs" data-testid="site-index-section-list-link">jobs</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/magazine" data-testid="site-index-section-list-link">magazine</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://parenting.nytimes.com/" data-testid="site-index-section-list-link">parenting</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/realestate" data-testid="site-index-section-list-link">real estate</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/style" data-testid="site-index-section-list-link">style</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/t-magazine" data-testid="site-index-section-list-link">t magazine</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/travel" data-testid="site-index-section-list-link">travel</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/fashion/weddings" data-testid="site-index-section-list-link">love</a></li></ul></section><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-4"><h3 class="css-rxqrcl" id="site-index-section-label-4">more</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/reader-center" data-testid="site-index-section-list-link">Reader Center</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://thewirecutter.com" data-testid="site-index-section-list-link">Wirecutter</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="http://nytconferences.com/" data-testid="site-index-section-list-link">Live Events</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/learning" data-testid="site-index-section-list-link">The Learning Network</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="http://www.nytimes.com/marketing/tools-and-services" data-testid="site-index-section-list-link">tools &amp; services</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/events" data-testid="site-index-section-list-link">N.Y.C. events guide</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/multimedia" data-testid="site-index-section-list-link">multimedia</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/lens" data-testid="site-index-section-list-link">photography</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/video" data-testid="site-index-section-list-link">video</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/newsletters" data-testid="site-index-section-list-link">Newsletters</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/store" data-testid="site-index-section-list-link">NYT store</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/times-journeys" data-testid="site-index-section-list-link">times journeys</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://myaccount.nytimes.com/membercenter/myaccount.html" data-testid="site-index-section-list-link">manage my account</a></li></ul></section><div class="css-6xhk3s" aria-labelledby="site-index-subscribe-label"><h3 class="css-rxqrcl" id="site-index-subscribe-label">Subscribe</h3><ul class="css-1iruc8t" data-testid="site-index-subscribe-list"><li class="css-tj0ten"><a class="css-1k2cjfc" href="https://www.nytimes.com/hdleftnav" data-testid="site-index-subscribe-list-link"><svg class="css-r5ic95" viewBox="0 0 14 13" fill="#000"><path d="M13.1,11.7H3.5V1.2h9.6V11.7zM13.1,0.4H3.5C3,0.4,2.6,0.8,2.6,1.2v2.2H0.9C0.4,3.4,0,3.8,0,4.3v5.2v1.5c0,0.8,0.8,1.5,1.8,1.5h1.7h0h7.4h2.2c0.5,0,0.9-0.4,0.9-0.9V1.2C14,0.8,13.6,0.4,13.1,0.4"></path><polygon points="10.9,3 5.2,3 5.2,3.9 11.4,3.9 11.4,3"></polygon><rect x="5.2" y="4.7" width="6.1" height="0.9"></rect><rect x="5.2" y="6.5" width="6.1" height="0.9"></rect></svg>home delivery</a></li><li class="css-tj0ten"><a class="css-1k2cjfc" href="https://www.nytimes.com/digitalleftnav" data-testid="site-index-subscribe-list-link"><svg class="css-r5ic95" viewBox="0 0 10 13"><path fill="#000" d="M9.9,8c-0.4,1.1-1.2,1.9-2.3,2.4V8l1.3-1.2L7.6,5.7V4c1.2-0.1,2-1,2-2c0-1.4-1.3-1.9-2.1-1.9c-0.2,0-0.3,0-0.6,0.1v0.1c0.1,0,0.2,0,0.3,0c0.5,0,0.9,0.2,0.9,0.7c0,0.4-0.3,0.7-0.8,0.7C6,1.7,4.5,0.6,2.8,0.6c-1.5,0-2.5,1.1-2.5,2.2C0.3,4,1,4.3,1.6,4.6l0-0.1C1.4,4.4,1.3,4.1,1.3,3.8c0-0.5,0.5-0.9,1-0.9C3.7,2.9,6,4,7.4,4h0.1v1.7L6.2,6.8L7.5,8v2.4c-0.5,0.2-1.1,0.3-1.7,0.3c-2.2,0-3.6-1.3-3.6-3.5c0-0.5,0.1-1,0.2-1.5l1.1-0.5V10l2.2-1v-5L2.5,5.5c0.3-1,1-1.7,1.8-2l0,0C2.2,3.9,0.1,5.6,0.1,8c0,2.9,2.4,4.8,5.2,4.8C8.2,12.9,9.9,10.9,9.9,8L9.9,8z"></path></svg>digital subscriptions</a></li><li class="css-tj0ten"><a class="css-1k2cjfc" href="https://www.nytimes.com/subscription/games/lp897H9.html" data-testid="site-index-subscribe-list-link"><svg class="css-r5ic95" viewBox="0 0 13 13" fill="#000"><polygon points="0,-93.6 0,-86.9 6.6,-93.6"></polygon><polygon points="0.9,-86 7.5,-86 7.5,-92.6"></polygon><polygon points="0,-98 0,-94.8 8.8,-94.8 8.8,-86 12,-86 12,-98"></polygon><path d="M11.9-40c-0.4,1.1-1.2,1.9-2.3,2.4V-40l1.3-1.2l-1.3-1.2V-44c1.2-0.1,2-1,2-2c0-1.4-1.3-1.9-2.1-1.9c-0.2,0-0.3,0-0.6,0.1v0.1c0.1,0,0.2,0,0.3,0c0.5,0,0.9,0.2,0.9,0.7c0,0.4-0.3,0.7-0.8,0.7c-1.3,0-2.8-1.1-4.5-1.1c-1.5,0-2.5,1.1-2.5,2.2c0,1.1,0.6,1.5,1.3,1.7l0-0.1c-0.2-0.1-0.4-0.4-0.4-0.7c0-0.5,0.5-0.9,1-0.9C5.7-45.1,8-44,9.4-44h0.1v1.7l-1.3,1.1L9.5-40v2.4c-0.5,0.2-1.1,0.3-1.7,0.3c-2.2,0-3.6-1.3-3.6-3.5c0-0.5,0.1-1,0.2-1.5l1.1-0.5v4.9l2.2-1v-5l-3.3,1.5c0.3-1,1-1.7,1.8-2l0,0c-2.2,0.5-4.3,2.1-4.3,4.6c0,2.9,2.4,4.8,5.2,4.8C10.2-35.1,11.9-37.1,11.9-40L11.9-40z"></path><path d="M12.2-23.7c-0.2,0-0.4,0.2-0.4,0.4v0.4L0.4-19.1v2.3l3,1l-0.2,0.6c-0.3,0.8,0.1,1.8,0.9,2.1l1.7,0.7c0.2,0.1,0.4,0.1,0.6,0.1c0.6,0,1.3-0.4,1.5-1l0.4-0.9l3.5,1.2v0.4c0,0.2,0.2,0.4,0.4,0.4c0.2,0,0.4-0.2,0.4-0.4v-10.7C12.6-23.5,12.4-23.7,12.2-23.7M7.1-13.6c-0.2,0.4-0.6,0.6-1,0.4l-1.7-0.7c-0.4-0.2-0.6-0.6-0.4-1l0.3-0.7l3.3,1.1L7.1-13.6z"></path><path d="M13.1-60.3H3.5v-10.5h9.6V-60.3zM13.1-71.6H3.5c-0.5,0-0.9,0.4-0.9,0.9v2.2H0.9c-0.5,0-0.9,0.4-0.9,0.9v5.2v1.5c0,0.8,0.8,1.5,1.8,1.5h1.7h0h7.4h2.2c0.5,0,0.9-0.4,0.9-0.9v-10.5C14-71.2,13.6-71.6,13.1-71.6"></path><polygon points="10.9,-69 5.2,-69 5.2,-68.1 11.4,-68.1 11.4,-69"></polygon><rect x="5.2" y="-67.3" width="6.1" height="0.9"></rect><rect x="5.2" y="-65.5" width="6.1" height="0.9"></rect><path d="M12,6.5H6.5V12H1V6.5h5.5V1H12V6.5zM12,0H1C0.4,0,0,0.5,0,1v11c0,0.6,0.4,1,1,1h11c0.5,0,1-0.4,1-1V1C13,0.5,12.5,0,12,0"></path></svg>Crossword</a></li><li class="css-tj0ten"><a class="css-1k2cjfc" href="https://www.nytimes.com/subscriptions/Multiproduct/lp8R3WU.html" data-testid="site-index-subscribe-list-link"><svg class="css-r5ic95" viewBox="0 0 13 13" fill="#000"><path d="M12,2.9L9.6,5.2c-0.1,0.1-0.3,0.1-0.4,0C9.1,5.2,9.1,5,9.3,4.9l2.4-2.4c-0.2-0.2-0.3-0.3-0.5-0.5L8.7,4.3c-0.1,0.1-0.3,0.1-0.4,0C8.2,4.3,8.2,4.1,8.4,4l2.4-2.4c-0.3-0.3-0.5-0.5-0.5-0.5L7.6,3.4C7.1,4,6.8,5.1,7.1,5.8c-1.4,1-4.6,3.5-5.1,4c-0.8,0.8-0.4,1.8-0.3,1.9c0,0,0,0,0,0c0,0,0,0,0,0c0.1,0.1,1.1,0.5,1.9-0.3c0.4-0.4,2.9-3.6,3.9-5C8.4,6.9,9.6,6.6,10.2,6l2.3-2.6C12.5,3.4,12.3,3.2,12,2.9z"></path><path d="M0.8,1.9l0.3-0.3c0.9-0.9,3.2,1.1,3.8,1.7s0.9,1.8,0.4,2.6c1.4,1.1,4.6,3.5,5,3.9c0.8,0.8,0.4,1.8,0.3,1.9c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0.1-1.1,0.5-1.9-0.3c-0.4-0.4-2.9-3.7-4-5.1C3.9,6.7,2.9,6.4,2.3,5.8S-0.2,2.9,0.8,1.9z"></path></svg>Cooking</a></li></ul><ul class="css-1iruc8t" data-testid="site-index-corporate-links"><li><a class="css-1vhk1ks" href="https://www.nytimes.com/marketing/newsletters">email newsletters</a></li><li><a class="css-1vhk1ks" href="https://www.nytimes.com/corporateleftnav">corporate subscriptions</a></li><li><a class="css-1vhk1ks" href="https://www.nytimes.com/educationleftnav">education rate</a></li></ul><ul class="css-6td9kr" data-testid="site-index-alternate-links"><li><a class="css-1vhk1ks" href="https://www.nytimes.com/services/mobile/index.html">mobile applications</a></li><li><a class="css-1vhk1ks" href="http://eedition.nytimes.com/cgi-bin/signup.cgi?cc=37FYY">replica edition</a></li></ul></div></div></div></div></nav><footer role="contentinfo" class="css-1qmnftd e5u916q0"><nav data-testid="footer" class="css-15uy5yv"><h2 class="css-vz7hjd">Site Information Navigation</h2><ul class="css-1ho5u4o e5u916q1"><li data-testid="copyright"><a class="css-1p8nkc0" href="https://www.nytimes.com/content/help/rights/copyright/copyright-notice.html">© <span itemProp="copyrightYear">2019</span><span itemProp="publisher copyrightHolder provider sourceOrganization" itemscope="" itemType="http://schema.org/NewsMediaOrganization" itemID="https://www.nytimes.com"> <meta itemProp="diversityPolicy" content="https://www.nytco.com/diversity-and-inclusion-at-the-new-york-times/"/><meta itemProp="ethicsPolicy" content="https://www.nytco.com/who-we-are/culture/standards-and-ethics/"/><meta itemProp="foundingDate" content="1851-09-18"/><span itemProp="logo" itemscope="" itemType="https://schema.org/ImageObject"><meta itemProp="url" content="https://static01.nyt.com/images/misc/NYT_logo_rss_250x40.png"/></span><meta itemProp="url" content="https://www.nytimes.com/"/><meta itemProp="masthead" content="https://www.nytimes.com/interactive/2018/09/28/admin/the-new-york-times-masthead.html"/><meta itemProp="name" content="The New York Times"/><span>The New York Times Company</span></span></a></li></ul><ul class="css-13o0c9t e5u916q2"><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://myaccount.nytimes.com/membercenter/feedback.html">Contact Us</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="http://www.nytco.com/careers">Work with us</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="http://nytmediakit.com/">Advertise</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="http://www.tbrandstudio.com/">T Brand Studio</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/content/help/rights/privacy/policy/privacy-policy.html#pp">Your Ad Choices</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/privacy">Privacy</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/ref/membercenter/help/agree.html">Terms of Service</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/content/help/rights/sale/terms-of-sale.html">Terms of Sale</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="http://spiderbites.nytimes.com">Site Map</a></li><li class="smartphone css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://mobile.nytimes.com/help">Help</a></li><li class="desktop css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/membercenter/sitehelp.html">Help</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/subscription/multiproduct/lp8HYKU?campaignId=37WXW">Subscriptions</a></li></ul></nav></footer></div></div></div></div>
+ <script>window.__preloadedData = {"initialState":{"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==":{"__typename":"Article","id":"QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==","compatibility":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.compatibility","typename":"CompatibilityFeatures"},"archiveProperties":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.archiveProperties","typename":"ArticleArchiveProperties"},"collections@filterEmpty":[{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzE5ZjY2OTk4LWY1NjItNWVjNi1iM2Y5LTI5OGYxYzc2ZGQ4NA==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzExZjcyYWI0LTdjZDAtNTQwYS05M2NjLWYzNWIzMmNkMDEzZA==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzU4ZWNlMGQwLTNjMzUtNWZhOS1iNTM1LTk0OTk3YTdjOGMwZg==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzkwODhjZmU2LTg1ZTMtNTJmYi05OTNlLTYyODk3MDJhMTFmZg==","typename":"LegacyCollection"}],"tone":"NEWS","section":{"type":"id","generated":false,"id":"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uLzM5NDgwMzc0LTY2ZDMtNTYwMy05Y2UxLTU4Y2ZhMTI5ODhlMg==","typename":"Section"},"subsection":null,"sprinkledBody":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody","typename":"DocumentBlock"},"url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F08\u002F01\u002Fnyregion\u002Fnypd-facial-recognition-children-teenagers.html","adTargetingParams({\"clientAdParams\":{\"edn\":\"us\",\"plat\":\"web\",\"prop\":\"nyt\"}})":[{"type":"id","generated":false,"id":"AdTargetingParam:als_test1565027040168","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:propnyt","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:platweb","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:ednus","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:brandsensitivefalse","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:per","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:orgpolicedepartmentnyc","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:geonewyorkcity","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:desjuveniledelinquency,facialrecognitionsoftware,privacy,surveillanceofcitizensbygovern,police,civilrightsandliberties","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:spon","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:authaliwatkins,josephgoldstein","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:col","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:collnewyork,usnews,technology,techandsociety","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:artlenmedium","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:ledemedsznone","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:gui","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:templatearticle","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:typart,oak","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:sectionnyregion","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:si_sectionnyregion","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:id100000006583622","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:trend","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:ptnt10,nt15,nt16,nt18,nt3,nt4,nt9","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:gscatneg_mastercard,gs_law_misc,neg_chanel,gv_crime,neg_hearts,gs_tech,gs_law,gs_tech_computing,neg_ibmtest,gs_tech_phones,neg_samsung,gs_education","typename":"AdTargetingParam"}],"sourceId":"100000006583622","type":"article","wordCount":1357,"bylines":[{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0","typename":"Byline"}],"displayProperties":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.displayProperties","typename":"CreativeWorkDisplayProperties"},"typeOfMaterials":{"type":"json","json":["News"]},"collections":[{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzE5ZjY2OTk4LWY1NjItNWVjNi1iM2Y5LTI5OGYxYzc2ZGQ4NA==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzExZjcyYWI0LTdjZDAtNTQwYS05M2NjLWYzNWIzMmNkMDEzZA==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzU4ZWNlMGQwLTNjMzUtNWZhOS1iNTM1LTk0OTk3YTdjOGMwZg==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzkwODhjZmU2LTg1ZTMtNTJmYi05OTNlLTYyODk3MDJhMTFmZg==","typename":"LegacyCollection"}],"timesTags@filterEmpty":[{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.0","typename":"Organization"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.1","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.2","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.3","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.4","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.5","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.6","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.7","typename":"Location"}],"language":null,"desk":"Metro","kicker":"","headline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.headline","typename":"CreativeWorkHeadline"},"commentStatus":"ACCEPT_AND_DISPLAY_COMMENTS","firstPublished":"2019-08-01T17:15:31.000Z","lastModified":"2019-08-02T17:26:37.071Z","originalDesk":"Metro","source":{"type":"id","generated":false,"id":"Organization:T3JnYW5pemF0aW9uOm55dDovL29yZ2FuaXphdGlvbi9jMjc5MTM4OC02YjE2LTVmZmQtYTExOS05NmVhY2IxOTg5YzE=","typename":"Organization"},"printInformation":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.printInformation","typename":"PrintInformation"},"sprinkled":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled","typename":"SprinkledContent"},"followChannels":[{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.0","typename":"ChannelMetadata"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.1","typename":"ChannelMetadata"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.2","typename":"ChannelMetadata"}],"dfpTaxonomyException":null,"advertisingProperties":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.advertisingProperties","typename":"CreativeWorkAdvertisingProperties"},"translations":[],"summary":"With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.","lastMajorModification":"2019-08-02T09:30:23.000Z","uri":"nyt:\u002F\u002Farticle\u002F9da58246-2495-505f-9abd-b5fda8e67b56","eventId":"pubp:\u002F\u002Fevent\u002F47a657bafa8a476bb36832f90ee5ac6e","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia","typename":"Image"},"newsStatus":"DEFAULT","episodeProperties":null,"column":null,"reviewItems":[],"shortUrl":"https:\u002F\u002Fnyti.ms\u002F2GEzuZ8","promotionalHeadline":"She Was Arrested at 14. Her Photo Went to a Facial Recognition Database.","promotionalSummary":"With little oversight, the police have been using the powerful surveillance technology on photos of children and teenagers.","reviewSummary":"","legacy":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.legacy","typename":"ArticleLegacyData"},"addendums":[],"related@filterEmpty":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.compatibility":{"isOak":true,"__typename":"CompatibilityFeatures","hasVideo":false,"hasOakConversionError":false,"isArtReview":false,"isBookReview":false,"isDiningReview":false,"isMovieReview":false,"isTheaterReview":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.archiveProperties":{"timesMachineUrl":"","lede@stripHtml":"","thumbnails":[],"__typename":"ArticleArchiveProperties"},"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzE5ZjY2OTk4LWY1NjItNWVjNi1iM2Y5LTI5OGYxYzc2ZGQ4NA==":{"id":"TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzE5ZjY2OTk4LWY1NjItNWVjNi1iM2Y5LTI5OGYxYzc2ZGQ4NA==","slug":"nyregion","__typename":"LegacyCollection","name":"New York","collectionType":"SECTION","uri":"nyt:\u002F\u002Flegacycollection\u002F19f66998-f562-5ec6-b3f9-298f1c76dd84","type":"legacycollection","header":"","showCollectionStories":false},"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzExZjcyYWI0LTdjZDAtNTQwYS05M2NjLWYzNWIzMmNkMDEzZA==":{"id":"TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzExZjcyYWI0LTdjZDAtNTQwYS05M2NjLWYzNWIzMmNkMDEzZA==","slug":"us","__typename":"LegacyCollection","name":"U.S. News","collectionType":"SECTION","uri":"nyt:\u002F\u002Flegacycollection\u002F11f72ab4-7cd0-540a-93cc-f35b32cd013d","type":"legacycollection","header":"","showCollectionStories":false},"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzU4ZWNlMGQwLTNjMzUtNWZhOS1iNTM1LTk0OTk3YTdjOGMwZg==":{"id":"TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzU4ZWNlMGQwLTNjMzUtNWZhOS1iNTM1LTk0OTk3YTdjOGMwZg==","slug":"technology","__typename":"LegacyCollection","name":"Technology","collectionType":"SECTION","uri":"nyt:\u002F\u002Flegacycollection\u002F58ece0d0-3c35-5fa9-b535-94997a7c8c0f","type":"legacycollection","header":"","showCollectionStories":false},"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzkwODhjZmU2LTg1ZTMtNTJmYi05OTNlLTYyODk3MDJhMTFmZg==":{"id":"TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzkwODhjZmU2LTg1ZTMtNTJmYi05OTNlLTYyODk3MDJhMTFmZg==","slug":"experience-tech-and-society","__typename":"LegacyCollection","name":"Tech and Society","collectionType":"SPOTLIGHT","uri":"nyt:\u002F\u002Flegacycollection\u002F9088cfe6-85e3-52fb-993e-6289702a11ff","type":"legacycollection","header":"","showCollectionStories":false},"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uLzM5NDgwMzc0LTY2ZDMtNTYwMy05Y2UxLTU4Y2ZhMTI5ODhlMg==":{"id":"U2VjdGlvbjpueXQ6Ly9zZWN0aW9uLzM5NDgwMzc0LTY2ZDMtNTYwMy05Y2UxLTU4Y2ZhMTI5ODhlMg==","name":"nyregion","displayName":"New York","url":"\u002Fsection\u002Fnyregion","uri":"nyt:\u002F\u002Fsection\u002F39480374-66d3-5603-9ce1-58cfa12988e2","__typename":"Section"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0":{"__typename":"HeaderBasicBlock","label":null,"headline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.headline","typename":"Heading1Block"},"summary":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.summary","typename":"SummaryBlock"},"ledeMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.ledeMedia","typename":"ImageBlock"},"byline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline","typename":"BylineBlock"},"timestampBlock":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.timestampBlock","typename":"TimestampBlock"}},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.3":{"__typename":"Dropzone","index":0,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.5":{"__typename":"Dropzone","index":1,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.6":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.6.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.7":{"__typename":"Dropzone","index":2,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.8":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.8.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.9":{"__typename":"Dropzone","index":3,"bad":false,"adsMobile":true,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.11":{"__typename":"Dropzone","index":4,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.12":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.12.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.13":{"__typename":"Dropzone","index":5,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.3","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.4","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.5","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.6","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.15":{"__typename":"Dropzone","index":6,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16.content.1","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.17":{"__typename":"Dropzone","index":7,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.3","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.19":{"__typename":"Dropzone","index":8,"bad":false,"adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.3","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.21":{"__typename":"Dropzone","index":9,"bad":true,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.22":{"__typename":"ImageBlock","size":"MEDIUM","media":{"type":"id","generated":false,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm","typename":"Image"}},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.23":{"__typename":"Dropzone","index":10,"bad":true,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.24":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.24.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.25":{"__typename":"Dropzone","index":11,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.3","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.4","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.5","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.6","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.7","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.8","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.9","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.27":{"__typename":"Dropzone","index":12,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.29":{"__typename":"Dropzone","index":13,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.3","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.4","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.5","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.31":{"__typename":"Dropzone","index":14,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.32":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.32.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.33":{"__typename":"Dropzone","index":15,"bad":false,"adsMobile":true,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.34":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.34.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.35":{"__typename":"Dropzone","index":16,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.3","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.37":{"__typename":"Dropzone","index":17,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.39":{"__typename":"Dropzone","index":18,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.40":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.40.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.41":{"__typename":"Dropzone","index":19,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.42":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.42.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.43":{"__typename":"Dropzone","index":20,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.44":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.44.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.45":{"__typename":"Dropzone","index":21,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.46":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.46.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.47":{"__typename":"Dropzone","index":22,"bad":false,"adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.48":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.48.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.49":{"__typename":"Dropzone","index":23,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.3","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.51":{"__typename":"Dropzone","index":24,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.52":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.52.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.53":{"__typename":"Dropzone","index":25,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54.content.1","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.55":{"__typename":"Dropzone","index":26,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.56":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.56.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.57":{"__typename":"Dropzone","index":27,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.58":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.58.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.59":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.59.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.60":{"__typename":"Dropzone","index":28,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61.content.1","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.62":{"__typename":"Dropzone","index":29,"bad":false,"adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.63":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.63.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.64":{"__typename":"Dropzone","index":30,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.65":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.65.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.66":{"__typename":"Dropzone","index":31,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.67":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.67.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.68":{"__typename":"Dropzone","index":32,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.70":{"__typename":"Dropzone","index":33,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.3","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.4","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.5","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.6","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.72":{"__typename":"Dropzone","index":34,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.74":{"__typename":"Dropzone","index":35,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.75":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.75.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.76":{"__typename":"Dropzone","index":36,"bad":true,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77":{"__typename":"RelatedLinksBlock","displayStyle":"STANDARD","title":[],"description":[],"related@filterEmpty":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0","typename":"Article"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1","typename":"Article"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody":{"content@filterEmpty":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0","typename":"HeaderBasicBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.3","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.5","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.6","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.7","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.8","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.9","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.11","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.12","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.13","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.15","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.17","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.19","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.21","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.22","typename":"ImageBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.23","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.24","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.25","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.27","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.29","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.31","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.32","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.33","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.34","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.35","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.37","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.39","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.40","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.41","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.42","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.43","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.44","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.45","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.46","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.47","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.48","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.49","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.51","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.52","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.53","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.55","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.56","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.57","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.58","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.59","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.60","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.62","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.63","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.64","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.65","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.66","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.67","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.68","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.70","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.72","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.74","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.75","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.76","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77","typename":"RelatedLinksBlock"}],"__typename":"DocumentBlock","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.0","typename":"HeaderBasicBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.1","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.2","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.3","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.4","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.5","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.6","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.7","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.8","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.9","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.10","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.11","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.12","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.13","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.14","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.15","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.16","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.17","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.18","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.19","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.20","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.21","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.22","typename":"ImageBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.23","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.24","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.25","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.26","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.27","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.28","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.29","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.30","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.31","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.32","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.33","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.34","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.35","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.36","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.37","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.38","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.39","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.40","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.41","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.42","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.43","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.44","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.45","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.46","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.47","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.48","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.49","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.50","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.51","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.52","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.53","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.54","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.55","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.56","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.57","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.58","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.59","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.60","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.61","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.62","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.63","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.64","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.65","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.66","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.67","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.68","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.69","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.70","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.71","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.72","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.73","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.74","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.75","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.76","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.77","typename":"RelatedLinksBlock"}],"content@take({\"first\":10000})@filterEmpty":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.0","typename":"HeaderBasicBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.1","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.2","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.3","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.4","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.5","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.6","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.7","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.8","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.9","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.10","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.11","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.12","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.13","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.14","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.15","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.16","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.17","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.18","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.19","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.20","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.21","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.22","typename":"ImageBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.23","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.24","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.25","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.26","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.27","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.28","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.29","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.30","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.31","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.32","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.33","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.34","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.35","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.36","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.37","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.38","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.39","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.40","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.41","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.42","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.43","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.44","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.45","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.46","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.47","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.48","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.49","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.50","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.51","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.52","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.53","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.54","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.55","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.56","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.57","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.58","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.59","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.60","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.61","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.62","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.63","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.64","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.65","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.66","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.67","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.68","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.69","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.70","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.71","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.72","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.73","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.74","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.75","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.76","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.77","typename":"RelatedLinksBlock"}]},"AdTargetingParam:als_test1565027040168":{"key":"als_test","value":"1565027040168","__typename":"AdTargetingParam"},"AdTargetingParam:propnyt":{"key":"prop","value":"nyt","__typename":"AdTargetingParam"},"AdTargetingParam:platweb":{"key":"plat","value":"web","__typename":"AdTargetingParam"},"AdTargetingParam:ednus":{"key":"edn","value":"us","__typename":"AdTargetingParam"},"AdTargetingParam:brandsensitivefalse":{"key":"brandsensitive","value":"false","__typename":"AdTargetingParam"},"AdTargetingParam:per":{"key":"per","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:orgpolicedepartmentnyc":{"key":"org","value":"policedepartmentnyc","__typename":"AdTargetingParam"},"AdTargetingParam:geonewyorkcity":{"key":"geo","value":"newyorkcity","__typename":"AdTargetingParam"},"AdTargetingParam:desjuveniledelinquency,facialrecognitionsoftware,privacy,surveillanceofcitizensbygovern,police,civilrightsandliberties":{"key":"des","value":"juveniledelinquency,facialrecognitionsoftware,privacy,surveillanceofcitizensbygovern,police,civilrightsandliberties","__typename":"AdTargetingParam"},"AdTargetingParam:spon":{"key":"spon","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:authaliwatkins,josephgoldstein":{"key":"auth","value":"aliwatkins,josephgoldstein","__typename":"AdTargetingParam"},"AdTargetingParam:col":{"key":"col","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:collnewyork,usnews,technology,techandsociety":{"key":"coll","value":"newyork,usnews,technology,techandsociety","__typename":"AdTargetingParam"},"AdTargetingParam:artlenmedium":{"key":"artlen","value":"medium","__typename":"AdTargetingParam"},"AdTargetingParam:ledemedsznone":{"key":"ledemedsz","value":"none","__typename":"AdTargetingParam"},"AdTargetingParam:gui":{"key":"gui","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:templatearticle":{"key":"template","value":"article","__typename":"AdTargetingParam"},"AdTargetingParam:typart,oak":{"key":"typ","value":"art,oak","__typename":"AdTargetingParam"},"AdTargetingParam:sectionnyregion":{"key":"section","value":"nyregion","__typename":"AdTargetingParam"},"AdTargetingParam:si_sectionnyregion":{"key":"si_section","value":"nyregion","__typename":"AdTargetingParam"},"AdTargetingParam:id100000006583622":{"key":"id","value":"100000006583622","__typename":"AdTargetingParam"},"AdTargetingParam:trend":{"key":"trend","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:ptnt10,nt15,nt16,nt18,nt3,nt4,nt9":{"key":"pt","value":"nt10,nt15,nt16,nt18,nt3,nt4,nt9","__typename":"AdTargetingParam"},"AdTargetingParam:gscatneg_mastercard,gs_law_misc,neg_chanel,gv_crime,neg_hearts,gs_tech,gs_law,gs_tech_computing,neg_ibmtest,gs_tech_phones,neg_samsung,gs_education":{"key":"gscat","value":"neg_mastercard,gs_law_misc,neg_chanel,gv_crime,neg_hearts,gs_tech,gs_law,gs_tech_computing,neg_ibmtest,gs_tech_phones,neg_samsung,gs_education","__typename":"AdTargetingParam"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0":{"displayName":"Joseph Goldstein","__typename":"Person","url":"","contactDetails":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails","typename":"ContactDetails"},"legacyData":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.legacyData","typename":"PersonLegacyData"}},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1":{"displayName":"Ali Watkins","__typename":"Person","url":"","contactDetails":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails","typename":"ContactDetails"},"legacyData":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.legacyData","typename":"PersonLegacyData"}},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0":{"creators":[{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0","typename":"Person"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1","typename":"Person"}],"__typename":"Byline","renderedRepresentation":"By Joseph Goldstein and Ali Watkins"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.displayProperties":{"fullBleedDisplayStyle":"","__typename":"CreativeWorkDisplayProperties","serveAsNyt4":false},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.0":{"__typename":"Organization","vernacular":"NYPD","isAdvertisingBrandSensitive":false,"displayName":"Police Department (NYC)"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.1":{"__typename":"Subject","vernacular":"Juvenile delinquency","isAdvertisingBrandSensitive":false,"displayName":"Juvenile Delinquency"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.2":{"__typename":"Subject","vernacular":"Facial Recognition","isAdvertisingBrandSensitive":false,"displayName":"Facial Recognition Software"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.3":{"__typename":"Subject","vernacular":"Privacy","isAdvertisingBrandSensitive":false,"displayName":"Privacy"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.4":{"__typename":"Subject","vernacular":"Government Surveillance","isAdvertisingBrandSensitive":false,"displayName":"Surveillance of Citizens by Government"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.5":{"__typename":"Subject","vernacular":"Police","isAdvertisingBrandSensitive":false,"displayName":"Police"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.6":{"__typename":"Subject","vernacular":"Civil Rights","isAdvertisingBrandSensitive":false,"displayName":"Civil Rights and Liberties"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.7":{"__typename":"Location","vernacular":"NYC","isAdvertisingBrandSensitive":false,"displayName":"New York City"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.headline":{"default":"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.","__typename":"CreativeWorkHeadline","default@stripHtml":"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.","seo@stripHtml":""},"Organization:T3JnYW5pemF0aW9uOm55dDovL29yZ2FuaXphdGlvbi9jMjc5MTM4OC02YjE2LTVmZmQtYTExOS05NmVhY2IxOTg5YzE=":{"id":"T3JnYW5pemF0aW9uOm55dDovL29yZ2FuaXphdGlvbi9jMjc5MTM4OC02YjE2LTVmZmQtYTExOS05NmVhY2IxOTg5YzE=","displayName":"New York Times","__typename":"Organization"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.printInformation":{"page":"1","section":"A","publicationDate":"2019-08-02T04:00:00.000Z","__typename":"PrintInformation","edition":"NewYork","headline@stripHtml":"In New York, Police Computers Scan Faces, Some as Young as 11"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.0":{"name":"mobile","stride":4,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.1":{"name":"desktop","stride":7,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.2":{"name":"mobileHoldout","stride":6,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.3":{"name":"desktopHoldout","stride":8,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.4":{"name":"hybrid","stride":4,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled":{"configs":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.0","typename":"SprinkledConfig"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.1","typename":"SprinkledConfig"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.2","typename":"SprinkledConfig"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.3","typename":"SprinkledConfig"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.4","typename":"SprinkledConfig"}],"__typename":"SprinkledContent"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.0":{"__typename":"HeaderBasicBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.1":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.2":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.3":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.4":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.5":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.6":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.7":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.8":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.9":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.10":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.11":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.12":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.13":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.14":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.15":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.16":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.17":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.18":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.19":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.20":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.21":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.22":{"__typename":"ImageBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.23":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.24":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.25":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.26":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.27":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.28":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.29":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.30":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.31":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.32":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.33":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.34":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.35":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.36":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.37":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.38":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.39":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.40":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.41":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.42":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.43":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.44":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.45":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.46":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.47":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.48":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.49":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.50":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.51":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.52":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.53":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.54":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.55":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.56":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.57":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.58":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.59":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.60":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.61":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.62":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.63":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.64":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.65":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.66":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.67":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.68":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.69":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.70":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.71":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.72":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.73":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.74":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.75":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.76":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.77":{"__typename":"RelatedLinksBlock"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.0":{"uri":"nyt:\u002F\u002Fchannel\u002Fdd1a5725-c3be-4673-be2c-9055eb12c10f","__typename":"ChannelMetadata"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.1":{"uri":"nyt:\u002F\u002Fchannel\u002F7cf18b43-f1c6-4946-8a9c-4e24bad34c5c","__typename":"ChannelMetadata"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.2":{"uri":"nyt:\u002F\u002Fchannel\u002F679a17bb-20e6-40a7-a589-e7742a2a52ed","__typename":"ChannelMetadata"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.0":{"__typename":"HeaderBasicBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.1":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.2":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.3":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.4":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.5":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.6":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.7":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.8":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.9":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.10":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.11":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.12":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.13":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.14":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.15":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.16":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.17":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.18":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.19":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.20":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.21":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.22":{"__typename":"ImageBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.23":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.24":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.25":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.26":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.27":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.28":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.29":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.30":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.31":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.32":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.33":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.34":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.35":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.36":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.37":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.38":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.39":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.40":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.41":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.42":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.43":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.44":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.45":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.46":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.47":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.48":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.49":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.50":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.51":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.52":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.53":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.54":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.55":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.56":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.57":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.58":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.59":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.60":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.61":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.62":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.63":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.64":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.65":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.66":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.67":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.68":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.69":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.70":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.71":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.72":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.73":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.74":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.75":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.76":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.77":{"__typename":"RelatedLinksBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.advertisingProperties":{"sensitivity":"SHOW_ADS","__typename":"CreativeWorkAdvertisingProperties"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).0":{"name":"MASTER","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-articleLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-superJumbo.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-articleLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-articleLarge.jpg","height":400,"width":600,"name":"articleLarge","__typename":"ImageRendition"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-superJumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-superJumbo.jpg","height":1365,"width":2048,"name":"superJumbo","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).1":{"name":"SMALL_SQUARE","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-thumbStandard.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-thumbLarge.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-thumbStandard.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-thumbStandard.jpg","height":75,"width":75,"name":"thumbStandard","__typename":"ImageRendition"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-thumbLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-thumbLarge.jpg","height":150,"width":150,"name":"thumbLarge","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).2":{"name":"SIXTEEN_BY_NINE","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg","height":900,"width":1600,"name":"videoSixteenByNineJumbo1600","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).3":{"name":"FACEBOOK","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-facebookJumbo.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-facebookJumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-facebookJumbo.jpg","height":550,"width":1050,"name":"facebookJumbo","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).4":{"name":"WATCH","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-watch308.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-watch308.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-watch308.jpg","height":348,"width":312,"name":"watch308","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia":{"crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]})":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).1","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).2","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).3","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).4","typename":"ImageCrop"}],"caption":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.caption","typename":"TextOnlyDocumentBlock"},"__typename":"Image"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.caption":{"text":"","__typename":"TextOnlyDocumentBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.headline":{"textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.headline.content.0","typename":"TextInline"}],"__typename":"Heading1Block"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.headline.content.0":{"__typename":"TextInline","text@stripHtml":"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.summary":{"textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.summary.content.0","typename":"TextInline"}],"__typename":"SummaryBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.summary.content.0":{"__typename":"TextInline","text":"With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.ledeMedia":{"__typename":"ImageBlock","size":"MEDIUM","media":{"type":"id","generated":false,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz","typename":"Image"}},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz":{"id":"SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz","imageType":"photo","url":"\u002Fimagepages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F00nypd-juveniles.html","uri":"nyt:\u002F\u002Fimage\u002F2ad4fe36-f59f-5211-a12e-6b1f5bce2fa3","credit":"Sarah Blesener for The New York Times","legacyHtmlCaption":"“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, who pleaded guilty to an assault that occurred when she was 14.","crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]})":[{"type":"id","generated":true,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).1","typename":"ImageCrop"}],"caption":{"type":"id","generated":true,"id":"$Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.caption","typename":"TextOnlyDocumentBlock"},"__typename":"Image"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg","name":"articleLarge","width":600,"height":900,"__typename":"ImageRendition"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-popup.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-popup.jpg","name":"popup","width":334,"height":500,"__typename":"ImageRendition"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-jumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-jumbo.jpg","name":"jumbo","width":683,"height":1024,"__typename":"ImageRendition"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-superJumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-superJumbo.jpg","name":"superJumbo","width":1366,"height":2048,"__typename":"ImageRendition"},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-popup.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-jumbo.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-superJumbo.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleInline.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleInline.jpg","name":"articleInline","width":190,"height":285,"__typename":"ImageRendition"},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).1":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleInline.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.caption":{"text":"“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, who pleaded guilty to an assault that occurred when she was 14.","__typename":"TextOnlyDocumentBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline":{"textAlign":"LEFT","hideHeadshots":false,"bylines":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0","typename":"Byline"}],"role@filterEmpty":[],"__typename":"BylineBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0":{"prefix":"By","creators":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0","typename":"Person"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1","typename":"Person"}],"renderedRepresentation":null,"__typename":"Byline"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0":{"displayName":"Joseph Goldstein","bioUrl":"https:\u002F\u002Fwww.nytimes.com\u002Fby\u002Fjoseph-goldstein","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia","typename":"Image"},"__typename":"Person"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-articleLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-articleLarge.png","name":"articleLarge","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-popup.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-popup.png","name":"popup","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog480.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blog480.png","name":"blog480","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog533.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blog533.png","name":"blog533","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog427.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blog427.png","name":"blog427","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-tmagSF.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-tmagSF.png","name":"tmagSF","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-tmagArticle.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-tmagArticle.png","name":"tmagArticle","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-slide.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-slide.png","name":"slide","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-jumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-jumbo.png","name":"jumbo","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-superJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-superJumbo.png","name":"superJumbo","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blog225.png","name":"blog225","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master675.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master675.png","name":"master675","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master495.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master495.png","name":"master495","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master180.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master180.png","name":"master180","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master315.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master315.png","name":"master315","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master768.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master768.png","name":"master768","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-articleLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-popup.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog480.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog533.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog427.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-tmagSF.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-tmagArticle.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-slide.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-jumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-superJumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master675.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master495.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master180.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master315.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master768.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbStandard.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-thumbStandard.png","name":"thumbStandard","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blogSmallThumb.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blogSmallThumb.png","name":"blogSmallThumb","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-thumbLarge.png","name":"thumbLarge","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-smallSquare168.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-smallSquare168.png","name":"smallSquare168","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-smallSquare252.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-smallSquare252.png","name":"smallSquare252","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.1":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbStandard.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blogSmallThumb.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-smallSquare168.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-smallSquare252.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-square320.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-square320.png","name":"square320","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-moth.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-moth.png","name":"moth","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-filmstrip.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-filmstrip.png","name":"filmstrip","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-square640.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-square640.png","name":"square640","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumSquare149.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumSquare149.png","name":"mediumSquare149","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.2":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-square320.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-moth.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-filmstrip.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-square640.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumSquare149.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-sfSpan.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-sfSpan.png","name":"sfSpan","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeHorizontal375.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-largeHorizontal375.png","name":"largeHorizontal375","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeHorizontalJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-largeHorizontalJumbo.png","name":"largeHorizontalJumbo","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-horizontalMediumAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-horizontalMediumAt2X.png","name":"horizontalMediumAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.3":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-sfSpan.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeHorizontal375.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeHorizontalJumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-horizontalMediumAt2X.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-hpLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-hpLarge.png","name":"hpLarge","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeWidescreen573.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-largeWidescreen573.png","name":"largeWidescreen573","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.4":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-hpLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeWidescreen573.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbWide.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-thumbWide.png","name":"thumbWide","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoThumb.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoThumb.png","name":"videoThumb","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoLarge.png","name":"videoLarge","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo210.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo210.png","name":"mediumThreeByTwo210","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo225.png","name":"mediumThreeByTwo225","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo440.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo440.png","name":"mediumThreeByTwo440","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo252.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo252.png","name":"mediumThreeByTwo252","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo378.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo378.png","name":"mediumThreeByTwo378","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoLargeAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-threeByTwoLargeAt2X.png","name":"threeByTwoLargeAt2X","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoMediumAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-threeByTwoMediumAt2X.png","name":"threeByTwoMediumAt2X","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoSmallAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-threeByTwoSmallAt2X.png","name":"threeByTwoSmallAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.5":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbWide.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoThumb.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo210.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo440.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo252.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo378.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoLargeAt2X.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoMediumAt2X.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoSmallAt2X.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-articleInline.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-articleInline.png","name":"articleInline","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-hpSmall.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-hpSmall.png","name":"hpSmall","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blogSmallInline.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blogSmallInline.png","name":"blogSmallInline","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumFlexible177.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumFlexible177.png","name":"mediumFlexible177","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.6":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-articleInline.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-hpSmall.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blogSmallInline.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumFlexible177.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSmall.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSmall.png","name":"videoSmall","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoHpMedium.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoHpMedium.png","name":"videoHpMedium","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine600.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine600.png","name":"videoSixteenByNine600","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine540.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine540.png","name":"videoSixteenByNine540","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine495.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine495.png","name":"videoSixteenByNine495","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine390.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine390.png","name":"videoSixteenByNine390","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine480.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine480.png","name":"videoSixteenByNine480","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine310.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine310.png","name":"videoSixteenByNine310","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine225.png","name":"videoSixteenByNine225","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine96.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine96.png","name":"videoSixteenByNine96","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine768.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine768.png","name":"videoSixteenByNine768","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine150.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine150.png","name":"videoSixteenByNine150","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNineJumbo1600.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNineJumbo1600.png","name":"videoSixteenByNineJumbo1600","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.7":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSmall.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoHpMedium.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine600.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine540.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine495.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine390.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine480.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine310.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine96.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine768.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine150.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNineJumbo1600.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-miniMoth.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-miniMoth.png","name":"miniMoth","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-windowsTile336H.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-windowsTile336H.png","name":"windowsTile336H","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.8":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-miniMoth.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-windowsTile336H.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.9":{"renditions":[],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-facebookJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-facebookJumbo.png","name":"facebookJumbo","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.10":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-facebookJumbo.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-watch308.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-watch308.png","name":"watch308","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-watch268.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-watch268.png","name":"watch268","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.11":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-watch308.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-watch268.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.12":{"renditions":[],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia":{"crops":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.1","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.2","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.3","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.4","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.5","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.6","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.7","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.8","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.9","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.10","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.11","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.12","typename":"ImageCrop"}],"__typename":"Image"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1":{"displayName":"Ali Watkins","bioUrl":"https:\u002F\u002Fwww.nytimes.com\u002Fby\u002Fali-watkins","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia","typename":"Image"},"__typename":"Person"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-articleLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-articleLarge.png","name":"articleLarge","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-popup.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-popup.png","name":"popup","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog480.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blog480.png","name":"blog480","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog533.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blog533.png","name":"blog533","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog427.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blog427.png","name":"blog427","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-tmagSF.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-tmagSF.png","name":"tmagSF","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-tmagArticle.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-tmagArticle.png","name":"tmagArticle","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-slide.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-slide.png","name":"slide","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-jumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-jumbo.png","name":"jumbo","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-superJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-superJumbo.png","name":"superJumbo","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blog225.png","name":"blog225","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master675.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master675.png","name":"master675","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master495.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master495.png","name":"master495","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master180.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master180.png","name":"master180","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master315.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master315.png","name":"master315","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master768.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master768.png","name":"master768","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-articleLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-popup.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog480.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog533.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog427.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-tmagSF.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-tmagArticle.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-slide.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-jumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-superJumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master675.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master495.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master180.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master315.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master768.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbStandard.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-thumbStandard.png","name":"thumbStandard","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blogSmallThumb.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blogSmallThumb.png","name":"blogSmallThumb","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-thumbLarge.png","name":"thumbLarge","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-smallSquare168.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-smallSquare168.png","name":"smallSquare168","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-smallSquare252.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-smallSquare252.png","name":"smallSquare252","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.1":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbStandard.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blogSmallThumb.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-smallSquare168.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-smallSquare252.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-square320.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-square320.png","name":"square320","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-moth.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-moth.png","name":"moth","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-filmstrip.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-filmstrip.png","name":"filmstrip","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-square640.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-square640.png","name":"square640","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumSquare149.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumSquare149.png","name":"mediumSquare149","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.2":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-square320.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-moth.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-filmstrip.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-square640.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumSquare149.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-sfSpan.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-sfSpan.png","name":"sfSpan","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeHorizontal375.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-largeHorizontal375.png","name":"largeHorizontal375","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeHorizontalJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-largeHorizontalJumbo.png","name":"largeHorizontalJumbo","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-horizontalMediumAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-horizontalMediumAt2X.png","name":"horizontalMediumAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.3":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-sfSpan.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeHorizontal375.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeHorizontalJumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-horizontalMediumAt2X.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-hpLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-hpLarge.png","name":"hpLarge","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeWidescreen573.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-largeWidescreen573.png","name":"largeWidescreen573","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeWidescreen1050.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-largeWidescreen1050.png","name":"largeWidescreen1050","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.4":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-hpLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeWidescreen573.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeWidescreen1050.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbWide.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-thumbWide.png","name":"thumbWide","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoThumb.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoThumb.png","name":"videoThumb","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoLarge.png","name":"videoLarge","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo210.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo210.png","name":"mediumThreeByTwo210","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo225.png","name":"mediumThreeByTwo225","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo440.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo440.png","name":"mediumThreeByTwo440","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo252.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo252.png","name":"mediumThreeByTwo252","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo378.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo378.png","name":"mediumThreeByTwo378","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoLargeAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-threeByTwoLargeAt2X.png","name":"threeByTwoLargeAt2X","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoMediumAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-threeByTwoMediumAt2X.png","name":"threeByTwoMediumAt2X","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoSmallAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-threeByTwoSmallAt2X.png","name":"threeByTwoSmallAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.5":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbWide.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoThumb.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo210.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo440.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo252.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo378.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoLargeAt2X.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoMediumAt2X.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoSmallAt2X.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-articleInline.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-articleInline.png","name":"articleInline","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-hpSmall.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-hpSmall.png","name":"hpSmall","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blogSmallInline.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blogSmallInline.png","name":"blogSmallInline","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumFlexible177.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumFlexible177.png","name":"mediumFlexible177","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.6":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-articleInline.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-hpSmall.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blogSmallInline.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumFlexible177.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSmall.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSmall.png","name":"videoSmall","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoHpMedium.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoHpMedium.png","name":"videoHpMedium","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine600.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine600.png","name":"videoSixteenByNine600","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine540.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine540.png","name":"videoSixteenByNine540","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine495.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine495.png","name":"videoSixteenByNine495","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine390.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine390.png","name":"videoSixteenByNine390","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine1050.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine1050.png","name":"videoSixteenByNine1050","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine480.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine480.png","name":"videoSixteenByNine480","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine310.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine310.png","name":"videoSixteenByNine310","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine225.png","name":"videoSixteenByNine225","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine96.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine96.png","name":"videoSixteenByNine96","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine768.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine768.png","name":"videoSixteenByNine768","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine150.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine150.png","name":"videoSixteenByNine150","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNineJumbo1600.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNineJumbo1600.png","name":"videoSixteenByNineJumbo1600","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.7":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSmall.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoHpMedium.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine600.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine540.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine495.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine390.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine1050.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine480.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine310.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine96.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine768.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine150.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNineJumbo1600.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-miniMoth.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-miniMoth.png","name":"miniMoth","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-windowsTile336H.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-windowsTile336H.png","name":"windowsTile336H","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoFifteenBySeven1305.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoFifteenBySeven1305.png","name":"videoFifteenBySeven1305","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.8":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-miniMoth.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-windowsTile336H.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoFifteenBySeven1305.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.9":{"renditions":[],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-facebookJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-facebookJumbo.png","name":"facebookJumbo","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.10":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-facebookJumbo.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-watch308.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-watch308.png","name":"watch308","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-watch268.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-watch268.png","name":"watch268","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.11":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-watch308.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-watch268.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.12":{"renditions":[],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia":{"crops":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.1","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.2","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.3","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.4","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.5","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.6","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.7","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.8","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.9","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.10","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.11","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.12","typename":"ImageCrop"}],"__typename":"Image"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.timestampBlock":{"timestamp":"2019-08-01T17:15:31.000Z","align":"LEFT","__typename":"TimestampBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.0":{"__typename":"TextInline","text":"[What you need to know to start the day: ","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.0.formats.0","typename":"ItalicFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.0.formats.0":{"__typename":"ItalicFormat","type":null},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1":{"__typename":"TextInline","text":"Get New York Today in your inbox","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1.formats.0","typename":"ItalicFormat"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1.formats.1","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1.formats.0":{"__typename":"ItalicFormat","type":null},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1.formats.1":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002Fnewsletters\u002Fnewyorktoday?module=inline","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.2":{"__typename":"TextInline","text":".]","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.2.formats.0","typename":"ItalicFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.2.formats.0":{"__typename":"ItalicFormat","type":null},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.0":{"__typename":"TextInline","text":"The New York Police Department has been loading ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.1":{"__typename":"TextInline","text":"thousands of arrest photos of children and teenagers","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.2":{"__typename":"TextInline","text":" into a facial recognition database despite evidence the technology has a higher risk of false matches in younger faces. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.0":{"__typename":"TextInline","text":"For about four years, internal records show, the department has used the technology to compare crime scene images with its collection of juvenile mug ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.1":{"__typename":"TextInline","text":"shots","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.2":{"__typename":"TextInline","text":", the photos that are taken at an arrest. Most of the photos are of teenagers, largely 13 to 16 years old, but children as young as 11 have been included. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.6.content.0":{"__typename":"TextInline","text":"Elected officials and civil rights groups said the disclosure that the city was deploying a powerful surveillance tool on adolescents — whose privacy seems sacrosanct and whose status is protected in the criminal justice system — was a striking example of the Police Department’s ability to adopt advancing technology with little public scrutiny.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.8.content.0":{"__typename":"TextInline","text":"Several members of the City Council as well as a range of civil liberties groups said they were unaware of the policy until they were contacted by The New York Times. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.0":{"__typename":"TextInline","text":"Police ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.1":{"__typename":"TextInline","text":"Department officials defended the decision, ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.2":{"__typename":"TextInline","text":"saying it was just the latest evolution of a longstanding policing technique: using arrest photos to identify suspects.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.12.content.0":{"__typename":"TextInline","text":"“I don’t think this is any secret decision that’s made behind closed doors,” the city’s chief of detectives, Dermot F. Shea, said in an interview. “This is just process, and making sure we’re doing everything to fight crime.” ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.0":{"__typename":"TextInline","text":"Other cities have begun to debate whether law enforcement should use facial recognition, which relies on an algorithm to quickly pore through images and suggest matches. In May, ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.1":{"__typename":"TextInline","text":"San Francisco blocked city agencies, including the police","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.1.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.1.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F05\u002F14\u002Fus\u002Ffacial-recognition-ban-san-francisco.html?module=inline","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.2":{"__typename":"TextInline","text":", from using the tool amid unease about potential government ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.3":{"__typename":"TextInline","text":"abuse","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.4":{"__typename":"TextInline","text":". ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.5":{"__typename":"TextInline","text":"Detroit is facing public resistance to a technology ","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.5.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.5.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F07\u002F08\u002Fus\u002Fdetroit-facial-recognition-cameras.html","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.6":{"__typename":"TextInline","text":"that has been shown to have lower accuracy with people with darker skin. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16.content.0":{"__typename":"TextInline","text":"In New York, the state Education Department recently told the Lockport, N.Y., ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16.content.1":{"__typename":"TextInline","text":"school district to delay a plan to use facial recognition on students, citing privacy concerns. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.0":{"__typename":"TextInline","text":"“At the end ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.1":{"__typename":"TextInline","text":"of the day, it should be banned — no young people,” said ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.2":{"__typename":"TextInline","text":"Councilman Donovan Richards","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.3":{"__typename":"TextInline","text":", a Queens Democrat who heads the Public Safety Committee, which oversees the Police Department. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.0":{"__typename":"TextInline","text":"The department said its legal bureau had approved using facial recognition on juveniles. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.1":{"__typename":"TextInline","text":"The algorithm may suggest a lead, but detectives would not make an arrest based solely on ","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.1.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.1.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F06\u002F09\u002Fopinion\u002Ffacial-recognition-police-new-york-city.html","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.2":{"__typename":"TextInline","text":"that","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.2.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.2.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F06\u002F09\u002Fopinion\u002Ffacial-recognition-police-new-york-city.html","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.3":{"__typename":"TextInline","text":", Chief Shea said.","formats":[]},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm":{"id":"SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm","imageType":"photo","url":"\u002Fimagepages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F00nypd-juveniles2.html","uri":"nyt:\u002F\u002Fimage\u002F5a694c3e-2066-51a1-965d-4be8779badef","credit":"Chang W. Lee\u002FThe New York Times","legacyHtmlCaption":"Dermot Shea, the city&rsquo;s chief of detectives, said investigators would not arrest anyone based solely on a facial recognition match.","crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]})":[{"type":"id","generated":true,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).1","typename":"ImageCrop"}],"caption":{"type":"id","generated":true,"id":"$Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.caption","typename":"TextOnlyDocumentBlock"},"__typename":"Image"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg","name":"articleLarge","width":600,"height":400,"__typename":"ImageRendition"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-popup.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-popup.jpg","name":"popup","width":650,"height":433,"__typename":"ImageRendition"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-jumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-jumbo.jpg","name":"jumbo","width":1024,"height":683,"__typename":"ImageRendition"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-superJumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-superJumbo.jpg","name":"superJumbo","width":2048,"height":1365,"__typename":"ImageRendition"},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-popup.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-jumbo.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-superJumbo.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleInline.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleInline.jpg","name":"articleInline","width":190,"height":127,"__typename":"ImageRendition"},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).1":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleInline.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.caption":{"text":"Dermot Shea, the city&rsquo;s chief of detectives, said investigators would not arrest anyone based solely on a facial recognition match.","__typename":"TextOnlyDocumentBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.24.content.0":{"__typename":"TextInline","text":"Still, facial recognition has not been widely tested on children. Most algorithms are taught to “think” based on adult faces, and there is growing evidence that they do not work as well on children. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.0":{"__typename":"TextInline","text":"The National Institute of Standards and Technology","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.1":{"__typename":"TextInline","text":", which is part of the Commerce Department and ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.2":{"__typename":"TextInline","text":"evaluates facial recognition ","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.2.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.2.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fnvlpubs.nist.gov\u002Fnistpubs\u002Fir\u002F2018\u002FNIST.IR.8238.pdf","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.3":{"__typename":"TextInline","text":"algorithms","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.3.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.3.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fnvlpubs.nist.gov\u002Fnistpubs\u002Fir\u002F2018\u002FNIST.IR.8238.pdf","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.4":{"__typename":"TextInline","text":" for accuracy, recently found the ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.5":{"__typename":"TextInline","text":"vast majority of more than 100 ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.6":{"__typename":"TextInline","text":"facial recognition ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.7":{"__typename":"TextInline","text":"algorithms had a higher rate of mistaken matches among children. The ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.8":{"__typename":"TextInline","text":"e","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.9":{"__typename":"TextInline","text":"rror rate was most pronounced in young children but was also seen in those aged 10 to 16.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.0":{"__typename":"TextInline","text":"Aging poses another problem:","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.1":{"__typename":"TextInline","text":" The appearance of children and adolescents can change ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.2":{"__typename":"TextInline","text":" drastically as bones stretch and shift, altering the underlying facial structure. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.0":{"__typename":"TextInline","text":"“I would use extreme caution in using those algorithms,” said ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.1":{"__typename":"TextInline","text":"Karl Ricanek Jr.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.2":{"__typename":"TextInline","text":", a computer science professor and ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.3":{"__typename":"TextInline","text":"co-founder of the Face Aging Group at the University of North Carolina-","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.4":{"__typename":"TextInline","text":"Wilmington","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.5":{"__typename":"TextInline","text":". ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.32.content.0":{"__typename":"TextInline","text":"Technology that can match an image of a younger teenager to a recent arrest photo may be less effective at finding the same person even one or two years later, he said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.34.content.0":{"__typename":"TextInline","text":" “The systems do not have the capacity to understand the dynamic changes that occur to a child’s face,” Dr. Ricanek said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.0":{"__typename":"TextInline","text":"Idemia","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.1":{"__typename":"TextInline","text":" and ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.2":{"__typename":"TextInline","text":"DataWorks Plus","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.3":{"__typename":"TextInline","text":", the two companies that provide facial recognition software to the Police Department, did not respond to requests for comment. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.0":{"__typename":"TextInline","text":"The New York Police Department can take arrest photos of minors as young as ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.1":{"__typename":"TextInline","text":"11","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.2":{"__typename":"TextInline","text":" who are charged with a felony, depending on the severity of the charge. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.40.content.0":{"__typename":"TextInline","text":"And in many cases, the department keeps the photos for years, making facial recognition comparisons to what may have effectively become outdated images. There are photos of 5,500 individuals in the juvenile database, 4,100 of whom are no longer 16 or under, the department said. Teenagers 17 and older are considered adults in the criminal justice system. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.42.content.0":{"__typename":"TextInline","text":"Police officials declined to provide statistics on how often their facial recognition systems provide false matches, or to explain how they evaluate the system’s effectiveness.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.44.content.0":{"__typename":"TextInline","text":"“We are comfortable with this technology because it has proved to be a valuable investigative method,” Chief Shea said. Facial recognition has helped lead to thousands of arrests of both adults and juveniles, the department has said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.46.content.0":{"__typename":"TextInline","text":"Mayor Bill de Blasio had been aware the department was using the technology on minors, said Freddi Goldstein, a spokeswoman for the mayor. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.48.content.0":{"__typename":"TextInline","text":"She said the Police Department followed “strict guidelines” in applying the technology and City Hall monitored the agency’s compliance with the policies. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.0":{"__typename":"TextInline","text":"The Times learned details of the department’s use of facial recognition by reviewing documents posted online earlier this year by ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.1":{"__typename":"TextInline","text":"Clare Garvie, a senior associate","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.1.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.1.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.flawedfacedata.com\u002F#footnote5","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.2":{"__typename":"TextInline","text":" at the Center on Privacy and Technology at Georgetown Law","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.2.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.2.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.flawedfacedata.com\u002F#footnote5","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.3":{"__typename":"TextInline","text":". Ms. Garvie received the documents as part of an open records lawsuit. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.52.content.0":{"__typename":"TextInline","text":"It could not be determined whether other large police departments used facial recognition with juveniles because very few have written policies governing the use of the technology, Ms. Garvie said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54.content.0":{"__typename":"TextInline","text":"New York detectives rely on a vast network","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54.content.1":{"__typename":"TextInline","text":" of surveillance cameras throughout the city to provide images of people believed to have committed a crime. Since 2011, the department has had a dedicated unit of officers who use facial recognition to compare those images against millions of photos, usually mug shots. The software proposes matches, which have led to thousands of arrests, the department said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.56.content.0":{"__typename":"TextInline","text":"By 2013, top police officials were meeting to discuss including juveniles in the program, the documents reviewed by The Times showed. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.58.content.0":{"__typename":"TextInline","text":"The documents showed that the juvenile database had been integrated into the system by 2015. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.59.content.0":{"__typename":"TextInline","text":"“We have these photos. It makes sense,” Chief Shea said in the interview. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61.content.0":{"__typename":"TextInline","text":"State law requires that arrest photos be destroyed if the case is resolved in the juvenile’s favor, or if the child is found to have committed only a misdemeanor, rather than a felony. The photos also must be destroyed if a person reaches age 21 without a criminal record. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61.content.1":{"__typename":"TextInline","text":" ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.63.content.0":{"__typename":"TextInline","text":"When children are charged with crimes, the court system usually takes some steps to prevent their acts from defining them in later years. Children who are 16 and under, for instance, are generally sent to Family Court, where records are not public. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.65.content.0":{"__typename":"TextInline","text":"Yet including their photos in a facial recognition database runs the risk that an imperfect algorithm identifies them as possible suspects in later crimes, civil rights advocates said. A mistaken match could lead investigators to focus on the wrong person from the outset, they said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.67.content.0":{"__typename":"TextInline","text":"“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, a 17-year-old Brooklyn girl who had admitted guilt in Family Court to a group attack that happened when she was 14. She said she was present at the attack but did not participate. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.0":{"__typename":"TextInline","text":"Bailey, who asked that she be identified only by her ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.1":{"__typename":"TextInline","text":"last name","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.2":{"__typename":"TextInline","text":" because she did not want her juvenile arrest to be public, has not been arrested again and is now a student at John Jay College of Criminal Justice. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.0":{"__typename":"TextInline","text":"R","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.1":{"__typename":"TextInline","text":"ecent studies indicate that people of color, as well as children and women, have a greater risk of misidentification than their counterparts, sai","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.2":{"__typename":"TextInline","text":"d ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.3":{"__typename":"TextInline","text":"Joy Buolamwini","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.4":{"__typename":"TextInline","text":", ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.5":{"__typename":"TextInline","text":"the founder of the Algorithmic Justice League and graduate researcher at the M.I.T. Media Lab","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.6":{"__typename":"TextInline","text":", who has examined how human biases are built into artificial intelligence. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.0":{"__typename":"TextInline","text":"The racial disparities in the juvenile justice system are stark: In New York, black and Latino juveniles were charged with crimes at far higher rates than whites in 2017, the most recent year for which numbers were available. Black juveniles outnumbered white juveniles ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.1":{"__typename":"TextInline","text":"more than 15 to 1","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.1.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.1.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.criminaljustice.ny.gov\u002Fcrimnet\u002Fojsa\u002Fjj-reports\u002Fnewyorkcity.pdf","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.2":{"__typename":"TextInline","text":".","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.75.content.0":{"__typename":"TextInline","text":"“If the facial recognition algorithm has a negative bias toward a black population, that will get magnified more toward children,” Dr. Ricanek said, adding that in terms of diminished accuracy, “you’re now putting yourself in unknown territory.”","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0":{"__typename":"Article","promotionalHeadline":"Facial Recognition Makes You Safer","promotionalSummary":"Used properly, the software effectively identifies crime suspects without violating rights.","headline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.headline","typename":"CreativeWorkHeadline"},"summary":"Used properly, the software effectively identifies crime suspects without violating rights.","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F06\u002F09\u002Fopinion\u002Ffacial-recognition-police-new-york-city.html","firstPublished":"2019-06-09T23:00:05.000Z","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.promotionalMedia","typename":"Image"},"section":{"type":"id","generated":false,"id":"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uL2Q3YTcxMTg1LWFhNjAtNTYzNS1iY2UwLTVmYWI3NmM3YzI5Nw==","typename":"Section"},"bylines":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.bylines.0","typename":"Byline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.headline":{"default":"How Facial Recognition Makes You Safer","__typename":"CreativeWorkHeadline"},"ImageRendition:images20190607opinionsunday07Oneill07Oneill-videoLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002Fsunday\u002F07Oneill\u002F07Oneill-videoLarge.jpg","name":"videoLarge","__typename":"ImageRendition"},"ImageRendition:images20190607opinionsunday07Oneill07Oneill-mediumThreeByTwo440.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002Fsunday\u002F07Oneill\u002F07Oneill-mediumThreeByTwo440.jpg","name":"mediumThreeByTwo440","__typename":"ImageRendition"},"ImageRendition:images20190607opinionsunday07Oneill07Oneill-threeByTwoSmallAt2X.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002Fsunday\u002F07Oneill\u002F07Oneill-threeByTwoSmallAt2X.jpg","name":"threeByTwoSmallAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.promotionalMedia.crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]}).0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190607opinionsunday07Oneill07Oneill-videoLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190607opinionsunday07Oneill07Oneill-mediumThreeByTwo440.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190607opinionsunday07Oneill07Oneill-threeByTwoSmallAt2X.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.promotionalMedia":{"crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]})":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.promotionalMedia.crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]}).0","typename":"ImageCrop"}],"__typename":"Image"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1":{"__typename":"Article","promotionalHeadline":"Spying on Children Won’t Keep Them Safe","promotionalSummary":"This week my daughter’s school became the first in the nation to pilot facial-recognition software. The technology’s potential is chilling.","headline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.headline","typename":"CreativeWorkHeadline"},"summary":"This week my daughter’s school became the first in the nation to pilot facial-recognition software. The technology’s potential is chilling.","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F06\u002F07\u002Fopinion\u002Flockport-facial-recognition-schools.html","firstPublished":"2019-06-07T15:00:05.000Z","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.promotionalMedia","typename":"Image"},"section":{"type":"id","generated":false,"id":"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uL2Q3YTcxMTg1LWFhNjAtNTYzNS1iY2UwLTVmYWI3NmM3YzI5Nw==","typename":"Section"},"bylines":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.bylines.0","typename":"Byline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.headline":{"default":"Spying on Children Won’t Keep Them Safe","__typename":"CreativeWorkHeadline"},"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-videoLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002F07shultz-privacy\u002F07shultz-privacy-videoLarge.jpg","name":"videoLarge","__typename":"ImageRendition"},"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-mediumThreeByTwo440.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002F07shultz-privacy\u002F07shultz-privacy-mediumThreeByTwo440.jpg","name":"mediumThreeByTwo440","__typename":"ImageRendition"},"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-threeByTwoSmallAt2X.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002F07shultz-privacy\u002F07shultz-privacy-threeByTwoSmallAt2X.jpg","name":"threeByTwoSmallAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.promotionalMedia.crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]}).0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-videoLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-mediumThreeByTwo440.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-threeByTwoSmallAt2X.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.promotionalMedia":{"crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]})":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.promotionalMedia.crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]}).0","typename":"ImageCrop"}],"__typename":"Image"},"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uL2Q3YTcxMTg1LWFhNjAtNTYzNS1iY2UwLTVmYWI3NmM3YzI5Nw==":{"id":"U2VjdGlvbjpueXQ6Ly9zZWN0aW9uL2Q3YTcxMTg1LWFhNjAtNTYzNS1iY2UwLTVmYWI3NmM3YzI5Nw==","displayName":"Opinion","__typename":"Section"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.bylines.0.creators.0":{"displayName":"James O’Neill","__typename":"Person"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.bylines.0":{"creators":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.bylines.0.creators.0","typename":"Person"}],"__typename":"Byline"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.bylines.0.creators.0":{"displayName":"Jim Shultz","__typename":"Person"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.bylines.0":{"creators":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.bylines.0.creators.0","typename":"Person"}],"__typename":"Byline"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.legacy":{"reviewInformation":"","__typename":"ArticleLegacyData","htmlExtendedAuthorOrArticleInformation":"","htmlInfoBox":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails.socialMedia.0":{"type":"twitter","account":"JoeKGoldstein","__typename":"ContactDetailsSocialMedia"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails.socialMedia.1":{"type":"url","account":"https:\u002F\u002Fwww.nytimes.com\u002Fby\u002Fjoseph-goldstein","__typename":"ContactDetailsSocialMedia"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails":{"socialMedia":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails.socialMedia.0","typename":"ContactDetailsSocialMedia"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails.socialMedia.1","typename":"ContactDetailsSocialMedia"}],"__typename":"ContactDetails"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.legacyData":{"htmlShortBiography":"\u003Cp\u003EJoseph Goldstein writes about policing and the criminal justice system. He has been a reporter at The Times since 2011, and is based in New York. He also worked for a year in the Kabul bureau, reporting on Afghanistan.\u003C\u002Fp\u003E","__typename":"PersonLegacyData"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails.socialMedia.0":{"type":"url","account":"https:\u002F\u002Fwww.nytimes.com\u002Fby\u002Fali-watkins","__typename":"ContactDetailsSocialMedia"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails.socialMedia.1":{"type":"twitter","account":"AliWatkins","__typename":"ContactDetailsSocialMedia"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails":{"socialMedia":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails.socialMedia.0","typename":"ContactDetailsSocialMedia"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails.socialMedia.1","typename":"ContactDetailsSocialMedia"}],"__typename":"ContactDetails"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.legacyData":{"htmlShortBiography":"\u003Cp\u003EAli Watkins is a reporter on the Metro Desk, covering courts and social services. Previously, she covered national security in Washington for The Times, BuzzFeed and McClatchy Newspapers.\u003C\u002Fp\u003E","__typename":"PersonLegacyData"},"ROOT_QUERY":{"workOrLocation({\"id\":\"\u002F2019\u002F08\u002F01\u002Fnyregion\u002Fnypd-facial-recognition-children-teenagers.html\"})":{"type":"id","generated":false,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==","typename":"Article"}}},"config":{"gqlUrl":"https:\u002F\u002Fsamizdat-graphql.nytimes.com\u002Fgraphql\u002Fv2","gqlRequestHeaders":{"nyt-app-type":"project-vi","nyt-app-version":"0.0.5","nyt-token":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs+\u002FoUCTBmD\u002FcLdmcecrnBMHiU\u002FpxQCn2DDyaPKUOXxi4p0uUSZQzsuq1pJ1m5z1i0YGPd1U1OeGHAChWtqoxC7bFMCXcwnE1oyui9G1uobgpm1GdhtwkR7ta7akVTcsF8zxiXx7DNXIPd2nIJFH83rmkZueKrC4JVaNzjvD+Z03piLn5bHWU6+w+rA+kyJtGgZNTXKyPh6EC6o5N+rknNMG5+CdTq35p8f99WjFawSvYgP9V64kgckbTbtdJ6YhVP58TnuYgr12urtwnIqWP9KSJ1e5vmgf3tunMqWNm6+AnsqNj8mCLdCuc5cEB74CwUeQcP2HQQmbCddBy2y0mEwIDAQAB"},"gqlFetchTimeout":4000,"disablePersistedQueries":false,"initialDeviceType":"desktop","fastlyAbraConfig":{},"serviceWorkerFile":"service-worker-test-1565019880489.js"},"ssrQuery":{},"initialLocation":{"pathname":"\u002F2019\u002F08\u002F01\u002Fnyregion\u002Fnypd-facial-recognition-children-teenagers.html"},"externalAssets":[]};</script>
+ <script>!function(e){function r(r){for(var n,i,a=r[0],f=r[1],l=r[2],p=0,s=[];p<a.length;p++)i=a[p],o[i]&&s.push(o[i][0]),o[i]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var f=t[a];0!==o[f]&&(n=!1)}n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={37:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,function(r){return e[r]}.bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="/vi-assets/static-assets/";var a=window.webpackJsonp=window.webpackJsonp||[],f=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var c=f;t()}([]);
+//# sourceMappingURL=runtime~adslot-a45e9d5711d983de8fda.js.map</script>
+ <script async src="/vi-assets/static-assets/adslot-88dc25fbfb7328ff1466.js"></script>
+ <script>!function(e){function r(r){for(var o,n,c=r[0],i=r[1],s=r[2],f=0,l=[];f<c.length;f++)n=c[f],d[n]&&l.push(d[n][0]),d[n]=0;for(o in i)Object.prototype.hasOwnProperty.call(i,o)&&(e[o]=i[o]);for(b&&b(r);l.length;)l.shift()();return a.push.apply(a,s||[]),t()}function t(){for(var e,r=0;r<a.length;r++){for(var t=a[r],o=!0,c=1;c<t.length;c++){var i=t[c];0!==d[i]&&(o=!1)}o&&(a.splice(r--,1),e=n(n.s=t[0]))}return e}var o={},d={39:0},a=[];function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,n),t.l=!0,t.exports}n.e=function(e){var r=[],t=d[e];if(0!==t)if(t)r.push(t[2]);else{var o=new Promise(function(r,o){t=d[e]=[r,o]});r.push(t[2]=o);var a,c=document.createElement("script");c.charset="utf-8",c.timeout=120,n.nc&&c.setAttribute("nonce",n.nc),c.src=function(e){return n.p+""+({1:"answerpage~bestsellers~collections~hubpage~reviews~search~slideshow~timeswire~weddings",2:"vendors~audio~home~paidpost~story~trending~video",3:"bestsellers~byline~collections~reviews~trending",4:"vendors~answerpage~audio~slideshow~story",5:"vendors~audio~home~paidpost~story",6:"byline~timeswire~your-list",7:"answerpage~getstarted",8:"bestsellers~hubpage",9:"newsletter~regilite",10:"vendors~paidpost~video",11:"vendors~video~videoblock",13:"answerpage",14:"audio",15:"audioblock",16:"bestsellers",17:"blank",18:"byline",19:"coderedeem",20:"collections",21:"comments",22:"episodefooter",23:"getstarted",24:"home",25:"hubpage",28:"newsletter",29:"newsletters",30:"paidpost",31:"privacy",32:"programmables",33:"recirculation",34:"refer",35:"regilite",36:"reviews",40:"search",41:"slideshow",42:"stickyfilljs",43:"story",44:"timeswire",45:"trending",46:"vendors~audioblock",47:"vendors~collections",48:"vendors~episodefooter",49:"vendors~home",50:"vendors~slideshow",51:"video",52:"videoblock",53:"weddings",54:"your-list"}[e]||e)+"-"+{1:"3f4fa7221ef1476092a3",2:"99859b76d5b5d9a29339",3:"6f48de596aff21cee9e2",4:"4a8420b672b0eb786710",5:"ebc6aacf5f0b0f00d939",6:"cb31ca27d295accb8d47",7:"80921fe67fb06673afe2",8:"2cb427a40932c00d5467",9:"c17835f4020a81b3ebdf",10:"e6333c5f0c9d44a562b9",11:"340b908d6bbf26111cf8",13:"8c464e3538096d914776",14:"926f804d67e8a45a9f10",15:"287cb8154113b7f25784",16:"f4baccd76f2f8e8d9db8",17:"2102d3a3d664a932bad1",18:"7e235f2b3d6d19b68ded",19:"dd19d8e9f879d86abb75",20:"74e3e7b1d52b7fc14653",21:"bfae7d48bcf7e6c8ab89",22:"d32caaca6c5936978d4a",23:"300b3f609b3056db6c18",24:"e7c1959c1d8ba140707f",25:"c0e7bb29b120c3c2d802",28:"3ae59c9859d057a0249a",29:"e951dbf493cdf3558858",30:"c108afd87ed307bd7c43",31:"493cddaf9cad7abf670c",32:"3c3cfd695943ed02249d",33:"dc560ec354d5e74e6e39",34:"3202500fd0bc711d8680",35:"67182278afc38ad823b1",36:"f189bd767bbe13f59254",40:"33f98b7462fec3740a1d",41:"1d22c2cff98639b0c7b7",42:"8640087ba86873ebebae",43:"5230dd3423d03f5eb0b8",44:"2db55c4529c54890b4bd",45:"4614204aab86dd2d820f",46:"8574ab7f8faa5e4151d8",47:"07007634cf48d865ae1a",48:"1e063f58b4e3da82fc25",49:"6e63337189383a709584",50:"ab09592f64b71ce13dba",51:"35bd41b25aecc8dbc38b",52:"e71ac27943b9c0dc2f1c",53:"65894e06a558684c455b",54:"cf5b2e08b6f7a84842e2"}[e]+".js"}(e),a=function(r){c.onerror=c.onload=null,clearTimeout(i);var t=d[e];if(0!==t){if(t){var o=r&&("load"===r.type?"missing":r.type),a=r&&r.target&&r.target.src,n=new Error("Loading chunk "+e+" failed.\n("+o+": "+a+")");n.type=o,n.request=a,t[1](n)}d[e]=void 0}};var i=setTimeout(function(){a({type:"timeout",target:c})},12e4);c.onerror=c.onload=a,document.head.appendChild(c)}return Promise.all(r)},n.m=e,n.c=o,n.d=function(e,r,t){n.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,r){if(1&r&&(e=n(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(n.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var o in e)n.d(t,o,function(r){return e[r]}.bind(null,o));return t},n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,"a",r),r},n.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},n.p="/vi-assets/static-assets/",n.oe=function(e){throw console.error(e),e};var c=window.webpackJsonp=window.webpackJsonp||[],i=c.push.bind(c);c.push=r,c=c.slice();for(var s=0;s<c.length;s++)r(c[s]);var b=i;t()}([]);
+//# sourceMappingURL=runtime~main-262212ad851d651999bf.js.map</script>
+ <script defer src="/vi-assets/static-assets/vendor-3389f9c978bdc7cb443c.js"></script>
+ <script defer src="/vi-assets/static-assets/story-5230dd3423d03f5eb0b8.js"></script>
+ <script defer src="/vi-assets/static-assets/main-72d661c291004bc90d1b.js"></script>
+ <script>
+(function(w, l) {
+ w[l] = w[l] || [];
+ w[l].push({
+ 'gtm.start': new Date().getTime(),
+ event: 'gtm.js'
+ });
+})(window, 'dataLayer');
+(function(){
+ var url = 'https://et.nytimes.com/pixel' +
+ '?url=' + window.location.href +
+ '&referrer=' + document.referrer +
+ '&subject=module-interactions' +
+ '&moduleData=%7B%22module%22%3A%22nyt-vi-page-pixel%22%2C%22pgType%22%3A%22%22%2C%22eventName%22%3A%22Impression%22%2C%22action%22%3A%22Impression%22%7D' +
+ '&sourceApp=nyt-vi&instant=1' +
+ '&_=' + Date.now();
+ var img = document.createElement('img');
+ img.src = url;
+ img.alt = "";
+ img.style.cssText = 'position: absolute; z-index: -999999; left: -1000px; top: -1000px;';
+ document.body.appendChild(img);
+})();
+</script>
+ <script defer src="https://www.googletagmanager.com/gtm.js?id=GTM-P528B3&gtm_auth=tfAzqo1rYDLgYhmTnSjPqw&gtm_preview=env-130&gtm_cookies_win=x"></script>
+<noscript>
+<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-P528B3&gtm_auth=tfAzqo1rYDLgYhmTnSjPqw&gtm_preview=env-130&gtm_cookies_win=x" height="0" width="0" style="display:none;visibility:hidden"></iframe>
+</noscript>
+ <div id="RavenInstaller">
+<script>
+if (window.INSTALL_RAVEN) {
+ window.addEventListener('load', function(event) {
+ var includeRaven = document.getElementById("RavenInstaller");
+ var script = document.createElement("script");
+ script.src = "/vi-assets/static-assets/raven.min-830a6d04a55c283934dd1893d6ddc66d.js";
+ script.onload = function() {
+ /* eslint-disable */
+// Install Raven
+window.Raven.config('https://7bc8bccf5c254286a99b11c68f6bf4ce@sentry.io/178860', {
+ release: vi.env.RELEASE,
+ environment: vi.env.ENVIRONMENT,
+ ignoreErrors: [/SecurityError: Blocked a frame with origin.*/]
+}).install(); // Stop using our error handler
+
+window.nyt_errors.ravenInstalled = true;
+var regex = /nyt-a=(.*?)(;|$)/;
+var id = regex.exec(document.cookie);
+
+if (id !== null) {
+ id = id[1];
+} else {
+ id = '';
+} // Setting nyt-a as user context
+
+
+window.Raven.setUserContext({
+ id: id
+}); // Pass collected errors to Raven
+
+window.nyt_errors.list.forEach(function (err) {
+ // weird?
+ if (!err) {
+ return;
+ } // also weird ... ?
+
+
+ if (!err.err) {
+ // maybe err itself is an Error?
+ if (err instanceof Error) {
+ window.Raven.captureException(err, err.data || {});
+ } // else { silently ignore? }
+
+ } // just making sure ...
+
+
+ if (err.err instanceof Error) {
+ window.Raven.captureException(err.err, err.data || {});
+ } // else { silently ignore? }
+
+}); // Pass collected Tags to Raven
+
+window.nyt_errors.tags.forEach(function (tag) {
+ window.Raven.setTagsContext(tag);
+});
+ };
+ includeRaven.appendChild(script);
+ });
+}
+</script>
+</div>
+
+
+ </body>
+</html>
diff --git a/test/fixtures/relay/accept-follow.json b/test/fixtures/relay/accept-follow.json
new file mode 100644
index 000000000..1b166f2da
--- /dev/null
+++ b/test/fixtures/relay/accept-follow.json
@@ -0,0 +1,15 @@
+{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "actor": "https://relay.mastodon.host/actor",
+ "id": "https://relay.mastodon.host/activities/ec477b69-db26-4019-923e-cf809de516ab",
+ "object": {
+ "actor": "{{ap_id}}",
+ "id": "{{activity_id}}",
+ "object": "https://relay.mastodon.host/actor",
+ "type": "Follow"
+ },
+ "to": [
+ "{{ap_id}}"
+ ],
+ "type": "Accept"
+} \ No newline at end of file
diff --git a/test/fixtures/relay/relay.json b/test/fixtures/relay/relay.json
new file mode 100644
index 000000000..77ae7f06c
--- /dev/null
+++ b/test/fixtures/relay/relay.json
@@ -0,0 +1,20 @@
+{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "endpoints": {
+ "sharedInbox": "https://relay.mastodon.host/inbox"
+ },
+ "followers": "https://relay.mastodon.host/followers",
+ "following": "https://relay.mastodon.host/following",
+ "inbox": "https://relay.mastodon.host/inbox",
+ "name": "ActivityRelay",
+ "type": "Application",
+ "id": "https://relay.mastodon.host/actor",
+ "publicKey": {
+ "id": "https://relay.mastodon.host/actor#main-key",
+ "owner": "https://relay.mastodon.host/actor",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuNYHNYETdsZFsdcTTEQo\nlsTP9yz4ZjOGrQ1EjoBA7NkjBUxxUAPxZbBjWPT9F+L3IbCX1IwI2OrBM/KwDlug\nV41xnjNmxSCUNpxX5IMZtFaAz9/hWu6xkRTs9Bh6XWZxi+db905aOqszb9Mo3H2g\nQJiAYemXwTh2kBO7XlBDbsMhO11Tu8FxcWTMdR54vlGv4RoiVh8dJRa06yyiTs+m\njbj/OJwR06mHHwlKYTVT/587NUb+e9QtCK6t/dqpyZ1o7vKSK5PSldZVjwHt292E\nXVxFOQVXi7JazTwpdPww79ECSe8ThCykOYCNkm3RjsKuLuokp7Vzq1hXIoeBJ7z2\ndU8vbgg/JyazsOsTxkVs2nd2i9/QW2SH+sX9X3357+XLSCh/A8p8fv/GeoN7UCXe\n4DWHFJZDlItNFfymiPbQH+omuju8qrfW9ngk1gFeI2mahXFQVu7x0qsaZYioCIrZ\nwq0zPnUGl9u0tLUXQz+ZkInRrEz+JepDVauy5/3QdzMLG420zCj/ygDrFzpBQIrc\n62Z6URueUBJox0UK71K+usxqOrepgw8haFGMvg3STFo34pNYjoK4oKO+h5qZEDFD\nb1n57t6JWUaBocZbJns9RGASq5gih+iMk2+zPLWp1x64yvuLsYVLPLBHxjCxS6lA\ndWcopZHi7R/OsRz+vTT7420CAwEAAQ==\n-----END PUBLIC KEY-----"
+ },
+ "summary": "ActivityRelay bot",
+ "preferredUsername": "relay",
+ "url": "https://relay.mastodon.host/actor"
+} \ No newline at end of file
diff --git a/test/fixtures/tesla_mock/mobilizon.org-event.json b/test/fixtures/tesla_mock/mobilizon.org-event.json
new file mode 100644
index 000000000..7411cf817
--- /dev/null
+++ b/test/fixtures/tesla_mock/mobilizon.org-event.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://litepub.social/litepub/context.jsonld",{"GeoCoordinates":"sc:GeoCoordinates","Hashtag":"as:Hashtag","Place":"sc:Place","PostalAddress":"sc:PostalAddress","address":{"@id":"sc:address","@type":"sc:PostalAddress"},"addressCountry":"sc:addressCountry","addressLocality":"sc:addressLocality","addressRegion":"sc:addressRegion","category":"sc:category","commentsEnabled":{"@id":"pt:commentsEnabled","@type":"sc:Boolean"},"geo":{"@id":"sc:geo","@type":"sc:GeoCoordinates"},"ical":"http://www.w3.org/2002/12/cal/ical#","joinMode":{"@id":"mz:joinMode","@type":"mz:joinModeType"},"joinModeType":{"@id":"mz:joinModeType","@type":"rdfs:Class"},"location":{"@id":"sc:location","@type":"sc:Place"},"maximumAttendeeCapacity":"sc:maximumAttendeeCapacity","mz":"https://joinmobilizon.org/ns#","postalCode":"sc:postalCode","pt":"https://joinpeertube.org/ns#","repliesModerationOption":{"@id":"mz:repliesModerationOption","@type":"mz:repliesModerationOptionType"},"repliesModerationOptionType":{"@id":"mz:repliesModerationOptionType","@type":"rdfs:Class"},"sc":"http://schema.org#","streetAddress":"sc:streetAddress","uuid":"sc:identifier"}],"actor":"https://mobilizon.org/@tcit","attributedTo":"https://mobilizon.org/@tcit","category":"meeting","cc":[],"commentsEnabled":true,"content":"<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's still <u>a work in progress</u>: if reports made from an instance on events and comments can be federated, you can'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't advise to do that for now, we have a little documentation but it's quite the early days and you'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>","endTime":"2019-12-18T14:00:00Z","ical:status":"CONFIRMED","id":"https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39","joinMode":"free","location":{"address":{"addressCountry":"France","addressLocality":"Nantes","addressRegion":"Pays de la Loire","postalCode":null,"streetAddress":" ","type":"PostalAddress"},"geo":{"latitude":-1.54939699141711,"longitude":47.21617415,"type":"GeoCoordinates"},"id":"https://mobilizon.org/address/1368fdab-1e2c-4de6-bcff-a90c84abdee1","name":"Cour du Château des Ducs de Bretagne","type":"Place"},"maximumAttendeeCapacity":0,"mediaType":"text/html","name":"Mobilizon Launching Party","published":"2019-12-17T11:33:56Z","repliesModerationOption":"allow_all","startTime":"2019-12-18T13:00:00Z","tag":[{"href":"https://mobilizon.org/tags/mobilizon","name":"#Mobilizon","type":"Hashtag"},{"href":"https://mobilizon.org/tags/federation","name":"#Federation","type":"Hashtag"},{"href":"https://mobilizon.org/tags/activitypub","name":"#ActivityPub","type":"Hashtag"},{"href":"https://mobilizon.org/tags/party","name":"#Party","type":"Hashtag"}],"to":["https://www.w3.org/ns/activitystreams#Public"],"type":"Event","updated":"2019-12-17T12:25:01Z","url":"https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39","uuid":"252d5816-00a3-4a89-a66f-15bf65c33e39"} \ No newline at end of file
diff --git a/test/fixtures/tesla_mock/mobilizon.org-user.json b/test/fixtures/tesla_mock/mobilizon.org-user.json
new file mode 100644
index 000000000..f948ae5f0
--- /dev/null
+++ b/test/fixtures/tesla_mock/mobilizon.org-user.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://litepub.social/litepub/context.jsonld",{"GeoCoordinates":"sc:GeoCoordinates","Hashtag":"as:Hashtag","Place":"sc:Place","PostalAddress":"sc:PostalAddress","address":{"@id":"sc:address","@type":"sc:PostalAddress"},"addressCountry":"sc:addressCountry","addressLocality":"sc:addressLocality","addressRegion":"sc:addressRegion","category":"sc:category","commentsEnabled":{"@id":"pt:commentsEnabled","@type":"sc:Boolean"},"geo":{"@id":"sc:geo","@type":"sc:GeoCoordinates"},"ical":"http://www.w3.org/2002/12/cal/ical#","joinMode":{"@id":"mz:joinMode","@type":"mz:joinModeType"},"joinModeType":{"@id":"mz:joinModeType","@type":"rdfs:Class"},"location":{"@id":"sc:location","@type":"sc:Place"},"maximumAttendeeCapacity":"sc:maximumAttendeeCapacity","mz":"https://joinmobilizon.org/ns#","postalCode":"sc:postalCode","pt":"https://joinpeertube.org/ns#","repliesModerationOption":{"@id":"mz:repliesModerationOption","@type":"mz:repliesModerationOptionType"},"repliesModerationOptionType":{"@id":"mz:repliesModerationOptionType","@type":"rdfs:Class"},"sc":"http://schema.org#","streetAddress":"sc:streetAddress","uuid":"sc:identifier"}],"endpoints":{"sharedInbox":"https://mobilizon.org/inbox"},"followers":"https://mobilizon.org/@tcit/followers","following":"https://mobilizon.org/@tcit/following","icon":{"mediaType":null,"type":"Image","url":"https://mobilizon.org/media/3a5f18c058a8193b1febfaf561f94ae8b91f85ac64c01ddf5ad7b251fb43baf5.jpg?name=profil.jpg"},"id":"https://mobilizon.org/@tcit","inbox":"https://mobilizon.org/@tcit/inbox","manuallyApprovesFollowers":false,"name":"Thomas Citharel","outbox":"https://mobilizon.org/@tcit/outbox","preferredUsername":"tcit","publicKey":{"id":"https://mobilizon.org/@tcit#main-key","owner":"https://mobilizon.org/@tcit","publicKeyPem":"-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAtzuZFviv5f12SuA0wZFMuwKS8RIlT3IjPCMLRDhiorZeV3UJ1lik\nDYO6mEh22KDXYgJtNVSYGF0Q5LJivgcvuvU+VQ048iTB1B2x0rHMr47KPByPjfVb\nKDeHt6fkHcLY0JK8UkIxW542wXAg4jX5w3gJi3pgTQrCT8VNyPbH1CaA0uW//9jc\nqzZQVFzpfdJoVOM9E3Urc/u58HC4xOptlM7+B/594ZI9drYwy5m+ZxHwlQUYCva4\n34dvwsfOGxkQyIrzXoep80EnWnFpYCLMcCiz+sEhPYxqLgNE+Cmn/6pv7SIscz6p\neVlQXIchdw+J4yl07paJDkFc6CNTCmaIHQIDAQAB\n-----END RSA PUBLIC KEY-----\n\n"},"summary":"Main profile","type":"Person","url":"https://mobilizon.org/@tcit"} \ No newline at end of file
diff --git a/test/following_relationship_test.exs b/test/following_relationship_test.exs
index 93c079814..865bb3838 100644
--- a/test/following_relationship_test.exs
+++ b/test/following_relationship_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.FollowingRelationshipTest do
diff --git a/test/formatter_test.exs b/test/formatter_test.exs
index 087bdbcc2..cf8441cf6 100644
--- a/test/formatter_test.exs
+++ b/test/formatter_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.FormatterTest do
@@ -119,7 +119,20 @@ defmodule Pleroma.FormatterTest do
end
end
- describe "add_user_links" do
+ describe "Formatter.linkify" do
+ test "correctly finds mentions that contain the domain name" do
+ _user = insert(:user, %{nickname: "lain"})
+ _remote_user = insert(:user, %{nickname: "lain@lain.com", local: false})
+
+ text = "hey @lain@lain.com what's up"
+
+ {_text, mentions, []} = Formatter.linkify(text)
+ [{username, user}] = mentions
+
+ assert username == "@lain@lain.com"
+ assert user.nickname == "lain@lain.com"
+ end
+
test "gives a replacement for user links, using local nicknames in user links text" do
text = "@gsimg According to @archa_eme_, that is @daggsy. Also hello @archaeme@archae.me"
gsimg = insert(:user, %{nickname: "gsimg"})
diff --git a/test/healthcheck_test.exs b/test/healthcheck_test.exs
index 66d5026ff..e341e6983 100644
--- a/test/healthcheck_test.exs
+++ b/test/healthcheck_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HealthcheckTest do
diff --git a/test/html_test.exs b/test/html_test.exs
index c918dbe20..a006fd492 100644
--- a/test/html_test.exs
+++ b/test/html_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTMLTest do
diff --git a/test/http/request_builder_test.exs b/test/http/request_builder_test.exs
index 80ef25d7b..11a9314ae 100644
--- a/test/http/request_builder_test.exs
+++ b/test/http/request_builder_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.RequestBuilderTest do
@@ -9,6 +9,7 @@ defmodule Pleroma.HTTP.RequestBuilderTest do
describe "headers/2" do
clear_config([:http, :send_user_agent])
+ clear_config([:http, :user_agent])
test "don't send pleroma user agent" do
assert RequestBuilder.headers(%{}, []) == %{headers: []}
diff --git a/test/http_test.exs b/test/http_test.exs
index 5f9522cf0..3edb0de36 100644
--- a/test/http_test.exs
+++ b/test/http_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTPTest do
diff --git a/test/integration/mastodon_websocket_test.exs b/test/integration/mastodon_websocket_test.exs
index 63fce07bb..bd229c55f 100644
--- a/test/integration/mastodon_websocket_test.exs
+++ b/test/integration/mastodon_websocket_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Integration.MastodonWebsocketTest do
diff --git a/test/job_queue_monitor_test.exs b/test/job_queue_monitor_test.exs
index 17c6f3246..65c1e9f29 100644
--- a/test/job_queue_monitor_test.exs
+++ b/test/job_queue_monitor_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.JobQueueMonitorTest do
diff --git a/test/keys_test.exs b/test/keys_test.exs
index 059f70b74..9e8528cba 100644
--- a/test/keys_test.exs
+++ b/test/keys_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.KeysTest do
diff --git a/test/list_test.exs b/test/list_test.exs
index e7b23915b..b5572cbae 100644
--- a/test/list_test.exs
+++ b/test/list_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ListTest do
diff --git a/test/marker_test.exs b/test/marker_test.exs
index 04bd67fe6..c80ae16b6 100644
--- a/test/marker_test.exs
+++ b/test/marker_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MarkerTest do
diff --git a/test/moderation_log_test.exs b/test/moderation_log_test.exs
index 4240f6a65..59f4d67f8 100644
--- a/test/moderation_log_test.exs
+++ b/test/moderation_log_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ModerationLogTest do
@@ -214,7 +214,7 @@ defmodule Pleroma.ModerationLogTest do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
- action: "report_response",
+ action: "report_note",
subject: report,
text: "look at this"
})
@@ -222,7 +222,7 @@ defmodule Pleroma.ModerationLogTest do
log = Repo.one(ModerationLog)
assert log.data["message"] ==
- "@#{moderator.nickname} responded with 'look at this' to report ##{report.id}"
+ "@#{moderator.nickname} added note 'look at this' to report ##{report.id}"
end
test "logging status sensitivity update", %{moderator: moderator} do
diff --git a/test/notification_test.exs b/test/notification_test.exs
index 2200d03ea..81eb0e2f7 100644
--- a/test/notification_test.exs
+++ b/test/notification_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.NotificationTest do
@@ -14,9 +14,19 @@ defmodule Pleroma.NotificationTest do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Streamer
- import ExUnit.CaptureLog
-
describe "create_notifications" do
+ 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, _object} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
+
+ {:ok, [notification]} = Notification.create_notifications(activity)
+
+ assert notification.user_id == user.id
+ end
+
test "notifies someone when they are directly addressed" do
user = insert(:user)
other_user = insert(:user)
@@ -95,12 +105,12 @@ defmodule Pleroma.NotificationTest do
activity = insert(:note_activity)
author = User.get_cached_by_ap_id(activity.data["actor"])
user = insert(:user)
- {:ok, user} = User.block(user, author)
+ {:ok, _user_relationship} = User.block(user, author)
assert Notification.create_notification(activity, user)
end
- test "it creates a notificatin for the user if the user mutes the activity author" do
+ 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)
@@ -114,7 +124,7 @@ defmodule Pleroma.NotificationTest do
muter = insert(:user)
muted = insert(:user)
- {:ok, muter} = User.mute(muter, muted, false)
+ {:ok, _user_relationships} = User.mute(muter, muted, false)
{:ok, activity} = CommonAPI.post(muted, %{"status" => "Hi @#{muter.nickname}"})
@@ -138,7 +148,10 @@ defmodule Pleroma.NotificationTest do
test "it disables notifications from followers" do
follower = insert(:user)
- followed = insert(:user, notification_settings: %{"followers" => false})
+
+ followed =
+ insert(:user, notification_settings: %Pleroma.User.NotificationSetting{followers: false})
+
User.follow(follower, followed)
{:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"})
refute Notification.create_notification(activity, followed)
@@ -146,13 +159,20 @@ defmodule Pleroma.NotificationTest do
test "it disables notifications from non-followers" do
follower = insert(:user)
- followed = insert(:user, notification_settings: %{"non_followers" => false})
+
+ followed =
+ insert(:user,
+ notification_settings: %Pleroma.User.NotificationSetting{non_followers: false}
+ )
+
{:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"})
refute Notification.create_notification(activity, followed)
end
test "it disables notifications from people the user follows" do
- follower = insert(:user, notification_settings: %{"follows" => false})
+ follower =
+ insert(:user, notification_settings: %Pleroma.User.NotificationSetting{follows: false})
+
followed = insert(:user)
User.follow(follower, followed)
follower = Repo.get(User, follower.id)
@@ -161,7 +181,9 @@ defmodule Pleroma.NotificationTest do
end
test "it disables notifications from people the user does not follow" do
- follower = insert(:user, notification_settings: %{"non_follows" => false})
+ follower =
+ insert(:user, notification_settings: %Pleroma.User.NotificationSetting{non_follows: false})
+
followed = insert(:user)
{:ok, activity} = CommonAPI.post(followed, %{"status" => "hey @#{follower.nickname}"})
refute Notification.create_notification(activity, follower)
@@ -535,9 +557,7 @@ defmodule Pleroma.NotificationTest do
assert Enum.empty?(Notification.for_user(user))
- assert capture_log(fn ->
- {:error, _} = CommonAPI.favorite(other_user, activity.id)
- end) =~ "[error]"
+ {:error, :not_found} = CommonAPI.favorite(other_user, activity.id)
assert Enum.empty?(Notification.for_user(user))
end
@@ -647,13 +667,17 @@ defmodule Pleroma.NotificationTest do
Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user)
ObanHelpers.perform_all()
+ assert [] = Notification.for_user(follower)
+
assert [
%{
activity: %{
data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id}
}
}
- ] = Notification.for_user(follower)
+ ] = Notification.for_user(follower, %{with_move: true})
+
+ assert [] = Notification.for_user(other_follower)
assert [
%{
@@ -661,7 +685,7 @@ defmodule Pleroma.NotificationTest do
data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id}
}
}
- ] = Notification.for_user(other_follower)
+ ] = Notification.for_user(other_follower, %{with_move: true})
end
end
@@ -669,7 +693,7 @@ defmodule Pleroma.NotificationTest do
test "it returns notifications for muted user without notifications" do
user = insert(:user)
muted = insert(:user)
- {:ok, user} = User.mute(user, muted, false)
+ {:ok, _user_relationships} = User.mute(user, muted, false)
{:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
@@ -679,7 +703,7 @@ defmodule Pleroma.NotificationTest do
test "it doesn't return notifications for muted user with notifications" do
user = insert(:user)
muted = insert(:user)
- {:ok, user} = User.mute(user, muted)
+ {:ok, _user_relationships} = User.mute(user, muted)
{:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
@@ -689,7 +713,7 @@ defmodule Pleroma.NotificationTest do
test "it doesn't return notifications for blocked user" do
user = insert(:user)
blocked = insert(:user)
- {:ok, user} = User.block(user, blocked)
+ {:ok, _user_relationship} = User.block(user, blocked)
{:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
@@ -719,7 +743,7 @@ defmodule Pleroma.NotificationTest do
test "it returns notifications from a muted user when with_muted is set" do
user = insert(:user)
muted = insert(:user)
- {:ok, user} = User.mute(user, muted)
+ {:ok, _user_relationships} = User.mute(user, muted)
{:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
@@ -729,11 +753,11 @@ defmodule Pleroma.NotificationTest do
test "it doesn't return notifications from a blocked user when with_muted is set" do
user = insert(:user)
blocked = insert(:user)
- {:ok, user} = User.block(user, blocked)
+ {:ok, _user_relationship} = User.block(user, blocked)
{:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
- assert length(Notification.for_user(user, %{with_muted: true})) == 0
+ assert Enum.empty?(Notification.for_user(user, %{with_muted: true}))
end
test "it doesn't return notifications from a domain-blocked user when with_muted is set" do
@@ -743,7 +767,7 @@ defmodule Pleroma.NotificationTest do
{:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
- assert length(Notification.for_user(user, %{with_muted: true})) == 0
+ assert Enum.empty?(Notification.for_user(user, %{with_muted: true}))
end
test "it returns notifications from muted threads when with_muted is set" do
diff --git a/test/object/containment_test.exs b/test/object/containment_test.exs
index 7636803a6..90b6dccf2 100644
--- a/test/object/containment_test.exs
+++ b/test/object/containment_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object.ContainmentTest do
diff --git a/test/object/fetcher_test.exs b/test/object/fetcher_test.exs
index 9ae6b015d..4775ee152 100644
--- a/test/object/fetcher_test.exs
+++ b/test/object/fetcher_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object.FetcherTest do
@@ -26,6 +26,31 @@ defmodule Pleroma.Object.FetcherTest do
:ok
end
+ describe "max thread distance restriction" do
+ @ap_id "http://mastodon.example.org/@admin/99541947525187367"
+
+ clear_config([:instance, :federation_incoming_replies_max_depth])
+
+ test "it returns thread depth exceeded error if thread depth is exceeded" do
+ Pleroma.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
+ Pleroma.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
+ Pleroma.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")
@@ -77,6 +102,15 @@ defmodule Pleroma.Object.FetcherTest do
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")
diff --git a/test/object_test.exs b/test/object_test.exs
index 643b50ae6..703fd6e0f 100644
--- a/test/object_test.exs
+++ b/test/object_test.exs
@@ -1,15 +1,17 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# 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
@@ -71,6 +73,188 @@ defmodule Pleroma.ObjectTest do
end
end
+ describe "delete attachments" do
+ clear_config([Pleroma.Upload])
+ 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{} =
diff --git a/test/pagination_test.exs b/test/pagination_test.exs
index c0fbe7933..d5b1b782d 100644
--- a/test/pagination_test.exs
+++ b/test/pagination_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.PaginationTest do
diff --git a/test/plugs/admin_secret_authentication_plug_test.exs b/test/plugs/admin_secret_authentication_plug_test.exs
index 506b1f609..2e300ac0c 100644
--- a/test/plugs/admin_secret_authentication_plug_test.exs
+++ b/test/plugs/admin_secret_authentication_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.AdminSecretAuthenticationPlugTest do
@@ -23,6 +23,8 @@ defmodule Pleroma.Plugs.AdminSecretAuthenticationPlugTest do
end
describe "when secret set it assigns an admin user" do
+ clear_config([:admin_token])
+
test "with `admin_token` query parameter", %{conn: conn} do
Pleroma.Config.put(:admin_token, "password123")
diff --git a/test/plugs/authentication_plug_test.exs b/test/plugs/authentication_plug_test.exs
index 9ae4c506f..ae2f3f8ec 100644
--- a/test/plugs/authentication_plug_test.exs
+++ b/test/plugs/authentication_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.AuthenticationPlugTest do
diff --git a/test/plugs/basic_auth_decoder_plug_test.exs b/test/plugs/basic_auth_decoder_plug_test.exs
index 4d7728e93..a6063d4f6 100644
--- a/test/plugs/basic_auth_decoder_plug_test.exs
+++ b/test/plugs/basic_auth_decoder_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.BasicAuthDecoderPlugTest do
diff --git a/test/plugs/cache_control_test.exs b/test/plugs/cache_control_test.exs
index be78b3e1e..6b567e81d 100644
--- a/test/plugs/cache_control_test.exs
+++ b/test/plugs/cache_control_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CacheControlTest do
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.CacheControlTest do
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 max-age=86400 must-revalidate"]
+ assert Conn.get_resp_header(conn, "cache-control") == ["public, no-cache"]
end
test "Verify Cache-Control header on the API", %{conn: conn} do
diff --git a/test/plugs/cache_test.exs b/test/plugs/cache_test.exs
index e6e7f409e..8b231c881 100644
--- a/test/plugs/cache_test.exs
+++ b/test/plugs/cache_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.CacheTest do
diff --git a/test/plugs/ensure_authenticated_plug_test.exs b/test/plugs/ensure_authenticated_plug_test.exs
index 37ab5213a..7f3559b83 100644
--- a/test/plugs/ensure_authenticated_plug_test.exs
+++ b/test/plugs/ensure_authenticated_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.EnsureAuthenticatedPlugTest do
@@ -8,24 +8,62 @@ defmodule Pleroma.Plugs.EnsureAuthenticatedPlugTest do
alias Pleroma.Plugs.EnsureAuthenticatedPlug
alias Pleroma.User
- test "it halts if no user is assigned", %{conn: conn} do
- conn =
- conn
- |> EnsureAuthenticatedPlug.call(%{})
+ 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
+ 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, %{})
+
+ assert ret_conn == conn
+ end
end
- test "it continues if a user is assigned", %{conn: conn} do
- conn =
- conn
- |> assign(:user, %User{})
+ describe "with :if_func / :unless_func options" do
+ setup do
+ %{
+ true_fn: fn -> true end,
+ false_fn: fn -> 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{})
+ assert EnsureAuthenticatedPlug.call(conn, if_func: true_fn) == conn
+ assert EnsureAuthenticatedPlug.call(conn, if_func: false_fn) == conn
+ assert EnsureAuthenticatedPlug.call(conn, unless_func: true_fn) == conn
+ assert EnsureAuthenticatedPlug.call(conn, unless_func: false_fn) == conn
+ end
+
+ test "it continues if a user is NOT assigned but :if_func evaluates to `false`",
+ %{conn: conn, false_fn: false_fn} do
+ assert EnsureAuthenticatedPlug.call(conn, if_func: false_fn) == conn
+ end
+
+ test "it continues if a user is NOT assigned but :unless_func evaluates to `true`",
+ %{conn: conn, true_fn: true_fn} do
+ assert EnsureAuthenticatedPlug.call(conn, unless_func: true_fn) == conn
+ 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
- ret_conn =
- conn
- |> EnsureAuthenticatedPlug.call(%{})
+ 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 ret_conn == conn
+ 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
index bae95e150..3fcb4d372 100644
--- a/test/plugs/ensure_public_or_authenticated_plug_test.exs
+++ b/test/plugs/ensure_public_or_authenticated_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
diff --git a/test/plugs/ensure_user_key_plug_test.exs b/test/plugs/ensure_user_key_plug_test.exs
index 6a9627f6a..633c05447 100644
--- a/test/plugs/ensure_user_key_plug_test.exs
+++ b/test/plugs/ensure_user_key_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.EnsureUserKeyPlugTest do
diff --git a/test/plugs/http_security_plug_test.exs b/test/plugs/http_security_plug_test.exs
index 9c1c20541..944a9a139 100644
--- a/test/plugs/http_security_plug_test.exs
+++ b/test/plugs/http_security_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do
clear_config([:http_securiy, :enabled])
clear_config([:http_security, :sts])
+ clear_config([:http_security, :referrer_policy])
describe "http security enabled" do
setup do
diff --git a/test/plugs/http_signature_plug_test.exs b/test/plugs/http_signature_plug_test.exs
index d8ace36da..e6cbde803 100644
--- a/test/plugs/http_signature_plug_test.exs
+++ b/test/plugs/http_signature_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
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
@@ -20,10 +21,69 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
"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
index ac1735f13..21fa0fbcf 100644
--- a/test/plugs/idempotency_plug_test.exs
+++ b/test/plugs/idempotency_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.IdempotencyPlugTest do
diff --git a/test/plugs/instance_static_test.exs b/test/plugs/instance_static_test.exs
index 9b27246fa..8cd9b5712 100644
--- a/test/plugs/instance_static_test.exs
+++ b/test/plugs/instance_static_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RuntimeStaticPlugTest do
diff --git a/test/plugs/legacy_authentication_plug_test.exs b/test/plugs/legacy_authentication_plug_test.exs
index 568ef5abd..7559de7d3 100644
--- a/test/plugs/legacy_authentication_plug_test.exs
+++ b/test/plugs/legacy_authentication_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do
diff --git a/test/plugs/mapped_identity_to_signature_plug_test.exs b/test/plugs/mapped_identity_to_signature_plug_test.exs
index 6b9d3649d..0ad3c2929 100644
--- a/test/plugs/mapped_identity_to_signature_plug_test.exs
+++ b/test/plugs/mapped_identity_to_signature_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do
diff --git a/test/plugs/oauth_plug_test.exs b/test/plugs/oauth_plug_test.exs
index dea11cdb0..f74c068cd 100644
--- a/test/plugs/oauth_plug_test.exs
+++ b/test/plugs/oauth_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.OAuthPlugTest do
@@ -38,7 +38,7 @@ defmodule Pleroma.Plugs.OAuthPlugTest do
assert conn.assigns[:user] == opts[:user]
end
- test "with valid token(downcase) in url parameters, it assings the user", opts do
+ test "with valid token(downcase) in url parameters, it assigns the user", opts do
conn =
:get
|> build_conn("/?access_token=#{opts[:token]}")
diff --git a/test/plugs/oauth_scopes_plug_test.exs b/test/plugs/oauth_scopes_plug_test.exs
index be6d1340b..1b3aa85b6 100644
--- a/test/plugs/oauth_scopes_plug_test.exs
+++ b/test/plugs/oauth_scopes_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.OAuthScopesPlugTest do
@@ -16,34 +16,6 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
:ok
end
- describe "when `assigns[:token]` is nil, " do
- test "with :skip_instance_privacy_check option, proceeds with no op", %{conn: conn} do
- conn =
- conn
- |> assign(:user, insert(:user))
- |> OAuthScopesPlug.call(%{scopes: ["read"], skip_instance_privacy_check: true})
-
- refute conn.halted
- assert conn.assigns[:user]
-
- refute called(EnsurePublicOrAuthenticatedPlug.call(conn, :_))
- end
-
- test "without :skip_instance_privacy_check option, calls EnsurePublicOrAuthenticatedPlug", %{
- conn: conn
- } do
- conn =
- conn
- |> assign(:user, insert(:user))
- |> OAuthScopesPlug.call(%{scopes: ["read"]})
-
- refute conn.halted
- assert conn.assigns[:user]
-
- assert called(EnsurePublicOrAuthenticatedPlug.call(conn, :_))
- end
- end
-
test "if `token.scopes` fulfills specified 'any of' conditions, " <>
"proceeds with no op",
%{conn: conn} do
@@ -75,64 +47,56 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
end
describe "with `fallback: :proceed_unauthenticated` option, " do
- test "if `token.scopes` doesn't fulfill specified 'any of' conditions, " <>
- "clears `assigns[:user]` and calls EnsurePublicOrAuthenticatedPlug",
+ test "if `token.scopes` doesn't fulfill specified conditions, " <>
+ "clears :user and :token assigns and calls EnsurePublicOrAuthenticatedPlug",
%{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: ["follow"], fallback: :proceed_unauthenticated})
-
- refute conn.halted
- refute conn.assigns[:user]
-
- assert called(EnsurePublicOrAuthenticatedPlug.call(conn, :_))
- end
-
- test "if `token.scopes` doesn't fulfill specified 'all of' conditions, " <>
- "clears `assigns[:user] and calls EnsurePublicOrAuthenticatedPlug",
- %{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", "follow"],
- op: :&,
- fallback: :proceed_unauthenticated
- })
-
- refute conn.halted
- refute conn.assigns[:user]
-
- assert called(EnsurePublicOrAuthenticatedPlug.call(conn, :_))
+ 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]
+
+ assert called(EnsurePublicOrAuthenticatedPlug.call(ret_conn, :_))
+ end
end
test "with :skip_instance_privacy_check option, " <>
"if `token.scopes` doesn't fulfill specified conditions, " <>
- "clears `assigns[:user]` and does not call EnsurePublicOrAuthenticatedPlug",
+ "clears :user and :token assigns and does NOT call EnsurePublicOrAuthenticatedPlug",
%{conn: conn} do
- token = insert(:oauth_token, scopes: ["read:statuses", "write"]) |> Repo.preload(:user)
-
- conn =
- conn
- |> assign(:user, token.user)
- |> assign(:token, token)
- |> OAuthScopesPlug.call(%{
- scopes: ["read"],
- fallback: :proceed_unauthenticated,
- skip_instance_privacy_check: true
- })
-
- refute conn.halted
- refute conn.assigns[:user]
-
- refute called(EnsurePublicOrAuthenticatedPlug.call(conn, :_))
+ user = insert(:user)
+ token1 = insert(:oauth_token, scopes: ["read:statuses", "write"], user: user)
+
+ for token <- [token1, nil], op <- [:|, :&] do
+ ret_conn =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, token)
+ |> OAuthScopesPlug.call(%{
+ scopes: ["read"],
+ op: op,
+ fallback: :proceed_unauthenticated,
+ skip_instance_privacy_check: true
+ })
+
+ refute ret_conn.halted
+ refute ret_conn.assigns[:user]
+ refute ret_conn.assigns[:token]
+
+ refute called(EnsurePublicOrAuthenticatedPlug.call(ret_conn, :_))
+ end
end
end
@@ -140,39 +104,42 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
test "if `token.scopes` does not fulfill specified 'any of' conditions, " <>
"returns 403 and halts",
%{conn: conn} do
- token = insert(:oauth_token, scopes: ["read", "write"])
- any_of_scopes = ["follow"]
+ for token <- [insert(:oauth_token, scopes: ["read", "write"]), nil] do
+ any_of_scopes = ["follow", "push"]
- conn =
- conn
- |> assign(:token, token)
- |> OAuthScopesPlug.call(%{scopes: any_of_scopes})
+ ret_conn =
+ conn
+ |> assign(:token, token)
+ |> OAuthScopesPlug.call(%{scopes: any_of_scopes})
- assert conn.halted
- assert 403 == conn.status
+ assert ret_conn.halted
+ assert 403 == ret_conn.status
- expected_error = "Insufficient permissions: #{Enum.join(any_of_scopes, ", ")}."
- assert Jason.encode!(%{error: expected_error}) == conn.resp_body
+ 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
- token = insert(:oauth_token, scopes: ["read", "write"])
- all_of_scopes = ["write", "follow"]
+ 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: :&})
+ conn =
+ conn
+ |> assign(:token, token)
+ |> OAuthScopesPlug.call(%{scopes: all_of_scopes, op: :&})
- assert conn.halted
- assert 403 == conn.status
+ assert conn.halted
+ assert 403 == conn.status
- expected_error =
- "Insufficient permissions: #{Enum.join(all_of_scopes -- token.scopes, ", ")}."
+ expected_error =
+ "Insufficient permissions: #{Enum.join(all_of_scopes -- token_scopes, " & ")}."
- assert Jason.encode!(%{error: expected_error}) == conn.resp_body
+ assert Jason.encode!(%{error: expected_error}) == conn.resp_body
+ end
end
end
@@ -224,4 +191,42 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
assert f.(["admin:read"], ["write", "admin"]) == ["admin:read"]
end
end
+
+ describe "transform_scopes/2" 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
index 49f63c424..c6e494c13 100644
--- a/test/plugs/rate_limiter_test.exs
+++ b/test/plugs/rate_limiter_test.exs
@@ -1,133 +1,159 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.RateLimiterTest do
- use ExUnit.Case, async: true
- use Plug.Test
+ use Pleroma.Web.ConnCase
+ alias Pleroma.Config
alias Pleroma.Plugs.RateLimiter
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
+ clear_config([Pleroma.Web.Endpoint, :http, :ip])
+ clear_config(:rate_limit)
+
describe "config" do
+ @limiter_name :test_init
+
+ clear_config([Pleroma.Plugs.RemoteIp, :enabled])
+
test "config is required for plug to work" do
- limiter_name = :test_init
- Pleroma.Config.put([:rate_limit, limiter_name], {1, 1})
+ 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]} ==
- RateLimiter.init(name: limiter_name)
-
- assert nil == RateLimiter.init(name: :foo)
+ [name: @limiter_name]
+ |> RateLimiter.init()
+ |> RateLimiter.action_settings()
+
+ assert nil ==
+ [name: :nonexisting_limiter]
+ |> RateLimiter.init()
+ |> RateLimiter.action_settings()
end
+ end
- test "it restricts based on config values" do
- limiter_name = :test_opts
- scale = 80
- limit = 5
+ test "it is disabled if it remote ip plug is enabled but no remote ip is found" do
+ Config.put([Pleroma.Web.Endpoint, :http, :ip], {127, 0, 0, 1})
+ assert RateLimiter.disabled?(Plug.Conn.assign(build_conn(), :remote_ip_found, false))
+ end
- Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
+ test "it restricts based on config values" do
+ limiter_name = :test_plug_opts
+ scale = 80
+ limit = 5
- opts = RateLimiter.init(name: limiter_name)
- conn = conn(:get, "/")
+ Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
+ Config.put([:rate_limit, limiter_name], {scale, limit})
- for i <- 1..5 do
- conn = RateLimiter.call(conn, opts)
- assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
- Process.sleep(10)
- end
+ plug_opts = RateLimiter.init(name: limiter_name)
+ conn = build_conn(:get, "/")
- conn = RateLimiter.call(conn, opts)
- assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
- assert conn.halted
+ 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
- Process.sleep(50)
+ conn = RateLimiter.call(conn, plug_opts)
+ assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
+ assert conn.halted
- conn = conn(:get, "/")
+ Process.sleep(50)
- conn = RateLimiter.call(conn, opts)
- assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+ conn = build_conn(:get, "/")
- refute conn.status == Plug.Conn.Status.code(:too_many_requests)
- refute conn.resp_body
- refute conn.halted
- end
+ conn = RateLimiter.call(conn, plug_opts)
+ assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts)
+
+ refute conn.status == Plug.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
- Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
+ 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"
- opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name)
+ plug_opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name)
- conn = conn(:get, "/")
+ conn = build_conn(:get, "/")
- RateLimiter.call(conn, opts)
- assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, opts)
- assert {:err, :not_found} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+ 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
- Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
+ Config.put([:rate_limit, limiter_name], {1000, 5})
+ Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
- opts = RateLimiter.init(name: limiter_name, params: ["id"])
+ plug_opts = RateLimiter.init(name: limiter_name, params: ["id"])
- conn = conn(:get, "/?id=1")
+ conn = build_conn(:get, "/?id=1")
conn = Plug.Conn.fetch_query_params(conn)
- conn_2 = conn(:get, "/?id=2")
+ conn_2 = build_conn(:get, "/?id=2")
- RateLimiter.call(conn, opts)
- assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
- assert {0, 5} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
+ 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
- Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
+ 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"
- opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name, params: ["id"])
+
+ plug_opts =
+ RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name, params: ["id"])
+
id = "100"
- conn = conn(:get, "/?id=#{id}")
+ conn = build_conn(:get, "/?id=#{id}")
conn = Plug.Conn.fetch_query_params(conn)
- conn_2 = conn(:get, "/?id=#{101}")
+ conn_2 = build_conn(:get, "/?id=#{101}")
- RateLimiter.call(conn, opts)
- assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, opts)
- assert {0, 5} = RateLimiter.inspect_bucket(conn_2, base_bucket_name, opts)
+ 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
- Pleroma.Config.put([:rate_limit, limiter_name], [{1000, 5}, {1, 10}])
+ Config.put([:rate_limit, limiter_name], [{1000, 5}, {1, 10}])
+ Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
- opts = RateLimiter.init(name: limiter_name)
+ plug_opts = RateLimiter.init(name: limiter_name)
- conn = %{conn(:get, "/") | remote_ip: {127, 0, 0, 2}}
- conn_2 = %{conn(:get, "/") | remote_ip: {127, 0, 0, 3}}
+ 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, opts)
- assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+ conn = RateLimiter.call(conn, plug_opts)
+ assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts)
refute conn.halted
end
- conn = RateLimiter.call(conn, opts)
+ conn = RateLimiter.call(conn, plug_opts)
assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
assert conn.halted
- conn_2 = RateLimiter.call(conn_2, opts)
- assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
+ conn_2 = RateLimiter.call(conn_2, plug_opts)
+ assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts)
refute conn_2.status == Plug.Conn.Status.code(:too_many_requests)
refute conn_2.resp_body
@@ -142,66 +168,89 @@ defmodule Pleroma.Plugs.RateLimiterTest do
:ok
end
- test "can have limits seperate from unauthenticated connections" do
- limiter_name = :test_authenticated
+ test "can have limits separate from unauthenticated connections" do
+ limiter_name = :test_authenticated1
- scale = 1000
+ scale = 50
limit = 5
- Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {scale, limit}])
+ Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
+ Config.put([:rate_limit, limiter_name], [{1000, 1}, {scale, limit}])
- opts = RateLimiter.init(name: limiter_name)
+ plug_opts = RateLimiter.init(name: limiter_name)
user = insert(:user)
- conn = conn(:get, "/") |> assign(:user, user)
+ conn = build_conn(:get, "/") |> assign(:user, user)
for i <- 1..5 do
- conn = RateLimiter.call(conn, opts)
- assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+ conn = RateLimiter.call(conn, plug_opts)
+ assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts)
refute conn.halted
end
- conn = RateLimiter.call(conn, opts)
+ conn = RateLimiter.call(conn, plug_opts)
assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
assert conn.halted
-
- Process.sleep(1550)
-
- conn = conn(:get, "/") |> assign(:user, user)
- conn = RateLimiter.call(conn, opts)
- assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
-
- refute conn.status == Plug.Conn.Status.code(:too_many_requests)
- refute conn.resp_body
- refute conn.halted
end
- test "diffrerent users are counted independently" do
- limiter_name = :test_authenticated
- Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {1000, 5}])
+ 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})
- opts = RateLimiter.init(name: limiter_name)
+ plug_opts = RateLimiter.init(name: limiter_name)
user = insert(:user)
- conn = conn(:get, "/") |> assign(:user, user)
+ conn = build_conn(:get, "/") |> assign(:user, user)
user_2 = insert(:user)
- conn_2 = conn(:get, "/") |> assign(:user, user_2)
+ conn_2 = build_conn(:get, "/") |> assign(:user, user_2)
for i <- 1..5 do
- conn = RateLimiter.call(conn, opts)
- assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+ conn = RateLimiter.call(conn, plug_opts)
+ assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts)
end
- conn = RateLimiter.call(conn, opts)
+ conn = RateLimiter.call(conn, plug_opts)
assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
assert conn.halted
- conn_2 = RateLimiter.call(conn_2, opts)
- assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
+ conn_2 = RateLimiter.call(conn_2, plug_opts)
+ assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts)
refute conn_2.status == Plug.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
index d120c588b..9c3737b0b 100644
--- a/test/plugs/remote_ip_test.exs
+++ b/test/plugs/remote_ip_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.RemoteIpTest do
@@ -8,6 +8,10 @@ defmodule Pleroma.Plugs.RemoteIpTest do
alias Pleroma.Plugs.RemoteIp
+ import Pleroma.Tests.Helpers, only: [clear_config: 1, clear_config: 2]
+
+ clear_config(RemoteIp)
+
test "disabled" do
Pleroma.Config.put(RemoteIp, enabled: false)
diff --git a/test/plugs/session_authentication_plug_test.exs b/test/plugs/session_authentication_plug_test.exs
index 0000f4258..0949ecfed 100644
--- a/test/plugs/session_authentication_plug_test.exs
+++ b/test/plugs/session_authentication_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.SessionAuthenticationPlugTest do
diff --git a/test/plugs/set_format_plug_test.exs b/test/plugs/set_format_plug_test.exs
index 27c026fdd..7a1dfe9bf 100644
--- a/test/plugs/set_format_plug_test.exs
+++ b/test/plugs/set_format_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.SetFormatPlugTest do
diff --git a/test/plugs/set_locale_plug_test.exs b/test/plugs/set_locale_plug_test.exs
index 0aaeedc1e..7114b1557 100644
--- a/test/plugs/set_locale_plug_test.exs
+++ b/test/plugs/set_locale_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.SetLocalePlugTest do
diff --git a/test/plugs/set_user_session_id_plug_test.exs b/test/plugs/set_user_session_id_plug_test.exs
index f8bfde039..7f1a1e98b 100644
--- a/test/plugs/set_user_session_id_plug_test.exs
+++ b/test/plugs/set_user_session_id_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.SetUserSessionIdPlugTest do
diff --git a/test/plugs/uploaded_media_plug_test.exs b/test/plugs/uploaded_media_plug_test.exs
index 5ba963139..20b13dfac 100644
--- a/test/plugs/uploaded_media_plug_test.exs
+++ b/test/plugs/uploaded_media_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.UploadedMediaPlugTest do
diff --git a/test/plugs/user_enabled_plug_test.exs b/test/plugs/user_enabled_plug_test.exs
index a4035bf0e..931513d83 100644
--- a/test/plugs/user_enabled_plug_test.exs
+++ b/test/plugs/user_enabled_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.UserEnabledPlugTest do
@@ -8,6 +8,8 @@ defmodule Pleroma.Plugs.UserEnabledPlugTest do
alias Pleroma.Plugs.UserEnabledPlug
import Pleroma.Factory
+ clear_config([:instance, :account_activation_required])
+
test "doesn't do anything if the user isn't set", %{conn: conn} do
ret_conn =
conn
@@ -18,7 +20,6 @@ defmodule Pleroma.Plugs.UserEnabledPlugTest do
test "with a user that's not confirmed and a config requiring confirmation, it removes that user",
%{conn: conn} do
- old = Pleroma.Config.get([:instance, :account_activation_required])
Pleroma.Config.put([:instance, :account_activation_required], true)
user = insert(:user, confirmation_pending: true)
@@ -29,8 +30,6 @@ defmodule Pleroma.Plugs.UserEnabledPlugTest do
|> UserEnabledPlug.call(%{})
assert conn.assigns.user == nil
-
- Pleroma.Config.put([:instance, :account_activation_required], old)
end
test "with a user that is deactivated, it removes that user", %{conn: conn} do
diff --git a/test/plugs/user_fetcher_plug_test.exs b/test/plugs/user_fetcher_plug_test.exs
index 262eb8d93..0496f14dd 100644
--- a/test/plugs/user_fetcher_plug_test.exs
+++ b/test/plugs/user_fetcher_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.UserFetcherPlugTest do
diff --git a/test/plugs/user_is_admin_plug_test.exs b/test/plugs/user_is_admin_plug_test.exs
index 136dcc54e..015d51018 100644
--- a/test/plugs/user_is_admin_plug_test.exs
+++ b/test/plugs/user_is_admin_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.UserIsAdminPlugTest do
@@ -8,36 +8,116 @@ defmodule Pleroma.Plugs.UserIsAdminPlugTest do
alias Pleroma.Plugs.UserIsAdminPlug
import Pleroma.Factory
- test "accepts a user that is admin" do
- user = insert(:user, is_admin: true)
+ describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do
+ clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
+ Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
+ end
- conn =
- build_conn()
- |> assign(:user, user)
+ test "accepts a user that is an admin" do
+ user = insert(:user, is_admin: true)
- ret_conn =
- conn
- |> UserIsAdminPlug.call(%{})
+ conn = assign(build_conn(), :user, user)
- assert conn == ret_conn
- end
+ ret_conn = UserIsAdminPlug.call(conn, %{})
+
+ assert conn == ret_conn
+ end
+
+ test "denies a user that isn't an admin" do
+ user = insert(:user)
- test "denies a user that isn't admin" do
- user = insert(:user)
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> UserIsAdminPlug.call(%{})
- conn =
- build_conn()
- |> assign(:user, user)
- |> UserIsAdminPlug.call(%{})
+ assert conn.status == 403
+ end
- assert conn.status == 403
+ test "denies when a user isn't set" do
+ conn = UserIsAdminPlug.call(build_conn(), %{})
+
+ assert conn.status == 403
+ end
end
- test "denies when a user isn't set" do
- conn =
- build_conn()
- |> UserIsAdminPlug.call(%{})
+ describe "with [:auth, :enforce_oauth_admin_scope_usage]," do
+ clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
+ Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true)
+ end
+
+ setup do
+ admin_user = insert(:user, is_admin: true)
+ non_admin_user = insert(:user, is_admin: false)
+ blank_user = nil
+
+ {:ok, %{users: [admin_user, non_admin_user, blank_user]}}
+ end
+
+ test "if token has any of admin scopes, accepts a user that is an admin", %{conn: conn} do
+ user = insert(:user, is_admin: true)
+ token = insert(:oauth_token, user: user, scopes: ["admin:something"])
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, token)
+
+ ret_conn = UserIsAdminPlug.call(conn, %{})
+
+ assert conn == ret_conn
+ end
+
+ test "if token has any of admin scopes, denies a user that isn't an admin", %{conn: conn} do
+ user = insert(:user, is_admin: false)
+ token = insert(:oauth_token, user: user, scopes: ["admin:something"])
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, token)
+ |> UserIsAdminPlug.call(%{})
+
+ assert conn.status == 403
+ end
+
+ test "if token has any of admin scopes, denies when a user isn't set", %{conn: conn} do
+ token = insert(:oauth_token, scopes: ["admin:something"])
+
+ conn =
+ conn
+ |> assign(:user, nil)
+ |> assign(:token, token)
+ |> UserIsAdminPlug.call(%{})
+
+ assert conn.status == 403
+ end
+
+ test "if token lacks admin scopes, denies users regardless of is_admin flag",
+ %{users: users} do
+ for user <- users do
+ token = insert(:oauth_token, user: user)
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> assign(:token, token)
+ |> UserIsAdminPlug.call(%{})
+
+ assert conn.status == 403
+ end
+ end
+
+ test "if token is missing, denies users regardless of is_admin flag", %{users: users} do
+ for user <- users do
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> assign(:token, nil)
+ |> UserIsAdminPlug.call(%{})
- assert conn.status == 403
+ assert conn.status == 403
+ end
+ end
end
end
diff --git a/test/registration_test.exs b/test/registration_test.exs
index 6143b82c7..7db8e3664 100644
--- a/test/registration_test.exs
+++ b/test/registration_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.RegistrationTest do
diff --git a/test/repo_test.exs b/test/repo_test.exs
index 85b64d4d1..75e85f974 100644
--- a/test/repo_test.exs
+++ b/test/repo_test.exs
@@ -1,10 +1,13 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.RepoTest do
use Pleroma.DataCase
+ import ExUnit.CaptureLog
import Pleroma.Factory
+ import Mock
+
alias Pleroma.User
describe "find_resource/1" do
@@ -46,4 +49,36 @@ defmodule Pleroma.RepoTest do
assert Repo.get_assoc(token, :user) == {:error, :not_found}
end
end
+
+ describe "check_migrations_applied!" do
+ setup_with_mocks([
+ {Ecto.Migrator, [],
+ [
+ with_repo: fn repo, fun -> passthrough([repo, fun]) end,
+ migrations: fn Pleroma.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
+
+ clear_config([:i_am_aware_this_may_cause_data_loss, :disable_migration_check])
+
+ test "raises if it detects unapplied migrations" do
+ assert_raise Pleroma.Repo.UnappliedMigrationsError, fn ->
+ capture_log(&Repo.check_migrations_applied!/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 == Repo.check_migrations_applied!()
+ end
+ end
end
diff --git a/test/reverse_proxy_test.exs b/test/reverse_proxy_test.exs
index 0672f57db..87c6aca4e 100644
--- a/test/reverse_proxy_test.exs
+++ b/test/reverse_proxy_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxyTest do
@@ -275,17 +275,6 @@ defmodule Pleroma.ReverseProxyTest do
end
describe "cache resp headers" do
- test "returns headers", %{conn: conn} do
- ClientMock
- |> expect(:request, fn :get, "/cache/" <> ttl, _, _, _ ->
- {:ok, 200, [{"cache-control", "public, max-age=" <> ttl}], %{}}
- end)
- |> expect(:stream_body, fn _ -> :done end)
-
- conn = ReverseProxy.call(conn, "/cache/10")
- assert {"cache-control", "public, max-age=10"} in conn.resp_headers
- end
-
test "add cache-control", %{conn: conn} do
ClientMock
|> expect(:request, fn :get, "/cache", _, _, _ ->
@@ -294,7 +283,7 @@ defmodule Pleroma.ReverseProxyTest do
|> expect(:stream_body, fn _ -> :done end)
conn = ReverseProxy.call(conn, "/cache")
- assert {"cache-control", "public"} in conn.resp_headers
+ assert {"cache-control", "public, max-age=1209600"} in conn.resp_headers
end
end
diff --git a/test/runtime_test.exs b/test/runtime_test.exs
new file mode 100644
index 000000000..a1a6c57cd
--- /dev/null
+++ b/test/runtime_test.exs
@@ -0,0 +1,11 @@
+# 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/scheduled_activity_test.exs b/test/scheduled_activity_test.exs
index dcf12fb49..4369e7e8a 100644
--- a/test/scheduled_activity_test.exs
+++ b/test/scheduled_activity_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ScheduledActivityTest do
@@ -8,11 +8,51 @@ defmodule Pleroma.ScheduledActivityTest do
alias Pleroma.ScheduledActivity
import Pleroma.Factory
+ 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)
@@ -24,6 +64,7 @@ defmodule Pleroma.ScheduledActivityTest do
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
diff --git a/test/signature_test.exs b/test/signature_test.exs
index 15cf10fb6..04736d8b9 100644
--- a/test/signature_test.exs
+++ b/test/signature_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.SignatureTest do
diff --git a/test/stat_test.exs b/test/stat_test.exs
new file mode 100644
index 000000000..bccc1c8d0
--- /dev/null
+++ b/test/stat_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.StateTest do
+ use Pleroma.DataCase
+ import Pleroma.Factory
+ alias Pleroma.Web.CommonAPI
+
+ describe "status visibility count" do
+ test "on new status" 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 %{direct: 3, private: 4, public: 1, unlisted: 2} =
+ Pleroma.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} = Pleroma.Stats.get_status_visibility_count()
+ CommonAPI.delete(activity.id, user)
+ assert %{public: 0} = Pleroma.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} = Pleroma.Stats.get_status_visibility_count()
+ {:ok, _} = CommonAPI.update_activity_scope(activity.id, %{"visibility" => "private"})
+ assert %{public: 0, private: 1} = Pleroma.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} =
+ Pleroma.Stats.get_status_visibility_count()
+ end
+ end
+end
diff --git a/test/support/builders/user_builder.ex b/test/support/builders/user_builder.ex
index 6da16f71a..fcfea666f 100644
--- a/test/support/builders/user_builder.ex
+++ b/test/support/builders/user_builder.ex
@@ -10,7 +10,8 @@ defmodule Pleroma.Builders.UserBuilder do
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
bio: "A tester.",
ap_id: "some id",
- last_digest_emailed_at: NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
+ last_digest_emailed_at: NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
+ notification_settings: %Pleroma.User.NotificationSetting{}
}
Map.merge(user, data)
diff --git a/test/support/captcha_mock.ex b/test/support/captcha_mock.ex
index 65ca6b3bd..6dae94edf 100644
--- a/test/support/captcha_mock.ex
+++ b/test/support/captcha_mock.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Mock do
@@ -7,8 +7,17 @@ defmodule Pleroma.Captcha.Mock do
@behaviour Service
@impl Service
- def new, do: %{type: :mock}
+ def new,
+ do: %{
+ type: :mock,
+ token: "afa1815e14e29355e6c8f6b143a39fa2",
+ answer_data: "63615261b77f5354fb8c4e4986477555",
+ url: "https://example.org/captcha.png"
+ }
@impl Service
- def validate(_token, _captcha, _data), do: :ok
+ 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/channel_case.ex b/test/support/channel_case.ex
index 466d8986f..d63a0f06b 100644
--- a/test/support/channel_case.ex
+++ b/test/support/channel_case.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ChannelCase do
@@ -23,6 +23,7 @@ defmodule Pleroma.Web.ChannelCase do
quote do
# Import conveniences for testing with channels
use Phoenix.ChannelTest
+ use Pleroma.Tests.Helpers
# The default endpoint for testing
@endpoint Pleroma.Web.Endpoint
diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex
index 9897f72ce..064874201 100644
--- a/test/support/conn_case.ex
+++ b/test/support/conn_case.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ConnCase do
@@ -26,8 +26,52 @@ defmodule Pleroma.Web.ConnCase do
use Pleroma.Tests.Helpers
import Pleroma.Web.Router.Helpers
+ alias Pleroma.Config
+
# The default endpoint for testing
@endpoint Pleroma.Web.Endpoint
+
+ # Sets up OAuth access with specified scopes
+ defp oauth_access(scopes, opts \\ []) do
+ user =
+ Keyword.get_lazy(opts, :user, fn ->
+ Pleroma.Factory.insert(:user)
+ end)
+
+ token =
+ Keyword.get_lazy(opts, :oauth_token, fn ->
+ Pleroma.Factory.insert(:oauth_token, user: user, scopes: scopes)
+ end)
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> assign(:token, token)
+
+ %{user: user, token: token, conn: conn}
+ end
+
+ defp ensure_federating_or_authenticated(conn, url, user) do
+ initial_setting = Config.get([:instance, :federating])
+ on_exit(fn -> Config.put([:instance, :federating], initial_setting) end)
+
+ Config.put([:instance, :federating], false)
+
+ conn
+ |> get(url)
+ |> response(403)
+
+ conn
+ |> assign(:user, user)
+ |> get(url)
+ |> response(200)
+
+ Config.put([:instance, :federating], true)
+
+ conn
+ |> get(url)
+ |> response(200)
+ end
end
end
diff --git a/test/support/data_case.ex b/test/support/data_case.ex
index 4ffcbac9e..1669f2520 100644
--- a/test/support/data_case.ex
+++ b/test/support/data_case.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.DataCase do
diff --git a/test/support/factory.ex b/test/support/factory.ex
index bb8a64e72..af639b6cd 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Factory do
@@ -31,7 +31,8 @@ defmodule Pleroma.Factory do
nickname: sequence(:nickname, &"nick#{&1}"),
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
bio: sequence(:bio, &"Tester Number #{&1}"),
- last_digest_emailed_at: NaiveDateTime.utc_now()
+ last_digest_emailed_at: NaiveDateTime.utc_now(),
+ notification_settings: %Pleroma.User.NotificationSetting{}
}
%{
@@ -42,6 +43,18 @@ defmodule Pleroma.Factory do
}
end
+ def user_relationship_factory(attrs \\ %{}) do
+ source = attrs[:source] || insert(:user)
+ target = attrs[:target] || insert(:user)
+ relationship_type = attrs[:relationship_type] || :block
+
+ %Pleroma.UserRelationship{
+ source_id: source.id,
+ target_id: target.id,
+ relationship_type: relationship_type
+ }
+ end
+
def note_factory(attrs \\ %{}) do
text = sequence(:text, &"This is :moominmamma: note #{&1}")
@@ -283,7 +296,7 @@ defmodule Pleroma.Factory do
%Pleroma.Web.OAuth.App{
client_name: "Some client",
redirect_uris: "https://example.com/callback",
- scopes: ["read", "write", "follow", "push"],
+ scopes: ["read", "write", "follow", "push", "admin"],
website: "https://example.com",
client_id: Ecto.UUID.generate(),
client_secret: "aaa;/&bbb"
@@ -297,19 +310,37 @@ defmodule Pleroma.Factory do
}
end
- def oauth_token_factory do
- oauth_app = insert(:oauth_app)
+ def oauth_token_factory(attrs \\ %{}) do
+ scopes = Map.get(attrs, :scopes, ["read"])
+ oauth_app = Map.get_lazy(attrs, :app, fn -> insert(:oauth_app, scopes: scopes) end)
+ user = Map.get_lazy(attrs, :user, fn -> build(:user) end)
+
+ valid_until =
+ Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10))
%Pleroma.Web.OAuth.Token{
token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
- scopes: ["read"],
refresh_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
- user: build(:user),
- app_id: oauth_app.id,
- valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
+ scopes: scopes,
+ user: user,
+ app: oauth_app,
+ valid_until: valid_until
}
end
+ def oauth_admin_token_factory(attrs \\ %{}) do
+ user = Map.get_lazy(attrs, :user, fn -> build(:user, is_admin: true) end)
+
+ scopes =
+ attrs
+ |> Map.get(:scopes, ["admin"])
+ |> Kernel.++(["admin"])
+ |> Enum.uniq()
+
+ attrs = Map.merge(attrs, %{user: user, scopes: scopes})
+ oauth_token_factory(attrs)
+ end
+
def oauth_authorization_factory do
%Pleroma.Web.OAuth.Authorization{
token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false),
@@ -363,9 +394,15 @@ defmodule Pleroma.Factory do
end
def config_factory do
- %Pleroma.Web.AdminAPI.Config{
- key: sequence(:key, &"some_key_#{&1}"),
- group: "pleroma",
+ %Pleroma.ConfigDB{
+ key:
+ sequence(:key, fn key ->
+ # Atom dynamic registration hack in tests
+ "some_key_#{key}"
+ |> String.to_atom()
+ |> inspect()
+ end),
+ group: ":pleroma",
value:
sequence(
:value,
diff --git a/test/support/helpers.ex b/test/support/helpers.ex
index ce39dd9d8..6bf4b019e 100644
--- a/test/support/helpers.ex
+++ b/test/support/helpers.ex
@@ -1,11 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Tests.Helpers do
@moduledoc """
Helpers for use in tests.
"""
+ alias Pleroma.Config
defmacro clear_config(config_path) do
quote do
@@ -17,14 +18,15 @@ defmodule Pleroma.Tests.Helpers do
defmacro clear_config(config_path, do: yield) do
quote do
setup do
- initial_setting = Pleroma.Config.get(unquote(config_path))
+ initial_setting = Config.get(unquote(config_path))
unquote(yield)
- on_exit(fn -> Pleroma.Config.put(unquote(config_path), initial_setting) end)
+ on_exit(fn -> Config.put(unquote(config_path), initial_setting) end)
:ok
end
end
end
+ @doc "Stores initial config value and restores it after *all* test examples are executed."
defmacro clear_config_all(config_path) do
quote do
clear_config_all(unquote(config_path)) do
@@ -32,12 +34,17 @@ defmodule Pleroma.Tests.Helpers do
end
end
+ @doc """
+ Stores initial config value and restores it after *all* test examples are executed.
+ Only use if *all* test examples should work with the same stubbed value
+ (*no* examples set a different value).
+ """
defmacro clear_config_all(config_path, do: yield) do
quote do
setup_all do
- initial_setting = Pleroma.Config.get(unquote(config_path))
+ initial_setting = Config.get(unquote(config_path))
unquote(yield)
- on_exit(fn -> Pleroma.Config.put(unquote(config_path), initial_setting) end)
+ on_exit(fn -> Config.put(unquote(config_path), initial_setting) end)
:ok
end
end
@@ -53,6 +60,12 @@ defmodule Pleroma.Tests.Helpers do
clear_config_all: 2
]
+ def to_datetime(naive_datetime) do
+ naive_datetime
+ |> DateTime.from_naive!("Etc/UTC")
+ |> DateTime.truncate(:second)
+ end
+
def collect_ids(collection) do
collection
|> Enum.map(& &1.id)
@@ -75,12 +88,29 @@ defmodule Pleroma.Tests.Helpers do
|> Poison.decode!()
end
+ def stringify_keys(nil), do: nil
+
+ def stringify_keys(key) when key in [true, false], do: key
+ def stringify_keys(key) when is_atom(key), do: Atom.to_string(key)
+
+ def stringify_keys(map) when is_map(map) do
+ map
+ |> Enum.map(fn {k, v} -> {stringify_keys(k), stringify_keys(v)} end)
+ |> Enum.into(%{})
+ end
+
+ def stringify_keys([head | rest] = list) when is_list(list) do
+ [stringify_keys(head) | stringify_keys(rest)]
+ end
+
+ def stringify_keys(key), do: key
+
defmacro guards_config(config_path) do
quote do
- initial_setting = Pleroma.Config.get(config_path)
+ initial_setting = Config.get(config_path)
- Pleroma.Config.put(config_path, true)
- on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
+ Config.put(config_path, true)
+ on_exit(fn -> Config.put(config_path, initial_setting) end)
end
end
end
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index e3a621f49..e72638814 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule HttpRequestMock do
@@ -19,7 +19,7 @@ defmodule HttpRequestMock do
else
error ->
with {:error, message} <- error do
- Logger.warn(message)
+ Logger.warn(to_string(message))
end
{_, _r} = error
@@ -308,6 +308,24 @@ defmodule HttpRequestMock do
}}
end
+ def get("https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39", _, _,
+ Accept: "application/activity+json"
+ ) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/mobilizon.org-event.json")
+ }}
+ end
+
+ def get("https://mobilizon.org/@tcit", _, _, Accept: "application/activity+json") do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/mobilizon.org-user.json")
+ }}
+ end
+
def get("https://baptiste.gelez.xyz/@/BaptisteGelez", _, _, _) do
{:ok,
%Tesla.Env{
@@ -1259,6 +1277,10 @@ defmodule HttpRequestMock do
{:ok, %Tesla.Env{status: 404, body: ""}}
end
+ def get("https://relay.mastodon.host/actor", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/relay/relay.json")}}
+ end
+
def get(url, query, body, headers) do
{:error,
"Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{
@@ -1271,6 +1293,10 @@ defmodule HttpRequestMock do
def post(url, query \\ [], body \\ [], headers \\ [])
+ def post("https://relay.mastodon.host/inbox", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: ""}}
+ end
+
def post("http://example.org/needs_refresh", _, _, _) do
{:ok,
%Tesla.Env{
diff --git a/test/support/mrf_module_mock.ex b/test/support/mrf_module_mock.ex
index 632c7ff1d..028ea542a 100644
--- a/test/support/mrf_module_mock.ex
+++ b/test/support/mrf_module_mock.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule MRFModuleMock do
diff --git a/test/support/oban_helpers.ex b/test/support/oban_helpers.ex
index 72792c064..e96994c57 100644
--- a/test/support/oban_helpers.ex
+++ b/test/support/oban_helpers.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Tests.ObanHelpers do
@@ -9,6 +9,10 @@ defmodule Pleroma.Tests.ObanHelpers do
alias Pleroma.Repo
+ def wipe_all do
+ Repo.delete_all(Oban.Job)
+ end
+
def perform_all do
Oban.Job
|> Repo.all()
diff --git a/test/support/web_push_http_client_mock.ex b/test/support/web_push_http_client_mock.ex
index 1d6ccff7e..3cd12957d 100644
--- a/test/support/web_push_http_client_mock.ex
+++ b/test/support/web_push_http_client_mock.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebPushHttpClientMock do
diff --git a/test/support/websocket_client.ex b/test/support/websocket_client.ex
index 121231452..8c9d4b2b4 100644
--- a/test/support/websocket_client.ex
+++ b/test/support/websocket_client.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Integration.WebsocketClient do
diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs
index 9cd47380c..a6c0de351 100644
--- a/test/tasks/config_test.exs
+++ b/test/tasks/config_test.exs
@@ -1,66 +1,196 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.ConfigTest do
use Pleroma.DataCase
+
+ alias Pleroma.ConfigDB
alias Pleroma.Repo
- alias Pleroma.Web.AdminAPI.Config
setup_all do
Mix.shell(Mix.Shell.Process)
- temp_file = "config/temp.exported_from_db.secret.exs"
on_exit(fn ->
Mix.shell(Mix.Shell.IO)
Application.delete_env(:pleroma, :first_setting)
Application.delete_env(:pleroma, :second_setting)
- :ok = File.rm(temp_file)
end)
- {:ok, temp_file: temp_file}
+ :ok
end
- clear_config_all([:instance, :dynamic_configuration]) do
- Pleroma.Config.put([:instance, :dynamic_configuration], true)
+ clear_config_all(:configurable_from_database) do
+ Pleroma.Config.put(:configurable_from_database, true)
end
- test "settings are migrated to db" do
- assert Repo.all(Config) == []
+ 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
+
+ test "settings are migrated to db" do
+ assert Repo.all(ConfigDB) == []
- Application.put_env(:pleroma, :first_setting, key: "value", key2: [Pleroma.Repo])
- Application.put_env(:pleroma, :second_setting, key: "value2", key2: [Pleroma.Activity])
+ Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs")
- Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
+ 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"})
- first_db = Config.get_by_params(%{group: "pleroma", key: ":first_setting"})
- second_db = Config.get_by_params(%{group: "pleroma", key: ":second_setting"})
- refute Config.get_by_params(%{group: "pleroma", key: "Pleroma.Repo"})
+ assert ConfigDB.from_binary(config1.value) == [key: "value", key2: [Repo]]
+ assert ConfigDB.from_binary(config2.value) == [key: "value2", key2: ["Activity"]]
+ assert ConfigDB.from_binary(config3.value) == :info
+ end
- assert Config.from_binary(first_db.value) == [key: "value", key2: [Pleroma.Repo]]
- assert Config.from_binary(second_db.value) == [key: "value2", key2: [Pleroma.Activity]]
+ test "config table is truncated before migration" do
+ ConfigDB.create(%{
+ group: ":pleroma",
+ 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 ConfigDB.from_binary(config.value) == [key: "value", key2: [Repo]]
+ end
end
- test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do
- Config.create(%{
- group: "pleroma",
- key: ":setting_first",
- value: [key: "value", key2: [Pleroma.Activity]]
- })
+ 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
+ ConfigDB.create(%{
+ group: ":pleroma",
+ key: ":setting_first",
+ value: [key: "value", key2: ["Activity"]]
+ })
+
+ ConfigDB.create(%{
+ group: ":pleroma",
+ key: ":setting_second",
+ value: [key: "value2", key2: [Repo]]
+ })
+
+ ConfigDB.create(%{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
+ ConfigDB.create(%{
+ group: ":pleroma",
+ 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,
+ rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
+ public: true,
+ quarantined_instances: [],
+ managed_config: true,
+ static_dir: "instance/static/",
+ allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"],
+ mrf_transparency: true,
+ mrf_transparency_exclusions: [],
+ autofollowed_nicknames: [],
+ max_pinned_statuses: 1,
+ attachment_links: false,
+ welcome_user_nickname: nil,
+ welcome_message: nil,
+ 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 or 8
+ digits: 6,
+ period: 30
+ ],
+ backup_codes: [
+ number: 2,
+ length: 6
+ ]
+ ]
+ ]
+ })
- Config.create(%{
- group: "pleroma",
- key: ":setting_second",
- value: [key: "valu2", key2: [Pleroma.Repo]]
- })
+ Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"])
- Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "temp", "true"])
+ assert Repo.all(ConfigDB) == []
+ assert File.exists?(temp_file)
+ {:ok, file} = File.read(temp_file)
- assert Repo.all(Config) == []
- 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 =~ "config :pleroma, :setting_first,"
- assert file =~ "config :pleroma, :setting_second,"
+ 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 rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,\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 mrf_transparency: true,\n mrf_transparency_exclusions: [],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n attachment_links: false,\n welcome_user_nickname: nil,\n welcome_message: nil,\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
index bb5dc88f8..73c2ea690 100644
--- a/test/tasks/count_statuses_test.exs
+++ b/test/tasks/count_statuses_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.CountStatusesTest do
diff --git a/test/tasks/database_test.exs b/test/tasks/database_test.exs
index 77c102cce..7b05993d3 100644
--- a/test/tasks/database_test.exs
+++ b/test/tasks/database_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.DatabaseTest do
diff --git a/test/tasks/ecto/ecto_test.exs b/test/tasks/ecto/ecto_test.exs
index a1b9ca174..3a028df83 100644
--- a/test/tasks/ecto/ecto_test.exs
+++ b/test/tasks/ecto/ecto_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.EctoTest do
diff --git a/test/tasks/ecto/migrate_test.exs b/test/tasks/ecto/migrate_test.exs
index 42f6cbf47..43df176a1 100644
--- a/test/tasks/ecto/migrate_test.exs
+++ b/test/tasks/ecto/migrate_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-onl
defmodule Mix.Tasks.Pleroma.Ecto.MigrateTest do
diff --git a/test/tasks/ecto/rollback_test.exs b/test/tasks/ecto/rollback_test.exs
index c33c4e940..0236e35d5 100644
--- a/test/tasks/ecto/rollback_test.exs
+++ b/test/tasks/ecto/rollback_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Ecto.RollbackTest do
diff --git a/test/tasks/email_test.exs b/test/tasks/email_test.exs
new file mode 100644
index 000000000..944c07064
--- /dev/null
+++ b/test/tasks/email_test.exs
@@ -0,0 +1,52 @@
+defmodule Mix.Tasks.Pleroma.EmailTest do
+ use Pleroma.DataCase
+
+ import Swoosh.TestAssertions
+
+ alias Pleroma.Config
+ alias Pleroma.Tests.ObanHelpers
+
+ setup_all do
+ Mix.shell(Mix.Shell.Process)
+
+ on_exit(fn ->
+ Mix.shell(Mix.Shell.IO)
+ end)
+
+ :ok
+ end
+
+ 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
+ end
+end
diff --git a/test/tasks/instance_test.exs b/test/tasks/instance_test.exs
index 6d7eed4c1..f6a4ba508 100644
--- a/test/tasks/instance_test.exs
+++ b/test/tasks/instance_test.exs
@@ -1,9 +1,9 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.InstanceTest do
- use ExUnit.Case, async: true
+ use ExUnit.Case
setup do
File.mkdir_p!(tmp_path())
@@ -15,6 +15,8 @@ defmodule Pleroma.InstanceTest do
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
@@ -78,7 +80,7 @@ defmodule Pleroma.InstanceTest do
assert generated_config =~ "database: \"dbname\""
assert generated_config =~ "username: \"dbuser\""
assert generated_config =~ "password: \"dbpass\""
- assert generated_config =~ "dynamic_configuration: true"
+ assert generated_config =~ "configurable_from_database: true"
assert generated_config =~ "http: [ip: {127, 0, 0, 1}, port: 4000]"
assert File.read!(tmp_path() <> "setup.psql") == generated_setup_psql()
end
diff --git a/test/tasks/pleroma_test.exs b/test/tasks/pleroma_test.exs
index a20bd9cf2..c3e47b285 100644
--- a/test/tasks/pleroma_test.exs
+++ b/test/tasks/pleroma_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.PleromaTest do
diff --git a/test/tasks/refresh_counter_cache_test.exs b/test/tasks/refresh_counter_cache_test.exs
new file mode 100644
index 000000000..b63f44c08
--- /dev/null
+++ b/test/tasks/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/tasks/relay_test.exs b/test/tasks/relay_test.exs
index 04a1e45d7..d3d88467d 100644
--- a/test/tasks/relay_test.exs
+++ b/test/tasks/relay_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.RelayTest do
@@ -38,6 +38,9 @@ defmodule Mix.Tasks.Pleroma.RelayTest do
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, ["mastodon.example.org (no Accept received)"]}
end
end
diff --git a/test/tasks/robots_txt_test.exs b/test/tasks/robots_txt_test.exs
index 917df2675..e03c9c192 100644
--- a/test/tasks/robots_txt_test.exs
+++ b/test/tasks/robots_txt_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.RobotsTxtTest do
diff --git a/test/tasks/uploads_test.exs b/test/tasks/uploads_test.exs
index b0b8eda11..d69e149a8 100644
--- a/test/tasks/uploads_test.exs
+++ b/test/tasks/uploads_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.UploadsTest do
diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs
index bfd0ccbc5..b45f37263 100644
--- a/test/tasks/user_test.exs
+++ b/test/tasks/user_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.UserTest do
diff --git a/test/test_helper.exs b/test/test_helper.exs
index 241ad1f94..6b91d2b46 100644
--- a/test/test_helper.exs
+++ b/test/test_helper.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: []
diff --git a/test/upload/filter/anonymize_filename_test.exs b/test/upload/filter/anonymize_filename_test.exs
index 6b33e7395..330158580 100644
--- a/test/upload/filter/anonymize_filename_test.exs
+++ b/test/upload/filter/anonymize_filename_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do
diff --git a/test/upload/filter/dedupe_test.exs b/test/upload/filter/dedupe_test.exs
index 3de94dc20..966c353f7 100644
--- a/test/upload/filter/dedupe_test.exs
+++ b/test/upload/filter/dedupe_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.DedupeTest do
diff --git a/test/upload/filter/mogrifun_test.exs b/test/upload/filter/mogrifun_test.exs
index d5a8751cc..2426a8496 100644
--- a/test/upload/filter/mogrifun_test.exs
+++ b/test/upload/filter/mogrifun_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.MogrifunTest do
diff --git a/test/upload/filter/mogrify_test.exs b/test/upload/filter/mogrify_test.exs
index 210320d30..52483d80c 100644
--- a/test/upload/filter/mogrify_test.exs
+++ b/test/upload/filter/mogrify_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.MogrifyTest do
diff --git a/test/upload/filter_test.exs b/test/upload/filter_test.exs
index 03887c06a..2ffc5247b 100644
--- a/test/upload/filter_test.exs
+++ b/test/upload/filter_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.FilterTest do
diff --git a/test/upload_test.exs b/test/upload_test.exs
index 0ca5ebced..6ce42b630 100644
--- a/test/upload_test.exs
+++ b/test/upload_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UploadTest do
diff --git a/test/uploaders/local_test.exs b/test/uploaders/local_test.exs
index fc442d0f1..ae2cfef94 100644
--- a/test/uploaders/local_test.exs
+++ b/test/uploaders/local_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Uploaders.LocalTest do
@@ -29,4 +29,25 @@ defmodule Pleroma.Uploaders.LocalTest do
|> File.exists?()
end
end
+
+ describe "delete_file/1" do
+ test "deletes local file" do
+ 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/mdii_test.exs b/test/uploaders/mdii_test.exs
deleted file mode 100644
index d432d40f0..000000000
--- a/test/uploaders/mdii_test.exs
+++ /dev/null
@@ -1,50 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Uploaders.MDIITest do
- use Pleroma.DataCase
- alias Pleroma.Uploaders.MDII
- import Tesla.Mock
-
- describe "get_file/1" do
- test "it returns path to local folder for files" do
- assert MDII.get_file("") == {:ok, {:static_dir, "test/uploads"}}
- end
- end
-
- describe "put_file/1" do
- setup do
- file_upload = %Pleroma.Upload{
- name: "mdii-image.jpg",
- content_type: "image/jpg",
- path: "test_folder/mdii-image.jpg",
- tempfile: Path.absname("test/fixtures/image_tmp.jpg")
- }
-
- [file_upload: file_upload]
- end
-
- test "save file", %{file_upload: file_upload} do
- mock(fn
- %{method: :post, url: "https://mdii.sakura.ne.jp/mdii-post.cgi?jpg"} ->
- %Tesla.Env{status: 200, body: "mdii-image"}
- end)
-
- assert MDII.put_file(file_upload) ==
- {:ok, {:url, "https://mdii.sakura.ne.jp/mdii-image.jpg"}}
- end
-
- test "save file to local if MDII isn`t available", %{file_upload: file_upload} do
- mock(fn
- %{method: :post, url: "https://mdii.sakura.ne.jp/mdii-post.cgi?jpg"} ->
- %Tesla.Env{status: 500}
- end)
-
- assert MDII.put_file(file_upload) == :ok
-
- assert Path.join([Pleroma.Uploaders.Local.upload_path(), file_upload.path])
- |> File.exists?()
- end
- end
-end
diff --git a/test/uploaders/s3_test.exs b/test/uploaders/s3_test.exs
index 171316340..fdc7eff41 100644
--- a/test/uploaders/s3_test.exs
+++ b/test/uploaders/s3_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Uploaders.S3Test do
@@ -79,4 +79,11 @@ defmodule Pleroma.Uploaders.S3Test do
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/notification_setting_test.exs b/test/user/notification_setting_test.exs
new file mode 100644
index 000000000..95bca22c4
--- /dev/null
+++ b/test/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 valid privacy option" do
+ changeset =
+ NotificationSetting.changeset(
+ %NotificationSetting{},
+ %{"privacy_option" => true}
+ )
+
+ assert %Ecto.Changeset{valid?: true} = changeset
+ end
+ end
+end
diff --git a/test/user_invite_token_test.exs b/test/user_invite_token_test.exs
index 111e40361..4f70ef337 100644
--- a/test/user_invite_token_test.exs
+++ b/test/user_invite_token_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserInviteTokenTest do
diff --git a/test/user_relationship_test.exs b/test/user_relationship_test.exs
new file mode 100644
index 000000000..f12406097
--- /dev/null
+++ b/test/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/user_search_test.exs b/test/user_search_test.exs
index 98841dbbd..406cc8fb2 100644
--- a/test/user_search_test.exs
+++ b/test/user_search_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserSearchTest do
@@ -15,6 +15,8 @@ defmodule Pleroma.UserSearchTest do
end
describe "User.search" do
+ clear_config([:instance, :limit_to_local_content])
+
test "excluded invisible users from results" do
user = insert(:user, %{nickname: "john t1000"})
insert(:user, %{invisible: true, nickname: "john t800"})
@@ -127,8 +129,6 @@ defmodule Pleroma.UserSearchTest do
insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
assert [%{id: ^id}] = User.search("lain")
-
- Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
end
test "find all users for unauthenticated users when `limit_to_local_content` is `false`" do
@@ -145,8 +145,6 @@ defmodule Pleroma.UserSearchTest do
|> Enum.sort()
assert [u1.id, u2.id, u3.id] == results
-
- Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
end
test "does not yield false-positive matches" do
@@ -174,6 +172,7 @@ defmodule Pleroma.UserSearchTest do
|> Map.put(:search_rank, nil)
|> Map.put(:search_type, nil)
|> Map.put(:last_digest_emailed_at, nil)
+ |> Map.put(:notification_settings, nil)
assert user == expected
end
diff --git a/test/user_test.exs b/test/user_test.exs
index e1e57f07b..8dcac676d 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserTest do
@@ -17,6 +17,7 @@ defmodule Pleroma.UserTest do
import Mock
import Pleroma.Factory
+ import ExUnit.CaptureLog
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -26,6 +27,42 @@ defmodule Pleroma.UserTest 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"
@@ -44,6 +81,56 @@ defmodule Pleroma.UserTest do
end
end
+ describe "AP ID user relationships" do
+ setup do
+ {:ok, user: insert(:user)}
+ end
+
+ test "outgoing_relations_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_relations_ap_ids = User.outgoing_relations_ap_ids(user, rel_types)
+
+ assert ap_ids_by_rel ==
+ Enum.into(outgoing_relations_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})
@@ -119,7 +206,7 @@ defmodule Pleroma.UserTest do
CommonAPI.follow(follower, followed)
assert [_activity] = User.get_follow_requests(followed)
- {:ok, _follower} = User.block(followed, follower)
+ {:ok, _user_relationship} = User.block(followed, follower)
assert [] = User.get_follow_requests(followed)
end
@@ -132,8 +219,8 @@ defmodule Pleroma.UserTest do
not_followed = insert(:user)
reverse_blocked = insert(:user)
- {:ok, user} = User.block(user, blocked)
- {:ok, reverse_blocked} = User.block(reverse_blocked, user)
+ {:ok, _user_relationship} = User.block(user, blocked)
+ {:ok, _user_relationship} = User.block(reverse_blocked, user)
{:ok, user} = User.follow(user, followed_zero)
@@ -186,7 +273,7 @@ defmodule Pleroma.UserTest do
blocker = insert(:user)
blockee = insert(:user)
- {:ok, blocker} = User.block(blocker, blockee)
+ {:ok, _user_relationship} = User.block(blocker, blockee)
{:error, _} = User.follow(blockee, blocker)
end
@@ -195,7 +282,7 @@ defmodule Pleroma.UserTest do
blocker = insert(:user)
blocked = insert(:user)
- {:ok, blocker} = User.block(blocker, blocked)
+ {:ok, _user_relationship} = User.block(blocker, blocked)
{:error, _} = User.subscribe(blocked, blocker)
end
@@ -210,15 +297,7 @@ defmodule Pleroma.UserTest do
end
describe "unfollow/2" do
- setup do
- setting = Pleroma.Config.get([:instance, :external_user_synchronization])
-
- on_exit(fn ->
- Pleroma.Config.put([:instance, :external_user_synchronization], setting)
- end)
-
- :ok
- end
+ clear_config([:instance, :external_user_synchronization])
test "unfollow with syncronizes external user" do
Pleroma.Config.put([:instance, :external_user_synchronization], true)
@@ -296,6 +375,7 @@ defmodule Pleroma.UserTest do
password_confirmation: "test",
email: "email@example.com"
}
+
clear_config([:instance, :autofollowed_nicknames])
clear_config([:instance, :welcome_message])
clear_config([:instance, :welcome_user_nickname])
@@ -332,7 +412,11 @@ defmodule Pleroma.UserTest do
assert activity.actor == welcome_user.ap_id
end
- test "it requires an email, name, nickname and password, bio is optional" do
+ clear_config([:instance, :account_activation_required])
+
+ 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 ->
@@ -343,6 +427,19 @@ defmodule Pleroma.UserTest do
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])
@@ -498,7 +595,7 @@ defmodule Pleroma.UserTest do
user = insert(:user)
assert User.ap_id(user) ==
- Pleroma.Web.Router.Helpers.feed_url(
+ Pleroma.Web.Router.Helpers.user_feed_url(
Pleroma.Web.Endpoint,
:feed_redirect,
user.nickname
@@ -509,7 +606,7 @@ defmodule Pleroma.UserTest do
user = insert(:user)
assert User.ap_followers(user) ==
- Pleroma.Web.Router.Helpers.feed_url(
+ Pleroma.Web.Router.Helpers.user_feed_url(
Pleroma.Web.Endpoint,
:feed_redirect,
user.nickname
@@ -678,7 +775,7 @@ defmodule Pleroma.UserTest do
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
- {:ok, user} = User.mute(user, muted_user)
+ {:ok, _user_relationships} = User.mute(user, muted_user)
assert User.mutes?(user, muted_user)
assert User.muted_notifications?(user, muted_user)
@@ -688,8 +785,8 @@ defmodule Pleroma.UserTest do
user = insert(:user)
muted_user = insert(:user)
- {:ok, user} = User.mute(user, muted_user)
- {:ok, user} = User.unmute(user, muted_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)
@@ -702,7 +799,7 @@ defmodule Pleroma.UserTest do
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
- {:ok, user} = User.mute(user, muted_user, false)
+ {:ok, _user_relationships} = User.mute(user, muted_user, false)
assert User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
@@ -716,7 +813,7 @@ defmodule Pleroma.UserTest do
refute User.blocks?(user, blocked_user)
- {:ok, user} = User.block(user, blocked_user)
+ {:ok, _user_relationship} = User.block(user, blocked_user)
assert User.blocks?(user, blocked_user)
end
@@ -725,8 +822,8 @@ defmodule Pleroma.UserTest do
user = insert(:user)
blocked_user = insert(:user)
- {:ok, user} = User.block(user, blocked_user)
- {:ok, user} = User.unblock(user, blocked_user)
+ {:ok, _user_relationship} = User.block(user, blocked_user)
+ {:ok, _user_block} = User.unblock(user, blocked_user)
refute User.blocks?(user, blocked_user)
end
@@ -741,7 +838,7 @@ defmodule Pleroma.UserTest do
assert User.following?(blocker, blocked)
assert User.following?(blocked, blocker)
- {:ok, blocker} = User.block(blocker, blocked)
+ {:ok, _user_relationship} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
@@ -759,7 +856,7 @@ defmodule Pleroma.UserTest do
assert User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
- {:ok, blocker} = User.block(blocker, blocked)
+ {:ok, _user_relationship} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
@@ -777,7 +874,7 @@ defmodule Pleroma.UserTest do
refute User.following?(blocker, blocked)
assert User.following?(blocked, blocker)
- {:ok, blocker} = User.block(blocker, blocked)
+ {:ok, _user_relationship} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
@@ -790,12 +887,12 @@ defmodule Pleroma.UserTest do
blocker = insert(:user)
blocked = insert(:user)
- {:ok, blocker} = User.subscribe(blocked, blocker)
+ {:ok, _subscription} = User.subscribe(blocked, blocker)
assert User.subscribed_to?(blocked, blocker)
refute User.subscribed_to?(blocker, blocked)
- {:ok, blocker} = User.block(blocker, blocked)
+ {:ok, _user_relationship} = User.block(blocker, blocked)
assert User.blocks?(blocker, blocked)
refute User.subscribed_to?(blocker, blocked)
@@ -864,6 +961,16 @@ defmodule Pleroma.UserTest do
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 "blocks_import" do
@@ -1189,23 +1296,35 @@ defmodule Pleroma.UserTest do
end
end
- test "auth_active?/1 works correctly" do
- Pleroma.Config.put([:instance, :account_activation_required], true)
+ describe "account_status/1" do
+ clear_config([:instance, :account_activation_required])
- local_user = insert(:user, local: true, confirmation_pending: true)
- confirmed_user = insert(:user, local: true, confirmation_pending: false)
- remote_user = insert(:user, local: false)
+ 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
- refute User.auth_active?(local_user)
- assert User.auth_active?(confirmed_user)
- assert User.auth_active?(remote_user)
+ 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
- # also shows unactive for deactivated users
+ test "return active for remote user" do
+ user = insert(:user, local: false)
+ assert User.account_status(user) == :active
+ end
- deactivated_but_confirmed =
- insert(:user, local: true, confirmation_pending: false, deactivated: true)
+ 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
- refute User.auth_active?(deactivated_but_confirmed)
+ test "returns :deactivated for deactivated user" do
+ user = insert(:user, local: true, confirmation_pending: false, deactivated: true)
+ assert User.account_status(user) == :deactivated
+ end
end
describe "superuser?/1" do
@@ -1324,7 +1443,8 @@ defmodule Pleroma.UserTest do
{:ok, _follower2} = User.follow(follower2, user)
{:ok, _follower3} = User.follow(follower3, user)
- {:ok, user} = User.block(user, follower)
+ {:ok, _user_relationship} = User.block(user, follower)
+ user = refresh_record(user)
assert user.follower_count == 2
end
@@ -1644,17 +1764,14 @@ defmodule Pleroma.UserTest do
describe "get_cached_by_nickname_or_id" do
setup do
- limit_to_local_content = Pleroma.Config.get([:instance, :limit_to_local_content])
local_user = insert(:user)
remote_user = insert(:user, nickname: "nickname@example.com", local: false)
- on_exit(fn ->
- Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local_content)
- end)
-
[local_user: local_user, remote_user: remote_user]
end
+ 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
diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs
index 1aa73d75c..bd8e0b5cc 100644
--- a/test/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/web/activity_pub/activity_pub_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
import Pleroma.Factory
alias Pleroma.Activity
+ alias Pleroma.Config
alias Pleroma.Delivery
alias Pleroma.Instances
alias Pleroma.Object
@@ -25,9 +26,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
:ok
end
- clear_config_all([:instance, :federating],
- do: Pleroma.Config.put([:instance, :federating], true)
- )
+ clear_config([:instance, :federating]) do
+ Config.put([:instance, :federating], true)
+ end
describe "/relay" do
clear_config([:instance, :allow_relay])
@@ -42,12 +43,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
end
test "with the relay disabled, it returns 404", %{conn: conn} do
- Pleroma.Config.put([:instance, :allow_relay], false)
+ Config.put([:instance, :allow_relay], false)
conn
|> get(activity_pub_path(conn, :relay))
|> json_response(404)
- |> assert
+ 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
@@ -60,6 +70,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
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
@@ -123,9 +143,34 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
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 "/object/:uuid" do
+ describe "/objects/:uuid" do
test "it returns a json representation of the object with accept application/json", %{
conn: conn
} do
@@ -236,6 +281,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
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
@@ -298,7 +355,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert json_response(conn1, :ok)
assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
- Activity.delete_by_ap_id(activity.object.data["id"])
+ Activity.delete_all_by_object_ap_id(activity.object.data["id"])
conn2 =
conn
@@ -307,6 +364,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
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
@@ -341,6 +410,72 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
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, ["relay.mastodon.host"]}
+ end
+
+ 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
@@ -479,22 +614,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
test "it rejects reads from other users", %{conn: conn} do
user = insert(:user)
- otheruser = insert(:user)
-
- conn =
- conn
- |> assign(:user, otheruser)
- |> put_req_header("accept", "application/activity+json")
- |> get("/users/#{user.nickname}/inbox")
-
- assert json_response(conn, 403)
- end
-
- test "it doesn't crash without an authenticated user", %{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")
@@ -575,14 +699,30 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
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 "/users/:nickname/outbox" do
- test "it will not bomb when there is no activity", %{conn: conn} do
+ describe "GET /users/:nickname/outbox" do
+ test "it returns 200 even if there're no activities", %{conn: conn} do
user = insert(:user)
conn =
conn
+ |> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/outbox")
@@ -597,6 +737,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
+ |> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/outbox?page=true")
@@ -609,24 +750,38 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
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 rejects posts from other users", %{conn: conn} do
+ 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" do
+ test "it rejects posts from other users / unauuthenticated users", %{conn: conn} do
data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
user = insert(:user)
- otheruser = insert(:user)
+ other_user = insert(:user)
+ conn = put_req_header(conn, "content-type", "application/activity+json")
- conn =
- conn
- |> assign(:user, otheruser)
- |> put_req_header("content-type", "application/activity+json")
- |> post("/users/#{user.nickname}/outbox", data)
+ conn
+ |> post("/users/#{user.nickname}/outbox", data)
+ |> json_response(403)
- assert json_response(conn, 403)
+ conn
+ |> assign(:user, other_user)
+ |> post("/users/#{user.nickname}/outbox", data)
+ |> json_response(403)
end
test "it inserts an incoming create activity into the database", %{conn: conn} do
@@ -741,24 +896,42 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
result =
conn
- |> assign(:relay, true)
|> 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
- |> assign(:relay, true)
|> 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
@@ -769,32 +942,36 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
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 returns a uri if the user has 'hide_followers' set", %{conn: conn} do
+ 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 not authenticated",
+ 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, hide_followers: true)
+ user = insert(:user)
+ other_user = insert(:user, hide_followers: true)
result =
conn
- |> get("/users/#{user.nickname}/followers?page=1")
+ |> assign(:user, user)
+ |> get("/users/#{other_user.nickname}/followers?page=1")
assert result.status == 403
assert result.resp_body == ""
@@ -826,6 +1003,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
result =
conn
+ |> assign(:user, user)
|> get("/users/#{user.nickname}/followers")
|> json_response(200)
@@ -835,12 +1013,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
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 "returns 403 if requester is not logged in", %{conn: conn} do
+ user = insert(:user)
+
+ conn
+ |> get("/users/#{user.nickname}/followers")
+ |> json_response(403)
+ end
end
describe "/users/:nickname/following" do
@@ -851,6 +1038,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
result =
conn
+ |> assign(:user, user)
|> get("/users/#{user.nickname}/following")
|> json_response(200)
@@ -858,25 +1046,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
end
test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
- user = insert(:user, hide_follows: true)
- user_two = insert(:user)
+ user = insert(:user)
+ user_two = insert(:user, hide_follows: true)
User.follow(user, user_two)
result =
conn
- |> get("/users/#{user.nickname}/following")
+ |> 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 not authenticated",
+ 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, hide_follows: true)
+ user = insert(:user)
+ user_two = insert(:user, hide_follows: true)
result =
conn
- |> get("/users/#{user.nickname}/following?page=1")
+ |> assign(:user, user)
+ |> get("/users/#{user_two.nickname}/following?page=1")
assert result.status == 403
assert result.resp_body == ""
@@ -909,6 +1100,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
result =
conn
+ |> assign(:user, user)
|> get("/users/#{user.nickname}/following")
|> json_response(200)
@@ -918,12 +1110,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
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 "returns 403 if requester is not logged in", %{conn: conn} do
+ user = insert(:user)
+
+ conn
+ |> get("/users/#{user.nickname}/following")
+ |> json_response(403)
+ end
end
describe "delivery tracking" do
@@ -1008,8 +1209,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
end
end
- describe "Additionnal ActivityPub C2S endpoints" do
- test "/api/ap/whoami", %{conn: conn} do
+ describe "Additional ActivityPub C2S endpoints" do
+ test "GET /api/ap/whoami", %{conn: conn} do
user = insert(:user)
conn =
@@ -1020,12 +1221,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
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
clear_config([:media_proxy])
clear_config([Pleroma.Upload])
- test "uploadMedia", %{conn: conn} do
+ test "POST /api/ap/upload_media", %{conn: conn} do
user = insert(:user)
desc = "Description of the image"
@@ -1045,6 +1250,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert object["name"] == desc
assert object["type"] == "Document"
assert object["actor"] == user.ap_id
+
+ 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
index 2677b9e36..d5dd44cc3 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
alias Pleroma.Activity
alias Pleroma.Builders.ActivityBuilder
+ alias Pleroma.Config
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.User
@@ -15,6 +16,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.Federator
import Pleroma.Factory
import Tesla.Mock
@@ -224,7 +226,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
describe "insertion" do
test "drops activities beyond a certain limit" do
- limit = Pleroma.Config.get([:instance, :remote_limit])
+ limit = Config.get([:instance, :remote_limit])
random_text =
:crypto.strong_rand_bytes(limit + 1)
@@ -385,6 +387,27 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
describe "create activities" do
+ test "it reverts create" do
+ user = insert(:user)
+
+ 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 "removes doubled 'to' recipients" do
user = insert(:user)
@@ -487,7 +510,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
activity_five = insert(:note_activity)
user = insert(:user)
- {:ok, user} = User.block(user, %{ap_id: activity_five.data["actor"]})
+ {: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]
@@ -500,7 +523,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
- {:ok, user} = User.block(user, %{ap_id: activity_one.data["actor"]})
+ {:ok, _user_relationship} = User.block(user, %{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
@@ -509,7 +532,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_one)
- {:ok, user} = User.unblock(user, %{ap_id: activity_one.data["actor"]})
+ {:ok, _user_block} = User.unblock(user, %{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
@@ -518,7 +541,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
- {:ok, user} = User.block(user, %{ap_id: activity_three.data["actor"]})
+ {:ok, _user_relationship} = User.block(user, %{ap_id: activity_three.data["actor"]})
{:ok, _announce, %{data: %{"id" => 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)
@@ -545,7 +568,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
blockee = insert(:user)
friend = insert(:user)
- {:ok, blocker} = User.block(blocker, blockee)
+ {:ok, _user_relationship} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
@@ -568,7 +591,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
blockee = insert(:user)
friend = insert(:user)
- {:ok, blocker} = User.block(blocker, blockee)
+ {:ok, _user_relationship} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
@@ -608,13 +631,48 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
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)
- {:ok, user} = User.mute(user, %User{ap_id: activity_one.data["actor"]})
+
+ 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})
@@ -635,7 +693,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
- {:ok, user} = User.unmute(user, %User{ap_id: activity_one.data["actor"]})
+ {:ok, _user_mute} = User.unmute(user, activity_one_actor)
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
@@ -644,7 +702,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
- {:ok, user} = User.mute(user, %User{ap_id: activity_three.data["actor"]})
+ activity_three_actor = User.get_by_ap_id(activity_three.data["actor"])
+ {:ok, _user_relationships} = User.mute(user, activity_three_actor)
{:ok, _announce, %{data: %{"id" => 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)
@@ -791,7 +850,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
activity = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
- {:ok, user} = CommonAPI.hide_reblogs(user, booster)
+ {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster)
{:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
@@ -804,8 +863,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
activity = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
- {:ok, user} = CommonAPI.hide_reblogs(user, booster)
- {:ok, user} = CommonAPI.show_reblogs(user, booster)
+ {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster)
+ {:ok, _reblog_mute} = CommonAPI.show_reblogs(user, booster)
{:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
@@ -816,8 +875,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
describe "react to an object" do
- test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
- Pleroma.Config.put([:instance, :federating], true)
+ test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
+ Config.put([:instance, :federating], true)
user = insert(:user)
reactor = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
@@ -825,12 +884,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
- assert called(Pleroma.Web.Federator.publish(reaction_activity))
+ assert called(Federator.publish(reaction_activity))
end
test "adds an emoji reaction activity to the db" do
user = insert(:user)
reactor = insert(:user)
+ third_user = insert(:user)
+ fourth_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity)
@@ -839,19 +900,48 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert reaction_activity
assert reaction_activity.data["actor"] == reactor.ap_id
- assert reaction_activity.data["type"] == "EmojiReaction"
+ assert reaction_activity.data["type"] == "EmojiReact"
assert reaction_activity.data["content"] == "🔥"
assert reaction_activity.data["object"] == object.data["id"]
assert reaction_activity.data["to"] == [User.ap_followers(reactor), activity.data["actor"]]
assert reaction_activity.data["context"] == object.data["context"]
assert object.data["reaction_count"] == 1
- assert object.data["reactions"]["🔥"] == [reactor.ap_id]
+ assert object.data["reactions"] == [["🔥", [reactor.ap_id]]]
+
+ {:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(third_user, object, "☕")
+
+ assert object.data["reaction_count"] == 2
+ assert object.data["reactions"] == [["🔥", [reactor.ap_id]], ["☕", [third_user.ap_id]]]
+
+ {:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(fourth_user, object, "🔥")
+
+ assert object.data["reaction_count"] == 3
+
+ assert object.data["reactions"] == [
+ ["🔥", [fourth_user.ap_id, reactor.ap_id]],
+ ["☕", [third_user.ap_id]]
+ ]
+ end
+
+ test "reverts emoji reaction on error" do
+ [user, reactor] = insert_list(2, :user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "Status"})
+ object = Object.normalize(activity)
+
+ with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+ assert {:error, :reverted} = ActivityPub.react_with_emoji(reactor, object, "😀")
+ end
+
+ object = Object.get_by_ap_id(object.data["id"])
+ refute object.data["reaction_count"]
+ refute object.data["reactions"]
end
end
describe "unreacting to an object" do
- test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
- Pleroma.Config.put([:instance, :federating], true)
+ test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
+ Config.put([:instance, :federating], true)
user = insert(:user)
reactor = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
@@ -859,12 +949,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
- assert called(Pleroma.Web.Federator.publish(reaction_activity))
+ assert called(Federator.publish(reaction_activity))
{:ok, unreaction_activity, _object} =
ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
- assert called(Pleroma.Web.Federator.publish(unreaction_activity))
+ assert called(Federator.publish(unreaction_activity))
end
test "adds an undo activity to the db" do
@@ -883,20 +973,38 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
object = Object.get_by_ap_id(object.data["id"])
assert object.data["reaction_count"] == 0
- assert object.data["reactions"] == %{}
+ assert object.data["reactions"] == []
+ end
+
+ test "reverts emoji unreact on error" do
+ [user, reactor] = insert_list(2, :user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "Status"})
+ object = Object.normalize(activity)
+
+ {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "😀")
+
+ with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+ assert {:error, :reverted} =
+ ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
+ end
+
+ object = Object.get_by_ap_id(object.data["id"])
+
+ assert object.data["reaction_count"] == 1
+ assert object.data["reactions"] == [["😀", [reactor.ap_id]]]
end
end
describe "like an object" do
- test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
- Pleroma.Config.put([:instance, :federating], true)
+ test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
+ Config.put([:instance, :federating], true)
note_activity = insert(:note_activity)
assert object_activity = Object.normalize(note_activity)
user = insert(:user)
{:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
- assert called(Pleroma.Web.Federator.publish(like_activity))
+ assert called(Federator.publish(like_activity))
end
test "returns exist activity if object already liked" do
@@ -911,6 +1019,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert like_activity == like_activity_exist
end
+ test "reverts like activity on error" do
+ note_activity = insert(:note_activity)
+ object = Object.normalize(note_activity)
+ user = insert(:user)
+
+ with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+ assert {:error, :reverted} = ActivityPub.like(user, object)
+ end
+
+ assert Repo.aggregate(Activity, :count, :id) == 1
+ assert Repo.get(Object, object.id) == object
+ end
+
test "adds a like activity to the db" do
note_activity = insert(:note_activity)
assert object = Object.normalize(note_activity)
@@ -941,15 +1062,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
describe "unliking" do
- test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
- Pleroma.Config.put([:instance, :federating], true)
+ test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
+ Config.put([:instance, :federating], true)
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
{:ok, object} = ActivityPub.unlike(user, object)
- refute called(Pleroma.Web.Federator.publish())
+ refute called(Federator.publish())
{:ok, _like_activity, object} = ActivityPub.like(user, object)
assert object.data["like_count"] == 1
@@ -957,7 +1078,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
{:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
- assert called(Pleroma.Web.Federator.publish(unlike_activity))
+ assert called(Federator.publish(unlike_activity))
+ end
+
+ test "reverts unliking on error" do
+ note_activity = insert(:note_activity)
+ object = Object.normalize(note_activity)
+ user = insert(:user)
+
+ {:ok, like_activity, object} = ActivityPub.like(user, object)
+ assert object.data["like_count"] == 1
+
+ with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+ assert {:error, :reverted} = ActivityPub.unlike(user, object)
+ end
+
+ assert Object.get_by_ap_id(object.data["id"]) == object
+ assert object.data["like_count"] == 1
+ assert Activity.get_by_id(like_activity.id)
end
test "unliking a previously liked object" do
@@ -999,6 +1137,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert announce_activity.data["actor"] == user.ap_id
assert announce_activity.data["context"] == object.data["context"]
end
+
+ test "reverts annouce from object on error" do
+ note_activity = insert(:note_activity)
+ object = Object.normalize(note_activity)
+ user = insert(:user)
+
+ with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+ assert {:error, :reverted} = ActivityPub.announce(user, object)
+ end
+
+ reloaded_object = Object.get_by_ap_id(object.data["id"])
+ assert reloaded_object == object
+ refute reloaded_object.data["announcement_count"]
+ refute reloaded_object.data["announcements"]
+ end
end
describe "announcing a private object" do
@@ -1041,8 +1194,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
user = insert(:user)
# Unannouncing an object that is not announced does nothing
- # {:ok, object} = ActivityPub.unannounce(user, object)
- # assert object.data["announcement_count"] == 0
+ {:ok, object} = ActivityPub.unannounce(user, object)
+ refute object.data["announcement_count"]
{:ok, announce_activity, object} = ActivityPub.announce(user, object)
assert object.data["announcement_count"] == 1
@@ -1062,6 +1215,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert Activity.get_by_id(announce_activity.id) == nil
end
+
+ test "reverts unannouncing on error" do
+ note_activity = insert(:note_activity)
+ object = Object.normalize(note_activity)
+ user = insert(:user)
+
+ {:ok, _announce_activity, object} = ActivityPub.announce(user, object)
+ assert object.data["announcement_count"] == 1
+
+ with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+ assert {:error, :reverted} = ActivityPub.unannounce(user, object)
+ end
+
+ object = Object.get_by_ap_id(object.data["id"])
+ assert object.data["announcement_count"] == 1
+ end
end
describe "uploading files" do
@@ -1096,6 +1265,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
describe "following / unfollowing" do
+ test "it reverts follow activity" do
+ follower = insert(:user)
+ followed = insert(:user)
+
+ with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+ assert {:error, :reverted} = ActivityPub.follow(follower, followed)
+ end
+
+ assert Repo.aggregate(Activity, :count, :id) == 0
+ assert Repo.aggregate(Object, :count, :id) == 0
+ end
+
+ test "it reverts unfollow activity" do
+ follower = insert(:user)
+ followed = insert(:user)
+
+ {:ok, follow_activity} = ActivityPub.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 a follow activity" do
follower = insert(:user)
followed = insert(:user)
@@ -1122,9 +1320,37 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
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} = ActivityPub.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 "blocking / unblocking" do
+ test "reverts block activity on error" do
+ [blocker, blocked] = insert_list(2, :user)
+
+ with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+ assert {:error, :reverted} = ActivityPub.block(blocker, blocked)
+ end
+
+ assert Repo.aggregate(Activity, :count, :id) == 0
+ assert Repo.aggregate(Object, :count, :id) == 0
+ end
+
test "creates a block activity" do
blocker = insert(:user)
blocked = insert(:user)
@@ -1136,6 +1362,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert activity.data["object"] == blocked.ap_id
end
+ test "reverts unblock activity on error" do
+ [blocker, blocked] = insert_list(2, :user)
+ {:ok, block_activity} = ActivityPub.block(blocker, blocked)
+
+ with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+ assert {:error, :reverted} = ActivityPub.unblock(blocker, blocked)
+ end
+
+ assert block_activity.data["type"] == "Block"
+ assert block_activity.data["actor"] == blocker.ap_id
+
+ assert Repo.aggregate(Activity, :count, :id) == 1
+ assert Repo.aggregate(Object, :count, :id) == 1
+ end
+
test "creates an undo activity for the last block" do
blocker = insert(:user)
blocked = insert(:user)
@@ -1155,6 +1396,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
describe "deletion" do
+ clear_config([:instance, :rewrite_policy])
+
+ test "it reverts deletion on error" do
+ note = insert(:note_activity)
+ object = Object.normalize(note)
+
+ with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+ assert {:error, :reverted} = ActivityPub.delete(object)
+ end
+
+ assert Repo.aggregate(Activity, :count, :id) == 1
+ assert Repo.get(Object, object.id) == object
+ assert Activity.get_by_id(note.id) == note
+ end
+
test "it creates a delete activity and deletes the original object" do
note = insert(:note_activity)
object = Object.normalize(note)
@@ -1256,6 +1512,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 0
end
+
+ test "it passes delete activity through MRF before deleting the object" do
+ Pleroma.Config.put([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.DropPolicy)
+
+ note = insert(:note_activity)
+ object = Object.normalize(note)
+
+ {:error, {:reject, _}} = ActivityPub.delete(object)
+
+ assert Activity.get_by_id(note.id)
+ assert Repo.get(Object, object.id).data["type"] == object.data["type"]
+ end
end
describe "timeline post-processing" do
@@ -1312,6 +1580,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
describe "update" do
+ clear_config([:instance, :max_pinned_statuses])
+
test "it creates an update activity with the new user data" do
user = insert(:user)
{:ok, user} = User.ensure_keys_present(user)
@@ -1334,7 +1604,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
test "returned pinned statuses" do
- Pleroma.Config.put([:instance, :max_pinned_statuses], 3)
+ Config.put([:instance, :max_pinned_statuses], 3)
user = insert(:user)
{:ok, activity_one} = CommonAPI.post(user, %{"status" => "HI!!!"})
@@ -1572,6 +1842,73 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
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
@@ -1619,10 +1956,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
activity = %Activity{activity | object: nil}
assert [%Notification{activity: ^activity}] =
- Notification.for_user_since(follower, ~N[2019-04-13 11:22:33])
+ Notification.for_user(follower, %{with_move: true})
assert [%Notification{activity: ^activity}] =
- Notification.for_user_since(follower_move_opted_out, ~N[2019-04-13 11:22:33])
+ Notification.for_user(follower_move_opted_out, %{with_move: true})
end
test "old user must be in the new user's `also_known_as` list" do
diff --git a/test/web/activity_pub/mrf/hellthread_policy_test.exs b/test/web/activity_pub/mrf/hellthread_policy_test.exs
index eb6ee4d04..916b95692 100644
--- a/test/web/activity_pub/mrf/hellthread_policy_test.exs
+++ b/test/web/activity_pub/mrf/hellthread_policy_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicyTest do
@@ -26,6 +26,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicyTest do
[user: user, message: message]
end
+ clear_config(:mrf_hellthread)
+
describe "reject" do
test "rejects the message if the recipient count is above reject_threshold", %{
message: message
diff --git a/test/web/activity_pub/mrf/keyword_policy_test.exs b/test/web/activity_pub/mrf/keyword_policy_test.exs
index 602892a37..18242a889 100644
--- a/test/web/activity_pub/mrf/keyword_policy_test.exs
+++ b/test/web/activity_pub/mrf/keyword_policy_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicyTest do
@@ -7,6 +7,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicyTest do
alias Pleroma.Web.ActivityPub.MRF.KeywordPolicy
+ clear_config(:mrf_keyword)
+
setup do
Pleroma.Config.put([:mrf_keyword], %{reject: [], federated_timeline_removal: [], replace: []})
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
index 95a809d25..313d59a66 100644
--- a/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs
+++ b/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do
diff --git a/test/web/activity_pub/mrf/mention_policy_test.exs b/test/web/activity_pub/mrf/mention_policy_test.exs
index 9fd9c31df..08f7be542 100644
--- a/test/web/activity_pub/mrf/mention_policy_test.exs
+++ b/test/web/activity_pub/mrf/mention_policy_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicyTest do
@@ -7,6 +7,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicyTest do
alias Pleroma.Web.ActivityPub.MRF.MentionPolicy
+ clear_config(:mrf_mention)
+
test "pass filter if allow list is empty" do
Pleroma.Config.delete([:mrf_mention])
diff --git a/test/web/activity_pub/mrf/subchain_policy_test.exs b/test/web/activity_pub/mrf/subchain_policy_test.exs
index f7cbcad48..221b8958e 100644
--- a/test/web/activity_pub/mrf/subchain_policy_test.exs
+++ b/test/web/activity_pub/mrf/subchain_policy_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicyTest do
@@ -14,6 +14,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicyTest do
"object" => %{"content" => "hi"}
}
+ 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]
diff --git a/test/web/activity_pub/mrf/tag_policy_test.exs b/test/web/activity_pub/mrf/tag_policy_test.exs
index 4aa35311e..e7793641a 100644
--- a/test/web/activity_pub/mrf/tag_policy_test.exs
+++ b/test/web/activity_pub/mrf/tag_policy_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.TagPolicyTest do
diff --git a/test/web/activity_pub/mrf/user_allowlist_policy_test.exs b/test/web/activity_pub/mrf/user_allowlist_policy_test.exs
index 72084c0fd..87c9e1b29 100644
--- a/test/web/activity_pub/mrf/user_allowlist_policy_test.exs
+++ b/test/web/activity_pub/mrf/user_allowlist_policy_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# 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
diff --git a/test/web/activity_pub/mrf/vocabulary_policy_test.exs b/test/web/activity_pub/mrf/vocabulary_policy_test.exs
index 38309f9f1..d9207b095 100644
--- a/test/web/activity_pub/mrf/vocabulary_policy_test.exs
+++ b/test/web/activity_pub/mrf/vocabulary_policy_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do
diff --git a/test/web/activity_pub/publisher_test.exs b/test/web/activity_pub/publisher_test.exs
index e885e5a5a..da26b13f7 100644
--- a/test/web/activity_pub/publisher_test.exs
+++ b/test/web/activity_pub/publisher_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.PublisherTest do
@@ -23,6 +23,31 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
:ok
end
+ clear_config_all([:instance, :federating]) do
+ Pleroma.Config.put([:instance, :federating], true)
+ end
+
+ 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 =
diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs
index 98dc78f46..e3115dcd8 100644
--- a/test/web/activity_pub/relay_test.exs
+++ b/test/web/activity_pub/relay_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.RelayTest do
diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs
index 75cfbea2e..c3d3f9830 100644
--- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs
+++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
@@ -19,6 +19,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
end
describe "handle_incoming" do
+ clear_config([:user, :deny_follow_blocked])
+
test "it works for osada follow request" do
user = insert(:user)
@@ -78,7 +80,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
)
|> Repo.all()
- assert length(accepts) == 0
+ assert Enum.empty?(accepts)
end
test "it works for follow requests when you are already followed, creating a new accept activity" do
@@ -128,7 +130,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
user = insert(:user)
{:ok, target} = User.get_or_fetch("http://mastodon.example.org/users/admin")
- {:ok, user} = User.block(user, target)
+ {:ok, _user_relationship} = User.block(user, target)
data =
File.read!("test/fixtures/mastodon-follow-activity.json")
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index 1910de6e0..83372ec7e 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -1,9 +1,11 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# 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.Object.Fetcher
@@ -40,7 +42,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
end
@tag capture_log: true
- test "it fetches replied-to activities if we don't have them" do
+ test "it fetches reply-to activities if we don't have them" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
@@ -61,7 +63,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert returned_object.data["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
end
- test "it does not fetch replied-to activities beyond max_replies_depth" do
+ 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!()
@@ -73,7 +75,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
data = Map.put(data, "object", object)
with_mock Pleroma.Web.Federator,
- allowed_incoming_reply_depth?: fn _ -> false end do
+ allowed_thread_distance?: fn _ -> false end do
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
returned_object = Object.normalize(returned_activity, false)
@@ -342,7 +344,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert data["object"] == activity.data["object"]
end
- test "it works for incoming misskey likes, turning them into EmojiReactions" do
+ test "it works for incoming misskey likes, turning them into EmojiReacts" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
@@ -354,13 +356,13 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == data["actor"]
- assert data["type"] == "EmojiReaction"
+ assert data["type"] == "EmojiReact"
assert data["id"] == data["id"]
assert data["object"] == activity.data["object"]
assert data["content"] == "🍮"
end
- test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReactions" do
+ 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"})
@@ -373,7 +375,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == data["actor"]
- assert data["type"] == "EmojiReaction"
+ assert data["type"] == "EmojiReact"
assert data["id"] == data["id"]
assert data["object"] == activity.data["object"]
assert data["content"] == "⭐"
@@ -391,12 +393,31 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
- assert data["type"] == "EmojiReaction"
+ assert data["type"] == "EmojiReact"
assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2"
assert data["object"] == activity.data["object"]
assert data["content"] == "👌"
end
+ test "it reject invalid emoji reactions" do
+ user = insert(:user)
+ {: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"])
+
+ assert :error = Transmogrifier.handle_incoming(data)
+
+ data =
+ File.read!("test/fixtures/emoji-reaction-no-emoji.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+
+ assert :error = Transmogrifier.handle_incoming(data)
+ end
+
test "it works for incoming emoji reaction undos" do
user = insert(:user)
@@ -1331,6 +1352,101 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
end
end
+ describe "`handle_incoming/2`, Mastodon format `replies` handling" do
+ clear_config([:activitypub, :note_replies_output_limit]) do
+ Pleroma.Config.put([:activitypub, :note_replies_output_limit], 5)
+ end
+
+ 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
+ clear_config([:activitypub, :note_replies_output_limit]) do
+ Pleroma.Config.put([:activitypub, :note_replies_output_limit], 5)
+ end
+
+ 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)
@@ -2029,4 +2145,49 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
}
end
end
+
+ describe "set_replies/1" do
+ clear_config([:activitypub, :note_replies_output_limit]) do
+ Pleroma.Config.put([:activitypub, :note_replies_output_limit], 2)
+ end
+
+ 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
end
diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs
index 1feb076ba..e913a5148 100644
--- a/test/web/activity_pub/utils_test.exs
+++ b/test/web/activity_pub/utils_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.UtilsTest do
@@ -177,71 +177,6 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
end
end
- describe "fetch_ordered_collection" do
- import Tesla.Mock
-
- test "fetches the first OrderedCollectionPage when an OrderedCollection is encountered" do
- mock(fn
- %{method: :get, url: "http://mastodon.com/outbox"} ->
- json(%{"type" => "OrderedCollection", "first" => "http://mastodon.com/outbox?page=true"})
-
- %{method: :get, url: "http://mastodon.com/outbox?page=true"} ->
- json(%{"type" => "OrderedCollectionPage", "orderedItems" => ["ok"]})
- end)
-
- assert Utils.fetch_ordered_collection("http://mastodon.com/outbox", 1) == ["ok"]
- end
-
- test "fetches several pages in the right order one after another, but only the specified amount" do
- mock(fn
- %{method: :get, url: "http://example.com/outbox"} ->
- json(%{
- "type" => "OrderedCollectionPage",
- "orderedItems" => [0],
- "next" => "http://example.com/outbox?page=1"
- })
-
- %{method: :get, url: "http://example.com/outbox?page=1"} ->
- json(%{
- "type" => "OrderedCollectionPage",
- "orderedItems" => [1],
- "next" => "http://example.com/outbox?page=2"
- })
-
- %{method: :get, url: "http://example.com/outbox?page=2"} ->
- json(%{"type" => "OrderedCollectionPage", "orderedItems" => [2]})
- end)
-
- assert Utils.fetch_ordered_collection("http://example.com/outbox", 0) == [0]
- assert Utils.fetch_ordered_collection("http://example.com/outbox", 1) == [0, 1]
- end
-
- test "returns an error if the url doesn't have an OrderedCollection/Page" do
- mock(fn
- %{method: :get, url: "http://example.com/not-an-outbox"} ->
- json(%{"type" => "NotAnOutbox"})
- end)
-
- assert {:error, _} = Utils.fetch_ordered_collection("http://example.com/not-an-outbox", 1)
- end
-
- test "returns the what was collected if there are less pages than specified" do
- mock(fn
- %{method: :get, url: "http://example.com/outbox"} ->
- json(%{
- "type" => "OrderedCollectionPage",
- "orderedItems" => [0],
- "next" => "http://example.com/outbox?page=1"
- })
-
- %{method: :get, url: "http://example.com/outbox?page=1"} ->
- json(%{"type" => "OrderedCollectionPage", "orderedItems" => [1]})
- end)
-
- assert Utils.fetch_ordered_collection("http://example.com/outbox", 5) == [0, 1]
- end
- end
-
test "make_json_ld_header/0" do
assert Utils.make_json_ld_header() == %{
"@context" => [
@@ -637,46 +572,16 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
end
end
- describe "get_reports_grouped_by_status/1" do
- setup do
- [reporter, target_user] = insert_pair(:user)
- first_status = insert(:note_activity, user: target_user)
- second_status = insert(:note_activity, user: target_user)
-
- CommonAPI.report(reporter, %{
- "account_id" => target_user.id,
- "comment" => "I feel offended",
- "status_ids" => [first_status.id]
- })
-
- CommonAPI.report(reporter, %{
- "account_id" => target_user.id,
- "comment" => "I feel offended2",
- "status_ids" => [second_status.id]
- })
-
- data = [%{activity: first_status.data["id"]}, %{activity: second_status.data["id"]}]
-
- {:ok,
- %{
- first_status: first_status,
- second_status: second_status,
- data: data
- }}
- end
-
- test "works for deprecated reports format", %{
- first_status: first_status,
- second_status: second_status,
- data: data
- } do
- groups = Utils.get_reports_grouped_by_status(data).groups
+ 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) == []
- first_group = Enum.find(groups, &(&1.status.id == first_status.data["id"]))
- second_group = Enum.find(groups, &(&1.status.id == second_status.data["id"]))
+ object = insert(:note, data: %{"reactions" => [["x", ["lain"]]]})
+ assert Utils.get_cached_emoji_reactions(object) == [["x", ["lain"]]]
- assert first_group.status.id == first_status.data["id"]
- assert second_group.status.id == second_status.data["id"]
+ 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
index 998247c5c..f6796ad4a 100644
--- a/test/web/activity_pub/views/object_view_test.exs
+++ b/test/web/activity_pub/views/object_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectViewTest do
@@ -36,6 +36,26 @@ defmodule Pleroma.Web.ActivityPub.ObjectViewTest do
assert result["@context"]
end
+ describe "note activity's `replies` collection rendering" do
+ clear_config([:activitypub, :note_replies_output_limit]) do
+ Pleroma.Config.put([:activitypub, :note_replies_output_limit], 5)
+ end
+
+ 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)
diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs
index 3299be2d5..ecb2dc386 100644
--- a/test/web/activity_pub/views/user_view_test.exs
+++ b/test/web/activity_pub/views/user_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.UserViewTest do
@@ -126,7 +126,7 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
{: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})
- assert %{"totalItems" => 0} = UserView.render("followers.json", %{user: user})
+ 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
diff --git a/test/web/activity_pub/visibilty_test.exs b/test/web/activity_pub/visibilty_test.exs
index 4c2e0d207..5b91630d4 100644
--- a/test/web/activity_pub/visibilty_test.exs
+++ b/test/web/activity_pub/visibilty_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.VisibilityTest do
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index 32577afee..e4c152fb7 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -1,22 +1,28 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# 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 Pleroma.Factory
+ import ExUnit.CaptureLog
+
alias Pleroma.Activity
+ alias Pleroma.Config
+ alias Pleroma.ConfigDB
alias Pleroma.HTML
alias Pleroma.ModerationLog
alias Pleroma.Repo
+ alias Pleroma.ReportNote
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
- import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -24,14 +30,129 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
: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 "with [:auth, :enforce_oauth_admin_scope_usage]," do
+ clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
+ Config.put([:auth, :enforce_oauth_admin_scope_usage], true)
+ end
+
+ test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope",
+ %{admin: admin} do
+ user = insert(:user)
+ url = "/api/pleroma/admin/users/#{user.nickname}"
+
+ good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"])
+ good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"])
+ good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"])
+
+ bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"])
+ bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"])
+ bad_token3 = nil
+
+ for good_token <- [good_token1, good_token2, good_token3] do
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> assign(:token, good_token)
+ |> get(url)
+
+ assert json_response(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
+ clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
+ Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
+ end
+
+ 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" do
- admin = insert(:user, is_admin: true)
+ test "single user", %{admin: admin, conn: conn} do
user = insert(:user)
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users?nickname=#{user.nickname}")
@@ -43,14 +164,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert json_response(conn, 200) == user.nickname
end
- test "multiple users" do
- admin = insert(:user, is_admin: true)
+ test "multiple users", %{admin: admin, conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users", %{
nicknames: [user_one.nickname, user_two.nickname]
@@ -67,12 +186,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "/api/pleroma/admin/users" do
- test "Create" do
- admin = insert(:user, is_admin: true)
-
+ test "Create", %{conn: conn} do
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
@@ -97,13 +213,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == []
end
- test "Cannot create user with exisiting email" do
- admin = insert(:user, is_admin: true)
+ test "Cannot create user with existing email", %{conn: conn} do
user = insert(:user)
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
@@ -128,13 +242,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
]
end
- test "Cannot create user with exisiting nickname" do
- admin = insert(:user, is_admin: true)
+ test "Cannot create user with existing nickname", %{conn: conn} do
user = insert(:user)
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
@@ -159,13 +271,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
]
end
- test "Multiple user creation works in transaction" do
- admin = insert(:user, is_admin: true)
+ test "Multiple user creation works in transaction", %{conn: conn} do
user = insert(:user)
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
@@ -209,13 +319,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
describe "/api/pleroma/admin/users/:nickname" do
test "Show", %{conn: conn} do
- admin = insert(:user, is_admin: true)
user = insert(:user)
- conn =
- conn
- |> assign(:user, admin)
- |> get("/api/pleroma/admin/users/#{user.nickname}")
+ conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}")
expected = %{
"deactivated" => false,
@@ -233,26 +339,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
test "when the user doesn't exist", %{conn: conn} do
- admin = insert(:user, is_admin: true)
user = build(:user)
- conn =
- conn
- |> assign(:user, admin)
- |> get("/api/pleroma/admin/users/#{user.nickname}")
+ conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}")
assert "Not found" == json_response(conn, 404)
end
end
describe "/api/pleroma/admin/users/follow" do
- test "allows to force-follow another user" do
- admin = insert(:user, is_admin: true)
+ test "allows to force-follow another user", %{admin: admin, conn: conn} do
user = insert(:user)
follower = insert(:user)
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/follow", %{
"follower" => follower.nickname,
@@ -272,15 +372,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "/api/pleroma/admin/users/unfollow" do
- test "allows to force-unfollow another user" do
- admin = insert(:user, is_admin: true)
+ test "allows to force-unfollow another user", %{admin: admin, conn: conn} do
user = insert(:user)
follower = insert(:user)
User.follow(follower, user)
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/unfollow", %{
"follower" => follower.nickname,
@@ -300,23 +398,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "PUT /api/pleroma/admin/users/tag" do
- setup do
- admin = insert(:user, is_admin: true)
+ setup %{conn: conn} do
user1 = insert(:user, %{tags: ["x"]})
user2 = insert(:user, %{tags: ["y"]})
user3 = insert(:user, %{tags: ["unchanged"]})
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> put(
- "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=#{
- user2.nickname
- }&tags[]=foo&tags[]=bar"
+ "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <>
+ "#{user2.nickname}&tags[]=foo&tags[]=bar"
)
- %{conn: conn, admin: admin, user1: user1, user2: user2, user3: user3}
+ %{conn: conn, user1: user1, user2: user2, user3: user3}
end
test "it appends specified tags to users with specified nicknames", %{
@@ -349,23 +444,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "DELETE /api/pleroma/admin/users/tag" do
- setup do
- admin = insert(:user, is_admin: true)
+ setup %{conn: conn} do
user1 = insert(:user, %{tags: ["x"]})
user2 = insert(:user, %{tags: ["y", "z"]})
user3 = insert(:user, %{tags: ["unchanged"]})
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> delete(
- "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=#{
- user2.nickname
- }&tags[]=x&tags[]=z"
+ "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <>
+ "#{user2.nickname}&tags[]=x&tags[]=z"
)
- %{conn: conn, admin: admin, user1: user1, user2: user2, user3: user3}
+ %{conn: conn, user1: user1, user2: user2, user3: user3}
end
test "it removes specified tags from users with specified nicknames", %{
@@ -398,12 +490,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "/api/pleroma/admin/users/:nickname/permission_group" do
- test "GET is giving user_info" do
- admin = insert(:user, is_admin: true)
-
+ test "GET is giving user_info", %{admin: admin, conn: conn} do
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> get("/api/pleroma/admin/users/#{admin.nickname}/permission_group/")
@@ -413,13 +502,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
}
end
- test "/:right POST, can add to a permission group" do
- admin = insert(:user, is_admin: true)
+ test "/:right POST, can add to a permission group", %{admin: admin, conn: conn} do
user = insert(:user)
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
@@ -433,22 +520,18 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"@#{admin.nickname} made @#{user.nickname} admin"
end
- test "/:right POST, can add to a permission group (multiple)" do
- admin = insert(:user, is_admin: true)
+ test "/:right POST, can add to a permission group (multiple)", %{admin: admin, conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
conn =
- build_conn()
- |> assign(:user, admin)
+ 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
- }
+ assert json_response(conn, 200) == %{"is_admin" => true}
log_entry = Repo.one(ModerationLog)
@@ -456,19 +539,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin"
end
- test "/:right DELETE, can remove from a permission group" do
- admin = insert(:user, is_admin: true)
+ test "/:right DELETE, can remove from a permission group", %{admin: admin, conn: conn} do
user = insert(:user, is_admin: true)
conn =
- build_conn()
- |> assign(:user, admin)
+ 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
- }
+ assert json_response(conn, 200) == %{"is_admin" => false}
log_entry = Repo.one(ModerationLog)
@@ -476,22 +555,21 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"@#{admin.nickname} revoked admin role from @#{user.nickname}"
end
- test "/:right DELETE, can remove from a permission group (multiple)" do
- admin = insert(:user, is_admin: true)
+ 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 =
- build_conn()
- |> assign(:user, admin)
+ 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
- }
+ assert json_response(conn, 200) == %{"is_admin" => false}
log_entry = Repo.one(ModerationLog)
@@ -503,41 +581,36 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "POST /api/pleroma/admin/email_invite, with valid config" do
- setup do
- [user: insert(:user, is_admin: true)]
- end
-
clear_config([:instance, :registrations_open]) do
- Pleroma.Config.put([:instance, :registrations_open], false)
+ Config.put([:instance, :registrations_open], false)
end
clear_config([:instance, :invites_enabled]) do
- Pleroma.Config.put([:instance, :invites_enabled], true)
+ Config.put([:instance, :invites_enabled], true)
end
- test "sends invitation and returns 204", %{conn: conn, user: user} do
+ test "sends invitation and returns 204", %{admin: admin, conn: conn} do
recipient_email = "foo@bar.com"
recipient_name = "J. D."
conn =
- conn
- |> assign(:user, user)
- |> post(
+ post(
+ conn,
"/api/pleroma/admin/users/email_invite?email=#{recipient_email}&name=#{recipient_name}"
)
assert json_response(conn, :no_content)
- token_record = List.last(Pleroma.Repo.all(Pleroma.UserInviteToken))
+ token_record = List.last(Repo.all(Pleroma.UserInviteToken))
assert token_record
refute token_record.used
- notify_email = Pleroma.Config.get([:instance, :notify_email])
- instance_name = Pleroma.Config.get([:instance, :name])
+ notify_email = Config.get([:instance, :notify_email])
+ instance_name = Config.get([:instance, :name])
email =
Pleroma.Emails.UserEmail.user_invitation_email(
- user,
+ admin,
token_record,
recipient_email,
recipient_name
@@ -550,12 +623,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
)
end
- test "it returns 403 if requested by a non-admin", %{conn: conn} do
+ 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 =
- conn
+ build_conn()
|> assign(:user, non_admin_user)
+ |> assign(:token, token)
|> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :forbidden)
@@ -563,45 +638,33 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do
- setup do
- [user: insert(:user, is_admin: true)]
- end
-
clear_config([:instance, :registrations_open])
clear_config([:instance, :invites_enabled])
- test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn, user: user} do
- Pleroma.Config.put([:instance, :registrations_open], false)
- Pleroma.Config.put([:instance, :invites_enabled], false)
+ 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
- |> assign(:user, user)
- |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
+ conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :internal_server_error)
end
- test "it returns 500 if `registrations_open` is enabled", %{conn: conn, user: user} do
- Pleroma.Config.put([:instance, :registrations_open], true)
- Pleroma.Config.put([:instance, :invites_enabled], true)
+ 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
- |> assign(:user, user)
- |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
+ conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :internal_server_error)
end
end
- test "/api/pleroma/admin/users/:nickname/password_reset" do
- admin = insert(:user, is_admin: true)
+ test "/api/pleroma/admin/users/:nickname/password_reset", %{conn: conn} do
user = insert(:user)
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> get("/api/pleroma/admin/users/#{user.nickname}/password_reset")
@@ -611,16 +674,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "GET /api/pleroma/admin/users" do
- setup do
- admin = insert(:user, is_admin: true)
-
- conn =
- build_conn()
- |> assign(:user, admin)
-
- {:ok, conn: conn, admin: admin}
- end
-
test "renders users array for the first page", %{conn: conn, admin: admin} do
user = insert(:user, local: false, tags: ["foo", "bar"])
conn = get(conn, "/api/pleroma/admin/users?page=1")
@@ -842,6 +895,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
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)
@@ -849,6 +903,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
conn =
build_conn()
|> assign(:user, admin)
+ |> assign(:token, token)
|> get("/api/pleroma/admin/users?query=bo&filters=local")
assert json_response(conn, 200) == %{
@@ -870,16 +925,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
}
end
- test "only local users with no query", %{admin: old_admin} do
+ 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 =
- build_conn()
- |> assign(:user, admin)
- |> get("/api/pleroma/admin/users?filters=local")
+ conn = get(conn, "/api/pleroma/admin/users?filters=local")
users =
[
@@ -1037,6 +1089,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
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)
@@ -1045,6 +1098,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
conn =
build_conn()
|> assign(:user, admin)
+ |> assign(:token, token)
|> get("/api/pleroma/admin/users?filters=deactivated,external")
assert json_response(conn, 200) == %{
@@ -1066,13 +1120,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
}
end
- test "it omits relay user", %{admin: admin} do
+ test "it omits relay user", %{admin: admin, conn: conn} do
assert %User{} = Relay.get_actor()
- conn =
- build_conn()
- |> assign(:user, admin)
- |> get("/api/pleroma/admin/users")
+ conn = get(conn, "/api/pleroma/admin/users")
assert json_response(conn, 200) == %{
"count" => 1,
@@ -1094,15 +1145,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
end
- test "PATCH /api/pleroma/admin/users/activate" do
- admin = insert(:user, is_admin: true)
+ 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 =
- build_conn()
- |> assign(:user, admin)
- |> patch(
+ patch(
+ conn,
"/api/pleroma/admin/users/activate",
%{nicknames: [user_one.nickname, user_two.nickname]}
)
@@ -1116,15 +1165,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"@#{admin.nickname} activated users: @#{user_one.nickname}, @#{user_two.nickname}"
end
- test "PATCH /api/pleroma/admin/users/deactivate" do
- admin = insert(:user, is_admin: true)
+ 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 =
- build_conn()
- |> assign(:user, admin)
- |> patch(
+ patch(
+ conn,
"/api/pleroma/admin/users/deactivate",
%{nicknames: [user_one.nickname, user_two.nickname]}
)
@@ -1138,14 +1185,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}"
end
- test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
- admin = insert(:user, is_admin: true)
+ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do
user = insert(:user)
- conn =
- build_conn()
- |> assign(:user, admin)
- |> patch("/api/pleroma/admin/users/#{user.nickname}/toggle_activation")
+ conn = patch(conn, "/api/pleroma/admin/users/#{user.nickname}/toggle_activation")
assert json_response(conn, 200) ==
%{
@@ -1167,16 +1210,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "POST /api/pleroma/admin/users/invite_token" do
- setup do
- admin = insert(:user, is_admin: true)
-
- conn =
- build_conn()
- |> assign(:user, admin)
-
- {:ok, conn: conn}
- end
-
test "without options", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/users/invite_token")
@@ -1231,16 +1264,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "GET /api/pleroma/admin/users/invites" do
- setup do
- admin = insert(:user, is_admin: true)
-
- conn =
- build_conn()
- |> assign(:user, admin)
-
- {:ok, conn: conn}
- end
-
test "no invites", %{conn: conn} do
conn = get(conn, "/api/pleroma/admin/users/invites")
@@ -1269,14 +1292,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "POST /api/pleroma/admin/users/revoke_invite" do
- test "with token" do
- admin = insert(:user, is_admin: true)
+ test "with token", %{conn: conn} do
{:ok, invite} = UserInviteToken.create_invite()
- conn =
- build_conn()
- |> assign(:user, admin)
- |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token})
+ conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token})
assert json_response(conn, 200) == %{
"expires_at" => nil,
@@ -1289,25 +1308,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
}
end
- test "with invalid token" do
- admin = insert(:user, is_admin: true)
-
- conn =
- build_conn()
- |> assign(:user, admin)
- |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"})
+ test "with invalid token", %{conn: conn} do
+ conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"})
assert json_response(conn, :not_found) == "Not found"
end
end
describe "GET /api/pleroma/admin/reports/:id" do
- setup %{conn: conn} do
- admin = insert(:user, is_admin: true)
-
- %{conn: assign(conn, :user, admin)}
- end
-
test "returns report by its id", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
@@ -1335,8 +1343,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "PATCH /api/pleroma/admin/reports" do
- setup %{conn: conn} do
- admin = insert(:user, is_admin: true)
+ setup do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
@@ -1355,13 +1362,35 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
})
%{
- conn: assign(conn, :user, admin),
id: report_id,
- admin: admin,
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)
+ |> patch("/api/pleroma/admin/reports", %{
+ "reports" => [%{"state" => "resolved", "id" => id}]
+ })
+ |> json_response(403)
+
+ assert response == %{
+ "error" => "Insufficient permissions: admin:write:reports."
+ }
+
+ conn
+ |> assign(:token, write_token)
+ |> patch("/api/pleroma/admin/reports", %{
+ "reports" => [%{"state" => "resolved", "id" => id}]
+ })
+ |> json_response(:no_content)
+ end
+
test "mark report as resolved", %{conn: conn, id: id, admin: admin} do
conn
|> patch("/api/pleroma/admin/reports", %{
@@ -1453,12 +1482,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "GET /api/pleroma/admin/reports" do
- setup %{conn: conn} do
- admin = insert(:user, is_admin: true)
-
- %{conn: assign(conn, :user, admin)}
- end
-
test "returns empty response when no reports created", %{conn: conn} do
response =
conn
@@ -1553,27 +1576,27 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
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 admin."}
+ assert json_response(conn, :forbidden) ==
+ %{"error" => "User is not an admin or OAuth admin scope is not granted."}
end
test "returns 403 when requested by anonymous" do
- conn =
- build_conn()
- |> get("/api/pleroma/admin/reports")
+ conn = get(build_conn(), "/api/pleroma/admin/reports")
assert json_response(conn, :forbidden) == %{"error" => "Invalid credentials."}
end
end
describe "GET /api/pleroma/admin/grouped_reports" do
- setup %{conn: conn} do
- admin = insert(:user, is_admin: true)
+ setup do
[reporter, target_user] = insert_pair(:user)
date1 = (DateTime.to_unix(DateTime.utc_now()) + 1000) |> DateTime.from_unix!()
@@ -1608,10 +1631,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
})
%{
- conn: assign(conn, :user, admin),
first_status: Activity.get_by_ap_id_with_object(first_status.data["id"]),
second_status: Activity.get_by_ap_id_with_object(second_status.data["id"]),
third_status: Activity.get_by_ap_id_with_object(third_status.data["id"]),
+ first_report: first_report,
first_status_reports: [first_report, second_report, third_report],
second_status_reports: [first_report, second_report],
third_status_reports: [first_report],
@@ -1638,14 +1661,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert length(response["reports"]) == 3
- first_group =
- Enum.find(response["reports"], &(&1["status"]["id"] == first_status.data["id"]))
+ first_group = Enum.find(response["reports"], &(&1["status"]["id"] == first_status.id))
- second_group =
- Enum.find(response["reports"], &(&1["status"]["id"] == second_status.data["id"]))
+ second_group = Enum.find(response["reports"], &(&1["status"]["id"] == second_status.id))
- third_group =
- Enum.find(response["reports"], &(&1["status"]["id"] == third_status.data["id"]))
+ third_group = Enum.find(response["reports"], &(&1["status"]["id"] == third_status.id))
assert length(first_group["reports"]) == 3
assert length(second_group["reports"]) == 2
@@ -1656,13 +1676,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
NaiveDateTime.from_iso8601!(act.data["published"])
end).data["published"]
- assert first_group["status"] == %{
- "id" => first_status.data["id"],
- "content" => first_status.object.data["content"],
- "published" => first_status.object.data["published"]
- }
+ assert first_group["status"] ==
+ Map.put(
+ stringify_keys(StatusView.render("show.json", %{activity: first_status})),
+ "deleted",
+ false
+ )
- assert first_group["account"]["id"] == target_user.id
+ assert(first_group["account"]["id"] == target_user.id)
assert length(first_group["actors"]) == 1
assert hd(first_group["actors"])["id"] == reporter.id
@@ -1675,11 +1696,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
NaiveDateTime.from_iso8601!(act.data["published"])
end).data["published"]
- assert second_group["status"] == %{
- "id" => second_status.data["id"],
- "content" => second_status.object.data["content"],
- "published" => second_status.object.data["published"]
- }
+ assert second_group["status"] ==
+ Map.put(
+ stringify_keys(StatusView.render("show.json", %{activity: second_status})),
+ "deleted",
+ false
+ )
assert second_group["account"]["id"] == target_user.id
@@ -1694,11 +1716,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
NaiveDateTime.from_iso8601!(act.data["published"])
end).data["published"]
- assert third_group["status"] == %{
- "id" => third_status.data["id"],
- "content" => third_status.object.data["content"],
- "published" => third_status.object.data["published"]
- }
+ assert third_group["status"] ==
+ Map.put(
+ stringify_keys(StatusView.render("show.json", %{activity: third_status})),
+ "deleted",
+ false
+ )
assert third_group["account"]["id"] == target_user.id
@@ -1708,69 +1731,77 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert Enum.map(third_group["reports"], & &1["id"]) --
Enum.map(third_status_reports, & &1.id) == []
end
- end
- describe "POST /api/pleroma/admin/reports/:id/respond" do
- setup %{conn: conn} do
- admin = insert(:user, is_admin: true)
+ test "reopened report renders status data", %{
+ conn: conn,
+ first_report: first_report,
+ first_status: first_status
+ } do
+ {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved")
+
+ response =
+ conn
+ |> get("/api/pleroma/admin/grouped_reports")
+ |> json_response(:ok)
- %{conn: assign(conn, :user, admin), admin: admin}
+ first_group = Enum.find(response["reports"], &(&1["status"]["id"] == first_status.id))
+
+ assert first_group["status"] ==
+ Map.put(
+ stringify_keys(StatusView.render("show.json", %{activity: first_status})),
+ "deleted",
+ false
+ )
end
- test "returns created dm", %{conn: conn, admin: admin} do
- [reporter, target_user] = insert_pair(:user)
- activity = insert(:note_activity, user: target_user)
+ test "reopened report does not render status data if status has been deleted", %{
+ conn: conn,
+ first_report: first_report,
+ first_status: first_status,
+ target_user: target_user
+ } do
+ {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved")
+ {:ok, _} = CommonAPI.delete(first_status.id, target_user)
- {:ok, %{id: report_id}} =
- CommonAPI.report(reporter, %{
- "account_id" => target_user.id,
- "comment" => "I feel offended",
- "status_ids" => [activity.id]
- })
+ refute Activity.get_by_ap_id(first_status.id)
response =
conn
- |> post("/api/pleroma/admin/reports/#{report_id}/respond", %{
- "status" => "I will check it out"
- })
+ |> get("/api/pleroma/admin/grouped_reports")
|> json_response(:ok)
- recipients = Enum.map(response["mentions"], & &1["username"])
-
- assert reporter.nickname in recipients
- assert response["content"] == "I will check it out"
- assert response["visibility"] == "direct"
-
- log_entry = Repo.one(ModerationLog)
+ assert Enum.find(response["reports"], &(&1["status"]["deleted"] == true))["status"][
+ "deleted"
+ ] == true
- assert ModerationLog.get_log_entry_message(log_entry) ==
- "@#{admin.nickname} responded with 'I will check it out' to report ##{
- response["id"]
- }"
+ assert length(Enum.filter(response["reports"], &(&1["status"]["deleted"] == false))) == 2
end
- test "returns 400 when status is missing", %{conn: conn} do
- conn = post(conn, "/api/pleroma/admin/reports/test/respond")
+ test "account not empty if status was deleted", %{
+ conn: conn,
+ first_report: first_report,
+ first_status: first_status,
+ target_user: target_user
+ } do
+ {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved")
+ {:ok, _} = CommonAPI.delete(first_status.id, target_user)
- assert json_response(conn, :bad_request) == "Invalid parameters"
- end
+ refute Activity.get_by_ap_id(first_status.id)
- test "returns 404 when report id is invalid", %{conn: conn} do
- conn =
- post(conn, "/api/pleroma/admin/reports/test/respond", %{
- "status" => "foo"
- })
+ response =
+ conn
+ |> get("/api/pleroma/admin/grouped_reports")
+ |> json_response(:ok)
- assert json_response(conn, :not_found) == "Not found"
+ assert Enum.find(response["reports"], &(&1["status"]["deleted"] == true))["account"]
end
end
describe "PUT /api/pleroma/admin/statuses/:id" do
- setup %{conn: conn} do
- admin = insert(:user, is_admin: true)
+ setup do
activity = insert(:note_activity)
- %{conn: assign(conn, :user, admin), id: activity.id, admin: admin}
+ %{id: activity.id}
end
test "toggle sensitive flag", %{conn: conn, id: id, admin: admin} do
@@ -1823,20 +1854,17 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
test "returns 400 when visibility is unknown", %{conn: conn, id: id} do
- conn =
- conn
- |> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "test"})
+ conn = put(conn, "/api/pleroma/admin/statuses/#{id}", %{"visibility" => "test"})
assert json_response(conn, :bad_request) == "Unsupported visibility"
end
end
describe "DELETE /api/pleroma/admin/statuses/:id" do
- setup %{conn: conn} do
- admin = insert(:user, is_admin: true)
+ setup do
activity = insert(:note_activity)
- %{conn: assign(conn, :user, admin), id: activity.id, admin: admin}
+ %{id: activity.id}
end
test "deletes status", %{conn: conn, id: id, admin: admin} do
@@ -1852,41 +1880,41 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"@#{admin.nickname} deleted status ##{id}"
end
- test "returns error when status is not exist", %{conn: conn} do
- conn =
- conn
- |> delete("/api/pleroma/admin/statuses/test")
+ test "returns 404 when the status does not exist", %{conn: conn} do
+ conn = delete(conn, "/api/pleroma/admin/statuses/test")
- assert json_response(conn, :bad_request) == "Could not delete"
+ assert json_response(conn, :not_found) == "Not found"
end
end
describe "GET /api/pleroma/admin/config" do
- setup %{conn: conn} do
- admin = insert(:user, is_admin: true)
-
- %{conn: assign(conn, :user, admin)}
+ clear_config(:configurable_from_database) do
+ Config.put(:configurable_from_database, true)
end
- test "without any settings in db", %{conn: conn} do
+ 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(conn, 200) == %{"configs" => []}
+ assert json_response(conn, 400) ==
+ "To use this endpoint you need to enable configuration from database."
end
- test "with settings in db", %{conn: conn} do
+ test "with settings only in db", %{conn: conn} do
config1 = insert(:config)
config2 = insert(:config)
- conn = get(conn, "/api/pleroma/admin/config")
+ conn = get(conn, "/api/pleroma/admin/config", %{"only_db" => true})
%{
"configs" => [
%{
+ "group" => ":pleroma",
"key" => key1,
"value" => _
},
%{
+ "group" => ":pleroma",
"key" => key2,
"value" => _
}
@@ -1896,13 +1924,107 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert key1 == config1.key
assert key2 == config2.key
end
+
+ test "db is added to settings that are in db", %{conn: conn} do
+ _config = insert(:config, key: ":instance", value: ConfigDB.to_binary(name: "Some name"))
+
+ %{"configs" => configs} =
+ conn
+ |> get("/api/pleroma/admin/config")
+ |> json_response(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: ConfigDB.to_binary(k1: :v1, k2: :v2)
+ )
+
+ %{"configs" => configs} =
+ conn
+ |> get("/api/pleroma/admin/config")
+ |> json_response(200)
+
+ assert length(configs) > 3
+
+ received_configs =
+ Enum.filter(configs, fn %{"group" => group, "key" => key} ->
+ group == ":pleroma" and key in [config1.key, config2.key, config3.key]
+ end)
+
+ assert length(received_configs) == 3
+
+ db_keys =
+ config3.value
+ |> ConfigDB.from_binary()
+ |> Keyword.keys()
+ |> ConfigDB.convert()
+
+ Enum.each(received_configs, fn %{"value" => value, "db" => db} ->
+ assert db in [[config1.key], [config2.key], db_keys]
+
+ assert value in [
+ ConfigDB.from_binary_with_convert(config1.value),
+ ConfigDB.from_binary_with_convert(config2.value),
+ ConfigDB.from_binary_with_convert(config3.value)
+ ]
+ end)
+ end
+
+ test "subkeys with full update right merge", %{conn: conn} do
+ config1 =
+ insert(:config,
+ key: ":emoji",
+ value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1])
+ )
+
+ config2 =
+ insert(:config,
+ key: ":assets",
+ value: ConfigDB.to_binary(mascots: [a: 1, b: 2], key: [a: 1])
+ )
+
+ %{"configs" => configs} =
+ conn
+ |> get("/api/pleroma/admin/config")
+ |> json_response(200)
+
+ vals =
+ Enum.filter(configs, fn %{"group" => group, "key" => key} ->
+ group == ":pleroma" and key in [config1.key, config2.key]
+ end)
+
+ emoji = Enum.find(vals, fn %{"key" => key} -> key == ":emoji" end)
+ assets = Enum.find(vals, fn %{"key" => key} -> key == ":assets" end)
+
+ emoji_val = ConfigDB.transform_with_out_binary(emoji["value"])
+ assets_val = ConfigDB.transform_with_out_binary(assets["value"])
+
+ assert emoji_val[:groups] == [a: 1, b: 2]
+ assert assets_val[:mascots] == [a: 1, b: 2]
+ end
end
- describe "POST /api/pleroma/admin/config" do
- setup %{conn: conn} do
- admin = insert(:user, is_admin: true)
+ test "POST /api/pleroma/admin/config error", %{conn: conn} do
+ conn = post(conn, "/api/pleroma/admin/config", %{"configs" => []})
- temp_file = "config/test.exported_from_db.secret.exs"
+ assert json_response(conn, 400) ==
+ "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)
@@ -1913,30 +2035,33 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
Application.delete_env(:pleroma, :keyaa2)
Application.delete_env(:pleroma, Pleroma.Web.Endpoint.NotReal)
Application.delete_env(:pleroma, Pleroma.Captcha.NotReal)
- :ok = File.rm(temp_file)
+ Application.put_env(:pleroma, :http, http)
+ Application.put_env(:tesla, :adapter, Tesla.Mock)
+ Restarter.Pleroma.refresh()
end)
-
- %{conn: assign(conn, :user, admin)}
end
- clear_config([:instance, :dynamic_configuration]) do
- Pleroma.Config.put([:instance, :dynamic_configuration], true)
+ clear_config(:configurable_from_database) do
+ Config.put(:configurable_from_database, true)
end
@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 =
post(conn, "/api/pleroma/admin/config", %{
configs: [
- %{group: "pleroma", key: "key1", value: "value1"},
+ %{group: ":pleroma", key: ":key1", value: "value1"},
%{
- group: "ueberauth",
- key: "Ueberauth.Strategy.Twitter.OAuth",
+ group: ":ueberauth",
+ key: "Ueberauth",
value: [%{"tuple" => [":consumer_secret", "aaaa"]}]
},
%{
- group: "pleroma",
- key: "key2",
+ group: ":pleroma",
+ key: ":key2",
value: %{
":nested_1" => "nested_value1",
":nested_2" => [
@@ -1946,21 +2071,21 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
}
},
%{
- group: "pleroma",
- key: "key3",
+ group: ":pleroma",
+ key: ":key3",
value: [
%{"nested_3" => ":nested_3", "nested_33" => "nested_33"},
%{"nested_4" => true}
]
},
%{
- group: "pleroma",
- key: "key4",
+ group: ":pleroma",
+ key: ":key4",
value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"}
},
%{
- group: "idna",
- key: "key5",
+ group: ":idna",
+ key: ":key5",
value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}
}
]
@@ -1969,43 +2094,49 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert json_response(conn, 200) == %{
"configs" => [
%{
- "group" => "pleroma",
- "key" => "key1",
- "value" => "value1"
+ "group" => ":pleroma",
+ "key" => ":key1",
+ "value" => "value1",
+ "db" => [":key1"]
},
%{
- "group" => "ueberauth",
- "key" => "Ueberauth.Strategy.Twitter.OAuth",
- "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}]
+ "group" => ":ueberauth",
+ "key" => "Ueberauth",
+ "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}],
+ "db" => [":consumer_secret"]
},
%{
- "group" => "pleroma",
- "key" => "key2",
+ "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",
+ "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"}
+ "group" => ":pleroma",
+ "key" => ":key4",
+ "value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"},
+ "db" => [":key4"]
},
%{
- "group" => "idna",
- "key" => "key5",
- "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}
+ "group" => ":idna",
+ "key" => ":key5",
+ "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]},
+ "db" => [":key5"]
}
]
}
@@ -2033,25 +2164,34 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []}
end
- test "update config setting & delete", %{conn: conn} do
- config1 = insert(:config, key: "keyaa1")
- config2 = insert(:config, key: "keyaa2")
+ 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)
- insert(:config,
- group: "ueberauth",
- key: "Ueberauth.Strategy.Microsoft.OAuth",
- value: :erlang.term_to_binary([])
- )
+ 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 =
post(conn, "/api/pleroma/admin/config", %{
configs: [
- %{group: config1.group, key: config1.key, value: "another_value"},
- %{group: config2.group, key: config2.key, delete: "true"},
%{
- group: "ueberauth",
- key: "Ueberauth.Strategy.Microsoft.OAuth",
- delete: "true"
+ group: ":quack",
+ key: ":level",
+ value: ":info"
+ },
+ %{
+ group: ":quack",
+ key: ":meta",
+ value: [":none"]
+ },
+ %{
+ group: ":quack",
+ key: ":webhook_url",
+ value: "https://hooks.slack.com/services/KEY"
}
]
})
@@ -2059,23 +2199,400 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert json_response(conn, 200) == %{
"configs" => [
%{
- "group" => "pleroma",
+ "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"]
+ }
+ ]
+ }
+
+ 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
+ config = insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2))
+
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ configs: [
+ %{group: config.group, key: config.key, value: [%{"tuple" => [":key3", 3]}]}
+ ]
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":pleroma",
+ "key" => ":key1",
+ "value" => [
+ %{"tuple" => [":key1", 1]},
+ %{"tuple" => [":key2", 2]},
+ %{"tuple" => [":key3", 3]}
+ ],
+ "db" => [":key1", ":key2", ":key3"]
+ }
+ ]
+ }
+ end
+
+ test "saving config which need pleroma reboot", %{conn: conn} do
+ chat = Config.get(:chat)
+ on_exit(fn -> Config.put(:chat, chat) end)
+
+ assert post(
+ conn,
+ "/api/pleroma/admin/config",
+ %{
+ configs: [
+ %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]}
+ ]
+ }
+ )
+ |> json_response(200) == %{
+ "configs" => [
+ %{
+ "db" => [":enabled"],
+ "group" => ":pleroma",
+ "key" => ":chat",
+ "value" => [%{"tuple" => [":enabled", true]}]
+ }
+ ],
+ "need_reboot" => true
+ }
+
+ configs =
+ conn
+ |> get("/api/pleroma/admin/config")
+ |> json_response(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(200)
+
+ refute Map.has_key?(configs, "need_reboot")
+ 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 post(
+ conn,
+ "/api/pleroma/admin/config",
+ %{
+ configs: [
+ %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]}
+ ]
+ }
+ )
+ |> json_response(200) == %{
+ "configs" => [
+ %{
+ "db" => [":enabled"],
+ "group" => ":pleroma",
+ "key" => ":chat",
+ "value" => [%{"tuple" => [":enabled", true]}]
+ }
+ ],
+ "need_reboot" => true
+ }
+
+ assert post(conn, "/api/pleroma/admin/config", %{
+ configs: [
+ %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]}
+ ]
+ })
+ |> json_response(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(200)
+
+ refute Map.has_key?(configs, "need_reboot")
+ end
+
+ test "saving config with nested merge", %{conn: conn} do
+ config =
+ insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: [k1: 1, k2: 2]))
+
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ configs: [
+ %{
+ group: config.group,
+ key: config.key,
+ value: [
+ %{"tuple" => [":key3", 3]},
+ %{
+ "tuple" => [
+ ":key2",
+ [
+ %{"tuple" => [":k2", 1]},
+ %{"tuple" => [":k3", 3]}
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+ })
+
+ assert json_response(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"]
+ }
+ ]
+ }
+ end
+
+ test "saving special atoms", %{conn: conn} do
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ "configs" => [
+ %{
+ "group" => ":pleroma",
+ "key" => ":key1",
+ "value" => [
+ %{
+ "tuple" => [
+ ":ssl_options",
+ [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}]
+ ]
+ }
+ ]
+ }
+ ]
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":pleroma",
+ "key" => ":key1",
+ "value" => [
+ %{
+ "tuple" => [
+ ":ssl_options",
+ [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}]
+ ]
+ }
+ ],
+ "db" => [":ssl_options"]
+ }
+ ]
+ }
+
+ 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)
+
+ config =
+ insert(:config,
+ group: ":logger",
+ key: ":backends",
+ value: :erlang.term_to_binary([])
+ )
+
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ configs: [
+ %{
+ group: config.group,
+ key: config.key,
+ value: [":console", %{"tuple" => ["ExSyslogger", ":ex_syslogger"]}]
+ }
+ ]
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":logger",
+ "key" => ":backends",
+ "value" => [
+ ":console",
+ %{"tuple" => ["ExSyslogger", ":ex_syslogger"]}
+ ],
+ "db" => [":backends"]
+ }
+ ]
+ }
+
+ assert Application.get_env(:logger, :backends) == [
+ :console,
+ {ExSyslogger, :ex_syslogger}
+ ]
+
+ capture_log(fn ->
+ require Logger
+ Logger.warn("Ooops...")
+ end) =~ "Ooops..."
+ end
+
+ test "saving full setting if value is not keyword", %{conn: conn} do
+ config =
+ insert(:config,
+ group: ":tesla",
+ key: ":adapter",
+ value: :erlang.term_to_binary(Tesla.Adapter.Hackey)
+ )
+
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ configs: [
+ %{group: config.group, key: config.key, value: "Tesla.Adapter.Httpc"}
+ ]
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":tesla",
+ "key" => ":adapter",
+ "value" => "Tesla.Adapter.Httpc",
+ "db" => [":adapter"]
+ }
+ ]
+ }
+ end
+
+ test "update config setting & delete with fallback to default value", %{
+ conn: conn,
+ admin: admin,
+ token: token
+ } do
+ ueberauth = Application.get_env(:ueberauth, Ueberauth)
+ config1 = insert(:config, key: ":keyaa1")
+ config2 = insert(:config, key: ":keyaa2")
+
+ config3 =
+ insert(:config,
+ group: ":ueberauth",
+ key: "Ueberauth"
+ )
+
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ configs: [
+ %{group: config1.group, key: config1.key, value: "another_value"},
+ %{group: config2.group, key: config2.key, value: "another_value"}
+ ]
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":pleroma",
"key" => config1.key,
- "value" => "another_value"
+ "value" => "another_value",
+ "db" => [":keyaa1"]
+ },
+ %{
+ "group" => ":pleroma",
+ "key" => config2.key,
+ "value" => "another_value",
+ "db" => [":keyaa2"]
}
]
}
assert Application.get_env(:pleroma, :keyaa1) == "another_value"
- refute Application.get_env(:pleroma, :keyaa2)
+ assert Application.get_env(:pleroma, :keyaa2) == "another_value"
+ assert Application.get_env(:ueberauth, Ueberauth) == ConfigDB.from_binary(config3.value)
+
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> assign(:token, token)
+ |> post("/api/pleroma/admin/config", %{
+ configs: [
+ %{group: config2.group, key: config2.key, delete: true},
+ %{
+ group: ":ueberauth",
+ key: "Ueberauth",
+ delete: true
+ }
+ ]
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => []
+ }
+
+ 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
+ adapter = Application.get_env(:tesla, :adapter)
+ on_exit(fn -> Application.put_env(:tesla, :adapter, adapter) end)
+
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
- "group" => "pleroma",
+ "group" => ":pleroma",
"key" => "Pleroma.Captcha.NotReal",
"value" => [
%{"tuple" => [":enabled", false]},
@@ -2087,16 +2604,25 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
%{"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" => [":regex4", "~r/https:\/\/example.com/s"]},
+ %{"tuple" => [":name", "Pleroma"]}
]
+ },
+ %{
+ "group" => ":tesla",
+ "key" => ":adapter",
+ "value" => "Tesla.Adapter.Httpc"
}
]
})
+ assert Application.get_env(:tesla, :adapter) == Tesla.Adapter.Httpc
+ assert Config.get([Pleroma.Captcha.NotReal, :name]) == "Pleroma"
+
assert json_response(conn, 200) == %{
"configs" => [
%{
- "group" => "pleroma",
+ "group" => ":pleroma",
"key" => "Pleroma.Captcha.NotReal",
"value" => [
%{"tuple" => [":enabled", false]},
@@ -2108,8 +2634,28 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
%{"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" => [":regex4", "~r/https:\\/\\/example.com/s"]},
+ %{"tuple" => [":name", "Pleroma"]}
+ ],
+ "db" => [
+ ":enabled",
+ ":method",
+ ":seconds_valid",
+ ":path",
+ ":key1",
+ ":partial_chain",
+ ":regex1",
+ ":regex2",
+ ":regex3",
+ ":regex4",
+ ":name"
]
+ },
+ %{
+ "group" => ":tesla",
+ "key" => ":adapter",
+ "value" => "Tesla.Adapter.Httpc",
+ "db" => [":adapter"]
}
]
}
@@ -2120,7 +2666,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
- "group" => "pleroma",
+ "group" => ":pleroma",
"key" => "Pleroma.Web.Endpoint.NotReal",
"value" => [
%{
@@ -2184,7 +2730,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert json_response(conn, 200) == %{
"configs" => [
%{
- "group" => "pleroma",
+ "group" => ":pleroma",
"key" => "Pleroma.Web.Endpoint.NotReal",
"value" => [
%{
@@ -2240,7 +2786,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
]
]
}
- ]
+ ],
+ "db" => [":http"]
}
]
}
@@ -2251,7 +2798,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
- "group" => "pleroma",
+ "group" => ":pleroma",
"key" => ":key1",
"value" => [
%{"tuple" => [":key2", "some_val"]},
@@ -2281,7 +2828,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
%{
"configs" => [
%{
- "group" => "pleroma",
+ "group" => ":pleroma",
"key" => ":key1",
"value" => [
%{"tuple" => [":key2", "some_val"]},
@@ -2302,7 +2849,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
}
]
}
- ]
+ ],
+ "db" => [":key2", ":key3"]
}
]
}
@@ -2313,7 +2861,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
- "group" => "pleroma",
+ "group" => ":pleroma",
"key" => ":key1",
"value" => %{"key" => "some_val"}
}
@@ -2324,83 +2872,21 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
%{
"configs" => [
%{
- "group" => "pleroma",
+ "group" => ":pleroma",
"key" => ":key1",
- "value" => %{"key" => "some_val"}
+ "value" => %{"key" => "some_val"},
+ "db" => [":key1"]
}
]
}
end
- test "dispatch setting", %{conn: conn} do
- conn =
- post(conn, "/api/pleroma/admin/config", %{
- configs: [
- %{
- "group" => "pleroma",
- "key" => "Pleroma.Web.Endpoint.NotReal",
- "value" => [
- %{
- "tuple" => [
- ":http",
- [
- %{"tuple" => [":ip", %{"tuple" => [127, 0, 0, 1]}]},
- %{"tuple" => [":dispatch", ["{:_,
- [
- {\"/api/v1/streaming\", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
- {\"/websocket\", Phoenix.Endpoint.CowboyWebSocket,
- {Phoenix.Transports.WebSocket,
- {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: \"/websocket\"]}}},
- {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
- ]}"]]}
- ]
- ]
- }
- ]
- }
- ]
- })
-
- dispatch_string =
- "{:_, [{\"/api/v1/streaming\", Pleroma.Web.MastodonAPI.WebsocketHandler, []}, " <>
- "{\"/websocket\", Phoenix.Endpoint.CowboyWebSocket, {Phoenix.Transports.WebSocket, " <>
- "{Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: \"/websocket\"]}}}, " <>
- "{:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}]}"
-
- assert json_response(conn, 200) == %{
- "configs" => [
- %{
- "group" => "pleroma",
- "key" => "Pleroma.Web.Endpoint.NotReal",
- "value" => [
- %{
- "tuple" => [
- ":http",
- [
- %{"tuple" => [":ip", %{"tuple" => [127, 0, 0, 1]}]},
- %{
- "tuple" => [
- ":dispatch",
- [
- dispatch_string
- ]
- ]
- }
- ]
- ]
- }
- ]
- }
- ]
- }
- end
-
test "queues key as atom", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
- "group" => "oban",
+ "group" => ":oban",
"key" => ":queues",
"value" => [
%{"tuple" => [":federator_incoming", 50]},
@@ -2418,7 +2904,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert json_response(conn, 200) == %{
"configs" => [
%{
- "group" => "oban",
+ "group" => ":oban",
"key" => ":queues",
"value" => [
%{"tuple" => [":federator_incoming", 50]},
@@ -2428,6 +2914,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
%{"tuple" => [":transmogrifier", 20]},
%{"tuple" => [":scheduled_activities", 10]},
%{"tuple" => [":background", 5]}
+ ],
+ "db" => [
+ ":federator_incoming",
+ ":federator_outgoing",
+ ":web_push",
+ ":mailer",
+ ":transmogrifier",
+ ":scheduled_activities",
+ ":background"
]
}
]
@@ -2437,7 +2932,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
test "delete part of settings by atom subkeys", %{conn: conn} do
config =
insert(:config,
- key: "keyaa1",
+ key: ":keyaa1",
value: :erlang.term_to_binary(subkey1: "val1", subkey2: "val2", subkey3: "val3")
)
@@ -2448,68 +2943,180 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
group: config.group,
key: config.key,
subkeys: [":subkey1", ":subkey3"],
- delete: "true"
+ delete: true
}
]
})
- assert(
- json_response(conn, 200) == %{
- "configs" => [
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":pleroma",
+ "key" => ":keyaa1",
+ "value" => [%{"tuple" => [":subkey2", "val2"]}],
+ "db" => [":subkey2"]
+ }
+ ]
+ }
+ end
+
+ test "proxy tuple localhost", %{conn: conn} do
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ configs: [
+ %{
+ group: ":pleroma",
+ key: ":http",
+ value: [
+ %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]},
+ %{"tuple" => [":send_user_agent", false]}
+ ]
+ }
+ ]
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":pleroma",
+ "key" => ":http",
+ "value" => [
+ %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]},
+ %{"tuple" => [":send_user_agent", false]}
+ ],
+ "db" => [":proxy_url", ":send_user_agent"]
+ }
+ ]
+ }
+ end
+
+ test "proxy tuple domain", %{conn: conn} do
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ configs: [
%{
- "group" => "pleroma",
- "key" => "keyaa1",
- "value" => [%{"tuple" => [":subkey2", "val2"]}]
+ group: ":pleroma",
+ key: ":http",
+ value: [
+ %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]},
+ %{"tuple" => [":send_user_agent", false]}
+ ]
}
]
- }
- )
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":pleroma",
+ "key" => ":http",
+ "value" => [
+ %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]},
+ %{"tuple" => [":send_user_agent", false]}
+ ],
+ "db" => [":proxy_url", ":send_user_agent"]
+ }
+ ]
+ }
end
- end
- describe "config mix tasks run" do
- setup %{conn: conn} do
- admin = insert(:user, is_admin: true)
+ test "proxy tuple ip", %{conn: conn} do
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ configs: [
+ %{
+ group: ":pleroma",
+ key: ":http",
+ value: [
+ %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]},
+ %{"tuple" => [":send_user_agent", false]}
+ ]
+ }
+ ]
+ })
- temp_file = "config/test.exported_from_db.secret.exs"
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":pleroma",
+ "key" => ":http",
+ "value" => [
+ %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]},
+ %{"tuple" => [":send_user_agent", false]}
+ ],
+ "db" => [":proxy_url", ":send_user_agent"]
+ }
+ ]
+ }
+ end
+ end
- Mix.shell(Mix.Shell.Quiet)
+ describe "GET /api/pleroma/admin/restart" do
+ clear_config(:configurable_from_database) do
+ Config.put(:configurable_from_database, true)
+ end
- on_exit(fn ->
- Mix.shell(Mix.Shell.IO)
- :ok = File.rm(temp_file)
- end)
+ test "pleroma restarts", %{conn: conn} do
+ capture_log(fn ->
+ assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{}
+ end) =~ "pleroma restarted"
- %{conn: assign(conn, :user, admin), admin: admin}
+ refute Restarter.Pleroma.need_reboot?()
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(200)
- clear_config([:instance, :dynamic_configuration]) do
- Pleroma.Config.put([:instance, :dynamic_configuration], true)
+ refute "private" in Enum.map(response, & &1["visibility"])
+ assert length(response) == 3
end
- clear_config([:feed, :post_title]) do
- Pleroma.Config.put([:feed, :post_title], %{max_length: 100, omission: "…"})
+ 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(200)
+
+ assert length(response) == 1
end
- test "transfer settings to DB and to file", %{conn: conn, admin: admin} do
- assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == []
- conn = get(conn, "/api/pleroma/admin/config/migrate_to_db")
- assert json_response(conn, 200) == %{}
- assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) > 0
+ test "returns private and direct statuses with godmode on", %{conn: conn, admin: admin} do
+ user = insert(:user)
- conn =
- build_conn()
- |> assign(:user, admin)
- |> get("/api/pleroma/admin/config/migrate_from_db")
+ {:ok, _} =
+ CommonAPI.post(user, %{"status" => "@#{admin.nickname}", "visibility" => "direct"})
- assert json_response(conn, 200) == %{}
- assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == []
+ {: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(conn, 200) |> length() == 3
end
end
describe "GET /api/pleroma/admin/users/:nickname/statuses" do
setup do
- admin = insert(:user, is_admin: true)
user = insert(:user)
date1 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!()
@@ -2520,11 +3127,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
insert(:note_activity, user: user, published: date2)
insert(:note_activity, user: user, published: date3)
- conn =
- build_conn()
- |> assign(:user, admin)
-
- {:ok, conn: conn, user: user}
+ %{user: user}
end
test "renders user's statuses", %{conn: conn, user: user} do
@@ -2562,14 +3165,27 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
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/moderation_log" do
- setup %{conn: conn} do
- admin = insert(:user, is_admin: true)
+ setup do
moderator = insert(:user, is_moderator: true)
- %{conn: assign(conn, :user, admin), admin: admin, moderator: moderator}
+ %{moderator: moderator}
end
test "returns the log", %{conn: conn, admin: admin} do
@@ -2774,20 +3390,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "PATCH /users/:nickname/force_password_reset" do
- setup %{conn: conn} do
- admin = insert(:user, is_admin: true)
+ test "sets password_reset_pending to true", %{conn: conn} do
user = insert(:user)
-
- %{conn: assign(conn, :user, admin), admin: admin, user: user}
- end
-
- test "sets password_reset_pending to true", %{admin: admin, user: user} do
assert user.password_reset_pending == false
conn =
- build_conn()
- |> assign(:user, admin)
- |> patch("/api/pleroma/admin/users/force_password_reset", %{nicknames: [user.nickname]})
+ patch(conn, "/api/pleroma/admin/users/force_password_reset", %{nicknames: [user.nickname]})
assert json_response(conn, 204) == ""
@@ -2798,17 +3406,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "relays" do
- setup %{conn: conn} do
- admin = insert(:user, is_admin: true)
-
- %{conn: assign(conn, :user, admin), admin: admin}
- end
-
- test "POST /relay", %{admin: admin} do
+ test "POST /relay", %{conn: conn, admin: admin} do
conn =
- build_conn()
- |> assign(:user, admin)
- |> post("/api/pleroma/admin/relay", %{
+ post(conn, "/api/pleroma/admin/relay", %{
relay_url: "http://mastodon.example.org/users/admin"
})
@@ -2820,7 +3420,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin"
end
- test "GET /relay", %{admin: admin} do
+ 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"]
@@ -2829,25 +3429,18 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
User.follow(relay_user, user)
end)
- conn =
- build_conn()
- |> assign(:user, admin)
- |> get("/api/pleroma/admin/relay")
+ conn = get(conn, "/api/pleroma/admin/relay")
assert json_response(conn, 200)["relays"] -- ["mastodon.example.org", "mstdn.io"] == []
end
- test "DELETE /relay", %{admin: admin} do
- build_conn()
- |> assign(:user, admin)
- |> post("/api/pleroma/admin/relay", %{
+ test "DELETE /relay", %{conn: conn, admin: admin} do
+ post(conn, "/api/pleroma/admin/relay", %{
relay_url: "http://mastodon.example.org/users/admin"
})
conn =
- build_conn()
- |> assign(:user, admin)
- |> delete("/api/pleroma/admin/relay", %{
+ delete(conn, "/api/pleroma/admin/relay", %{
relay_url: "http://mastodon.example.org/users/admin"
})
@@ -2864,63 +3457,58 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "instances" do
- test "GET /instances/:instance/statuses" do
- admin = insert(:user, is_admin: true)
+ 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)
- insert(:note_activity, user: user2)
+ activity = insert(:note_activity, user: user2)
- conn =
- build_conn()
- |> assign(:user, admin)
- |> get("/api/pleroma/admin/instances/archae.me/statuses")
+ ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses")
- response = json_response(conn, 200)
+ response = json_response(ret_conn, 200)
assert length(response) == 2
- conn =
- build_conn()
- |> assign(:user, admin)
- |> get("/api/pleroma/admin/instances/test.com/statuses")
+ ret_conn = get(conn, "/api/pleroma/admin/instances/test.com/statuses")
- response = json_response(conn, 200)
+ response = json_response(ret_conn, 200)
assert length(response) == 1
- conn =
- build_conn()
- |> assign(:user, admin)
- |> get("/api/pleroma/admin/instances/nonexistent.com/statuses")
+ ret_conn = get(conn, "/api/pleroma/admin/instances/nonexistent.com/statuses")
- response = json_response(conn, 200)
+ response = json_response(ret_conn, 200)
- assert length(response) == 0
- end
- end
+ assert Enum.empty?(response)
- describe "PATCH /confirm_email" do
- setup %{conn: conn} do
- admin = insert(:user, is_admin: true)
+ 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
- %{conn: assign(conn, :user, admin), admin: admin}
+ 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
- test "it confirms emails of two users", %{admin: admin} do
+ 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
- build_conn()
- |> assign(:user, admin)
- |> patch("/api/pleroma/admin/users/confirm_email", %{
- nicknames: [
- first_user.nickname,
- second_user.nickname
- ]
- })
+ 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
@@ -2935,23 +3523,18 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "PATCH /resend_confirmation_email" do
- setup %{conn: conn} do
- admin = insert(:user, is_admin: true)
-
- %{conn: assign(conn, :user, admin), admin: admin}
- end
-
- test "it resend emails for two users", %{admin: admin} do
+ test "it resend emails for two users", %{conn: conn, admin: admin} do
[first_user, second_user] = insert_pair(:user, confirmation_pending: true)
- build_conn()
- |> assign(:user, admin)
- |> patch("/api/pleroma/admin/users/resend_confirmation_email", %{
- nicknames: [
- first_user.nickname,
- second_user.nickname
- ]
- })
+ 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)
@@ -2961,6 +3544,100 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
}"
end
end
+
+ describe "POST /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]
+ })
+
+ post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{
+ content: "this is disgusting!"
+ })
+
+ post(conn, "/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
+ [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(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
+
+ [note, _] = Repo.all(ReportNote)
+
+ delete(conn, "/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}")
+
+ assert ReportNote |> Repo.all() |> length() == 1
+ end
+ end
+
+ test "GET /api/pleroma/admin/config/descriptions", %{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(conn, 200)
+
+ assert child["children"]
+ assert child["key"]
+ assert String.starts_with?(child["group"], ":")
+ assert child["description"]
+ 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
+ end
end
# Needed for testing
diff --git a/test/web/admin_api/config_test.exs b/test/web/admin_api/config_test.exs
deleted file mode 100644
index 204446b79..000000000
--- a/test/web/admin_api/config_test.exs
+++ /dev/null
@@ -1,497 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.AdminAPI.ConfigTest do
- use Pleroma.DataCase, async: true
- import Pleroma.Factory
- alias Pleroma.Web.AdminAPI.Config
-
- test "get_by_key/1" do
- config = insert(:config)
- insert(:config)
-
- assert config == Config.get_by_params(%{group: config.group, key: config.key})
- end
-
- test "create/1" do
- {:ok, config} = Config.create(%{group: "pleroma", key: "some_key", value: "some_value"})
- assert config == Config.get_by_params(%{group: "pleroma", key: "some_key"})
- end
-
- test "update/1" do
- config = insert(:config)
- {:ok, updated} = Config.update(config, %{value: "some_value"})
- loaded = Config.get_by_params(%{group: config.group, key: config.key})
- assert loaded == updated
- end
-
- test "update_or_create/1" do
- config = insert(:config)
- key2 = "another_key"
-
- params = [
- %{group: "pleroma", key: key2, value: "another_value"},
- %{group: config.group, key: config.key, value: "new_value"}
- ]
-
- assert Repo.all(Config) |> length() == 1
-
- Enum.each(params, &Config.update_or_create(&1))
-
- assert Repo.all(Config) |> length() == 2
-
- config1 = Config.get_by_params(%{group: config.group, key: config.key})
- config2 = Config.get_by_params(%{group: "pleroma", key: key2})
-
- assert config1.value == Config.transform("new_value")
- assert config2.value == Config.transform("another_value")
- end
-
- test "delete/1" do
- config = insert(:config)
- {:ok, _} = Config.delete(%{key: config.key, group: config.group})
- refute Config.get_by_params(%{key: config.key, group: config.group})
- end
-
- describe "transform/1" do
- test "string" do
- binary = Config.transform("value as string")
- assert binary == :erlang.term_to_binary("value as string")
- assert Config.from_binary(binary) == "value as string"
- end
-
- test "boolean" do
- binary = Config.transform(false)
- assert binary == :erlang.term_to_binary(false)
- assert Config.from_binary(binary) == false
- end
-
- test "nil" do
- binary = Config.transform(nil)
- assert binary == :erlang.term_to_binary(nil)
- assert Config.from_binary(binary) == nil
- end
-
- test "integer" do
- binary = Config.transform(150)
- assert binary == :erlang.term_to_binary(150)
- assert Config.from_binary(binary) == 150
- end
-
- test "atom" do
- binary = Config.transform(":atom")
- assert binary == :erlang.term_to_binary(:atom)
- assert Config.from_binary(binary) == :atom
- end
-
- test "pleroma module" do
- binary = Config.transform("Pleroma.Bookmark")
- assert binary == :erlang.term_to_binary(Pleroma.Bookmark)
- assert Config.from_binary(binary) == Pleroma.Bookmark
- end
-
- test "phoenix module" do
- binary = Config.transform("Phoenix.Socket.V1.JSONSerializer")
- assert binary == :erlang.term_to_binary(Phoenix.Socket.V1.JSONSerializer)
- assert Config.from_binary(binary) == Phoenix.Socket.V1.JSONSerializer
- end
-
- test "sigil" do
- binary = Config.transform("~r/comp[lL][aA][iI][nN]er/")
- assert binary == :erlang.term_to_binary(~r/comp[lL][aA][iI][nN]er/)
- assert Config.from_binary(binary) == ~r/comp[lL][aA][iI][nN]er/
- end
-
- test "link sigil" do
- binary = Config.transform("~r/https:\/\/example.com/")
- assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/)
- assert Config.from_binary(binary) == ~r/https:\/\/example.com/
- end
-
- test "link sigil with u modifier" do
- binary = Config.transform("~r/https:\/\/example.com/u")
- assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/u)
- assert Config.from_binary(binary) == ~r/https:\/\/example.com/u
- end
-
- test "link sigil with i modifier" do
- binary = Config.transform("~r/https:\/\/example.com/i")
- assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/i)
- assert Config.from_binary(binary) == ~r/https:\/\/example.com/i
- end
-
- test "link sigil with s modifier" do
- binary = Config.transform("~r/https:\/\/example.com/s")
- assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/s)
- assert Config.from_binary(binary) == ~r/https:\/\/example.com/s
- end
-
- test "2 child tuple" do
- binary = Config.transform(%{"tuple" => ["v1", ":v2"]})
- assert binary == :erlang.term_to_binary({"v1", :v2})
- assert Config.from_binary(binary) == {"v1", :v2}
- end
-
- test "tuple with n childs" do
- binary =
- Config.transform(%{
- "tuple" => [
- "v1",
- ":v2",
- "Pleroma.Bookmark",
- 150,
- false,
- "Phoenix.Socket.V1.JSONSerializer"
- ]
- })
-
- assert binary ==
- :erlang.term_to_binary(
- {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer}
- )
-
- assert Config.from_binary(binary) ==
- {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer}
- end
-
- test "tuple with dispatch key" do
- binary = Config.transform(%{"tuple" => [":dispatch", ["{:_,
- [
- {\"/api/v1/streaming\", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
- {\"/websocket\", Phoenix.Endpoint.CowboyWebSocket,
- {Phoenix.Transports.WebSocket,
- {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: \"/websocket\"]}}},
- {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
- ]}"]]})
-
- assert binary ==
- :erlang.term_to_binary(
- {:dispatch,
- [
- {:_,
- [
- {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
- {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
- {Phoenix.Transports.WebSocket,
- {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: "/websocket"]}}},
- {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
- ]}
- ]}
- )
-
- assert Config.from_binary(binary) ==
- {:dispatch,
- [
- {:_,
- [
- {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
- {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
- {Phoenix.Transports.WebSocket,
- {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: "/websocket"]}}},
- {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
- ]}
- ]}
- end
-
- test "map with string key" do
- binary = Config.transform(%{"key" => "value"})
- assert binary == :erlang.term_to_binary(%{"key" => "value"})
- assert Config.from_binary(binary) == %{"key" => "value"}
- end
-
- test "map with atom key" do
- binary = Config.transform(%{":key" => "value"})
- assert binary == :erlang.term_to_binary(%{key: "value"})
- assert Config.from_binary(binary) == %{key: "value"}
- end
-
- test "list of strings" do
- binary = Config.transform(["v1", "v2", "v3"])
- assert binary == :erlang.term_to_binary(["v1", "v2", "v3"])
- assert Config.from_binary(binary) == ["v1", "v2", "v3"]
- end
-
- test "list of modules" do
- binary = Config.transform(["Pleroma.Repo", "Pleroma.Activity"])
- assert binary == :erlang.term_to_binary([Pleroma.Repo, Pleroma.Activity])
- assert Config.from_binary(binary) == [Pleroma.Repo, Pleroma.Activity]
- end
-
- test "list of atoms" do
- binary = Config.transform([":v1", ":v2", ":v3"])
- assert binary == :erlang.term_to_binary([:v1, :v2, :v3])
- assert Config.from_binary(binary) == [:v1, :v2, :v3]
- end
-
- test "list of mixed values" do
- binary =
- Config.transform([
- "v1",
- ":v2",
- "Pleroma.Repo",
- "Phoenix.Socket.V1.JSONSerializer",
- 15,
- false
- ])
-
- assert binary ==
- :erlang.term_to_binary([
- "v1",
- :v2,
- Pleroma.Repo,
- Phoenix.Socket.V1.JSONSerializer,
- 15,
- false
- ])
-
- assert Config.from_binary(binary) == [
- "v1",
- :v2,
- Pleroma.Repo,
- Phoenix.Socket.V1.JSONSerializer,
- 15,
- false
- ]
- end
-
- test "simple keyword" do
- binary = Config.transform([%{"tuple" => [":key", "value"]}])
- assert binary == :erlang.term_to_binary([{:key, "value"}])
- assert Config.from_binary(binary) == [{:key, "value"}]
- assert Config.from_binary(binary) == [key: "value"]
- end
-
- test "keyword with partial_chain key" do
- binary =
- Config.transform([%{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}])
-
- assert binary == :erlang.term_to_binary(partial_chain: &:hackney_connect.partial_chain/1)
- assert Config.from_binary(binary) == [partial_chain: &:hackney_connect.partial_chain/1]
- end
-
- test "keyword" do
- binary =
- Config.transform([
- %{"tuple" => [":types", "Pleroma.PostgresTypes"]},
- %{"tuple" => [":telemetry_event", ["Pleroma.Repo.Instrumenter"]]},
- %{"tuple" => [":migration_lock", nil]},
- %{"tuple" => [":key1", 150]},
- %{"tuple" => [":key2", "string"]}
- ])
-
- assert binary ==
- :erlang.term_to_binary(
- types: Pleroma.PostgresTypes,
- telemetry_event: [Pleroma.Repo.Instrumenter],
- migration_lock: nil,
- key1: 150,
- key2: "string"
- )
-
- assert Config.from_binary(binary) == [
- types: Pleroma.PostgresTypes,
- telemetry_event: [Pleroma.Repo.Instrumenter],
- migration_lock: nil,
- key1: 150,
- key2: "string"
- ]
- end
-
- test "complex keyword with nested mixed childs" do
- binary =
- Config.transform([
- %{"tuple" => [":uploader", "Pleroma.Uploaders.Local"]},
- %{"tuple" => [":filters", ["Pleroma.Upload.Filter.Dedupe"]]},
- %{"tuple" => [":link_name", true]},
- %{"tuple" => [":proxy_remote", false]},
- %{"tuple" => [":common_map", %{":key" => "value"}]},
- %{
- "tuple" => [
- ":proxy_opts",
- [
- %{"tuple" => [":redirect_on_failure", false]},
- %{"tuple" => [":max_body_length", 1_048_576]},
- %{
- "tuple" => [
- ":http",
- [%{"tuple" => [":follow_redirect", true]}, %{"tuple" => [":pool", ":upload"]}]
- ]
- }
- ]
- ]
- }
- ])
-
- assert binary ==
- :erlang.term_to_binary(
- uploader: Pleroma.Uploaders.Local,
- filters: [Pleroma.Upload.Filter.Dedupe],
- link_name: true,
- proxy_remote: false,
- common_map: %{key: "value"},
- proxy_opts: [
- redirect_on_failure: false,
- max_body_length: 1_048_576,
- http: [
- follow_redirect: true,
- pool: :upload
- ]
- ]
- )
-
- assert Config.from_binary(binary) ==
- [
- uploader: Pleroma.Uploaders.Local,
- filters: [Pleroma.Upload.Filter.Dedupe],
- link_name: true,
- proxy_remote: false,
- common_map: %{key: "value"},
- proxy_opts: [
- redirect_on_failure: false,
- max_body_length: 1_048_576,
- http: [
- follow_redirect: true,
- pool: :upload
- ]
- ]
- ]
- end
-
- test "common keyword" do
- binary =
- Config.transform([
- %{"tuple" => [":level", ":warn"]},
- %{"tuple" => [":meta", [":all"]]},
- %{"tuple" => [":path", ""]},
- %{"tuple" => [":val", nil]},
- %{"tuple" => [":webhook_url", "https://hooks.slack.com/services/YOUR-KEY-HERE"]}
- ])
-
- assert binary ==
- :erlang.term_to_binary(
- level: :warn,
- meta: [:all],
- path: "",
- val: nil,
- webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
- )
-
- assert Config.from_binary(binary) == [
- level: :warn,
- meta: [:all],
- path: "",
- val: nil,
- webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
- ]
- end
-
- test "complex keyword with sigil" do
- binary =
- Config.transform([
- %{"tuple" => [":federated_timeline_removal", []]},
- %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]},
- %{"tuple" => [":replace", []]}
- ])
-
- assert binary ==
- :erlang.term_to_binary(
- federated_timeline_removal: [],
- reject: [~r/comp[lL][aA][iI][nN]er/],
- replace: []
- )
-
- assert Config.from_binary(binary) ==
- [federated_timeline_removal: [], reject: [~r/comp[lL][aA][iI][nN]er/], replace: []]
- end
-
- test "complex keyword with tuples with more than 2 values" do
- binary =
- Config.transform([
- %{
- "tuple" => [
- ":http",
- [
- %{
- "tuple" => [
- ":key1",
- [
- %{
- "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 binary ==
- :erlang.term_to_binary(
- http: [
- key1: [
- _: [
- {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
- {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
- {Phoenix.Transports.WebSocket,
- {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, []}}},
- {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
- ]
- ]
- ]
- )
-
- assert Config.from_binary(binary) == [
- http: [
- key1: [
- {:_,
- [
- {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
- {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
- {Phoenix.Transports.WebSocket,
- {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, []}}},
- {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
- ]}
- ]
- ]
- ]
- end
- end
-end
diff --git a/test/web/admin_api/search_test.exs b/test/web/admin_api/search_test.exs
index 082e691c4..e0e3d4153 100644
--- a/test/web/admin_api/search_test.exs
+++ b/test/web/admin_api/search_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.SearchTest do
diff --git a/test/web/admin_api/views/report_view_test.exs b/test/web/admin_api/views/report_view_test.exs
index ef4a806e4..5db6629f2 100644
--- a/test/web/admin_api/views/report_view_test.exs
+++ b/test/web/admin_api/views/report_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ReportViewTest do
@@ -30,6 +30,7 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: other_user})
),
statuses: [],
+ notes: [],
state: "open",
id: activity.id
}
@@ -65,6 +66,7 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
),
statuses: [StatusView.render("show.json", %{activity: activity})],
state: "open",
+ notes: [],
id: report_activity.id
}
diff --git a/test/web/auth/authenticator_test.exs b/test/web/auth/authenticator_test.exs
index fea5c8209..d54253343 100644
--- a/test/web/auth/authenticator_test.exs
+++ b/test/web/auth/authenticator_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.AuthenticatorTest do
diff --git a/test/web/chat_channel_test.exs b/test/web/chat_channel_test.exs
new file mode 100644
index 000000000..68c24a9f9
--- /dev/null
+++ b/test/web/chat_channel_test.exs
@@ -0,0 +1,37 @@
+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
+ 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
index d641f7478..c2ed1c789 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPITest do
@@ -14,7 +14,6 @@ defmodule Pleroma.Web.CommonAPITest do
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
- import ExUnit.CaptureLog
require Pleroma.Constants
@@ -69,6 +68,7 @@ defmodule Pleroma.Web.CommonAPITest do
har = insert(:user)
jafnhar = insert(:user)
tridi = insert(:user)
+
Pleroma.Config.put([:instance, :safe_dm_mentions], true)
{:ok, activity} =
@@ -202,13 +202,15 @@ defmodule Pleroma.Web.CommonAPITest do
CommonAPI.post(user, %{"status" => ""})
end
- test "it returns error when character limit is exceeded" do
+ 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
@@ -239,7 +241,9 @@ defmodule Pleroma.Web.CommonAPITest do
assert reaction.data["actor"] == user.ap_id
assert reaction.data["content"] == "👍"
- # TODO: test error case.
+ {: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
@@ -288,25 +292,22 @@ defmodule Pleroma.Web.CommonAPITest do
assert data["object"] == post_activity.data["object"]
end
- test "retweeting a status twice returns an error" do
+ 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{}, _object} = CommonAPI.repeat(activity.id, user)
- {:error, _} = CommonAPI.repeat(activity.id, user)
+ {:ok, %Activity{} = activity, object} = CommonAPI.repeat(activity.id, user)
+ {:ok, ^activity, ^object} = CommonAPI.repeat(activity.id, user)
end
- test "favoriting a status twice returns an error" do
+ 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 capture_log(fn ->
- assert {:error, _} = CommonAPI.favorite(user, activity.id)
- end) =~ "[error]"
+ assert {:ok, :already_liked} = CommonAPI.favorite(user, activity.id)
end
end
@@ -329,6 +330,21 @@ defmodule Pleroma.Web.CommonAPITest do
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)
@@ -516,14 +532,14 @@ defmodule Pleroma.Web.CommonAPITest do
end
test "add a reblog mute", %{muter: muter, muted: muted} do
- {:ok, muter} = CommonAPI.hide_reblogs(muter, muted)
+ {: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, muter} = CommonAPI.hide_reblogs(muter, muted)
- {:ok, muter} = CommonAPI.show_reblogs(muter, muted)
+ {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted)
+ {:ok, _reblog_mute} = CommonAPI.show_reblogs(muter, muted)
assert User.showing_reblogs?(muter, muted) == true
end
@@ -533,7 +549,7 @@ defmodule Pleroma.Web.CommonAPITest do
test "also unsubscribes a user" do
[follower, followed] = insert_pair(:user)
{:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
- {:ok, followed} = User.subscribe(follower, followed)
+ {:ok, _subscription} = User.subscribe(follower, followed)
assert User.subscribed_to?(follower, followed)
@@ -541,6 +557,50 @@ defmodule Pleroma.Web.CommonAPITest do
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) == "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) == "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
diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs
index 2588898d0..45fc94522 100644
--- a/test/web/common_api/common_api_utils_test.exs
+++ b/test/web/common_api/common_api_utils_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI.UtilsTest do
@@ -89,8 +89,8 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
assert output == expected
- text = "<p>hello world!</p>\n\n<p>second paragraph</p>"
- expected = "<p>hello world!</p>\n\n<p>second paragraph</p>"
+ 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")
@@ -99,14 +99,14 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
test "works for bare text/markdown" do
text = "**hello world**"
- expected = "<p><strong>hello world</strong></p>\n"
+ 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>\n<p><em>another paragraph</em></p>\n"
+ expected = "<p><strong>hello world</strong></p><p><em>another paragraph</em></p>"
{output, [], []} = Utils.format_input(text, "text/markdown")
@@ -118,7 +118,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
by someone
"""
- expected = "<blockquote><p>cool quote</p>\n</blockquote>\n<p>by someone</p>\n"
+ expected = "<blockquote><p>cool quote</p></blockquote><p>by someone</p>"
{output, [], []} = Utils.format_input(text, "text/markdown")
@@ -134,7 +134,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
assert output == expected
text = "[b]hello world![/b]\n\nsecond paragraph!"
- expected = "<strong>hello world!</strong><br>\n<br>\nsecond paragraph!"
+ expected = "<strong>hello world!</strong><br><br>second paragraph!"
{output, [], []} = Utils.format_input(text, "text/bbcode")
@@ -143,7 +143,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
text = "[b]hello world![/b]\n\n<strong>second paragraph!</strong>"
expected =
- "<strong>hello world!</strong><br>\n<br>\n&lt;strong&gt;second paragraph!&lt;/strong&gt;"
+ "<strong>hello world!</strong><br><br>&lt;strong&gt;second paragraph!&lt;/strong&gt;"
{output, [], []} = Utils.format_input(text, "text/bbcode")
@@ -156,16 +156,14 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
text = "**hello world**\n\n*another @user__test and @user__test google.com paragraph*"
- expected =
- ~s(<p><strong>hello world</strong></p>\n<p><em>another <span class="h-card"><a data-user="#{
- user.id
- }" class="u-url mention" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> and <span class="h-card"><a data-user="#{
- user.id
- }" class="u-url mention" 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>\n)
-
{output, _, _} = Utils.format_input(text, "text/markdown")
- assert output == expected
+ assert output ==
+ ~s(<p><strong>hello world</strong></p><p><em>another <span class="h-card"><a data-user="#{
+ user.id
+ }" class="u-url mention" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> and <span class="h-card"><a data-user="#{
+ user.id
+ }" class="u-url mention" 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
@@ -307,7 +305,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
{to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private", nil)
assert length(to) == 2
- assert length(cc) == 0
+ assert Enum.empty?(cc)
assert mentioned_user.ap_id in to
assert user.follower_address in to
@@ -323,7 +321,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
{to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private", nil)
assert length(to) == 3
- assert length(cc) == 0
+ assert Enum.empty?(cc)
assert mentioned_user.ap_id in to
assert third_user.ap_id in to
@@ -338,7 +336,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
{to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct", nil)
assert length(to) == 1
- assert length(cc) == 0
+ assert Enum.empty?(cc)
assert mentioned_user.ap_id in to
end
@@ -353,7 +351,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
{to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct", nil)
assert length(to) == 2
- assert length(cc) == 0
+ assert Enum.empty?(cc)
assert mentioned_user.ap_id in to
assert third_user.ap_id in to
@@ -575,11 +573,11 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
end
describe "maybe_add_attachments/3" do
- test "returns parsed results when no_links is true" do
+ test "returns parsed results when attachment_links is false" do
assert Utils.maybe_add_attachments(
{"test", [], ["tags"]},
[],
- true
+ false
) == {"test", [], ["tags"]}
end
@@ -589,7 +587,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
assert Utils.maybe_add_attachments(
{"test", [], ["tags"]},
[attachment],
- false
+ true
) == {
"test<br><a href=\"SakuraPM.png\" class='attachment'>SakuraPM.png</a>",
[],
diff --git a/test/web/fallback_test.exs b/test/web/fallback_test.exs
index c13db9526..3919ef93a 100644
--- a/test/web/fallback_test.exs
+++ b/test/web/fallback_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.FallbackTest do
diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs
index c224197c3..d2ee2267c 100644
--- a/test/web/federator_test.exs
+++ b/test/web/federator_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.FederatorTest do
diff --git a/test/web/feed/feed_controller_test.exs b/test/web/feed/feed_controller_test.exs
deleted file mode 100644
index 6f61acf43..000000000
--- a/test/web/feed/feed_controller_test.exs
+++ /dev/null
@@ -1,251 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Feed.FeedControllerTest do
- use Pleroma.Web.ConnCase
-
- import Pleroma.Factory
- import SweetXml
-
- alias Pleroma.Object
- alias Pleroma.User
-
- clear_config([:feed])
-
- test "gets a 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("content-type", "application/atom+xml")
- |> get("/users/#{user.nickname}/feed.atom")
- |> 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"]
- end
-
- test "returns 404 for a missing feed", %{conn: conn} do
- conn =
- conn
- |> put_req_header("content-type", "application/atom+xml")
- |> get("/users/nonexisting/feed.atom")
-
- assert response(conn, 404)
- end
-
- describe "feed_redirect" do
- test "undefined format. it redirects to feed", %{conn: conn} do
- note_activity = insert(:note_activity)
- user = User.get_cached_by_ap_id(note_activity.data["actor"])
-
- response =
- conn
- |> put_req_header("accept", "application/xml")
- |> get("/users/#{user.nickname}")
- |> response(302)
-
- assert response ==
- "<html><body>You are being <a href=\"#{Pleroma.Web.base_url()}/users/#{
- user.nickname
- }/feed.atom\">redirected</a>.</body></html>"
- end
-
- test "undefined format. it returns error when user not found", %{conn: conn} do
- response =
- conn
- |> put_req_header("accept", "application/xml")
- |> get("/users/jimm")
- |> response(404)
-
- assert response == ~S({"error":"Not found"})
- end
-
- test "activity+json format. it redirects on actual feed of user", %{conn: conn} do
- note_activity = insert(:note_activity)
- user = User.get_cached_by_ap_id(note_activity.data["actor"])
-
- response =
- conn
- |> put_req_header("accept", "application/activity+json")
- |> get("/users/#{user.nickname}")
- |> json_response(200)
-
- assert response["endpoints"] == %{
- "oauthAuthorizationEndpoint" => "#{Pleroma.Web.base_url()}/oauth/authorize",
- "oauthRegistrationEndpoint" => "#{Pleroma.Web.base_url()}/api/v1/apps",
- "oauthTokenEndpoint" => "#{Pleroma.Web.base_url()}/oauth/token",
- "sharedInbox" => "#{Pleroma.Web.base_url()}/inbox",
- "uploadMedia" => "#{Pleroma.Web.base_url()}/api/ap/upload_media"
- }
-
- assert response["@context"] == [
- "https://www.w3.org/ns/activitystreams",
- "http://localhost:4001/schemas/litepub-0.1.jsonld",
- %{"@language" => "und"}
- ]
-
- assert Map.take(response, [
- "followers",
- "following",
- "id",
- "inbox",
- "manuallyApprovesFollowers",
- "name",
- "outbox",
- "preferredUsername",
- "summary",
- "tag",
- "type",
- "url"
- ]) == %{
- "followers" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/followers",
- "following" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/following",
- "id" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}",
- "inbox" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/inbox",
- "manuallyApprovesFollowers" => false,
- "name" => user.name,
- "outbox" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/outbox",
- "preferredUsername" => user.nickname,
- "summary" => user.bio,
- "tag" => [],
- "type" => "Person",
- "url" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}"
- }
- end
-
- test "activity+json format. it returns error whe use not found", %{conn: conn} do
- response =
- conn
- |> put_req_header("accept", "application/activity+json")
- |> get("/users/jimm")
- |> json_response(404)
-
- assert response == "Not found"
- end
-
- test "json format. it redirects on actual feed of user", %{conn: conn} do
- note_activity = insert(:note_activity)
- user = User.get_cached_by_ap_id(note_activity.data["actor"])
-
- response =
- conn
- |> put_req_header("accept", "application/json")
- |> get("/users/#{user.nickname}")
- |> json_response(200)
-
- assert response["endpoints"] == %{
- "oauthAuthorizationEndpoint" => "#{Pleroma.Web.base_url()}/oauth/authorize",
- "oauthRegistrationEndpoint" => "#{Pleroma.Web.base_url()}/api/v1/apps",
- "oauthTokenEndpoint" => "#{Pleroma.Web.base_url()}/oauth/token",
- "sharedInbox" => "#{Pleroma.Web.base_url()}/inbox",
- "uploadMedia" => "#{Pleroma.Web.base_url()}/api/ap/upload_media"
- }
-
- assert response["@context"] == [
- "https://www.w3.org/ns/activitystreams",
- "http://localhost:4001/schemas/litepub-0.1.jsonld",
- %{"@language" => "und"}
- ]
-
- assert Map.take(response, [
- "followers",
- "following",
- "id",
- "inbox",
- "manuallyApprovesFollowers",
- "name",
- "outbox",
- "preferredUsername",
- "summary",
- "tag",
- "type",
- "url"
- ]) == %{
- "followers" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/followers",
- "following" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/following",
- "id" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}",
- "inbox" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/inbox",
- "manuallyApprovesFollowers" => false,
- "name" => user.name,
- "outbox" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/outbox",
- "preferredUsername" => user.nickname,
- "summary" => user.bio,
- "tag" => [],
- "type" => "Person",
- "url" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}"
- }
- end
-
- test "json format. it returns error whe use 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 "html format. it redirects on actual feed of user", %{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 "html format. it returns error when user not found", %{conn: conn} do
- response =
- conn
- |> get("/users/jimm")
- |> json_response(404)
-
- assert response == %{"error" => "Not found"}
- end
- end
-end
diff --git a/test/web/feed/tag_controller_test.exs b/test/web/feed/tag_controller_test.exs
new file mode 100644
index 000000000..5950605e8
--- /dev/null
+++ b/test/web/feed/tag_controller_test.exs
@@ -0,0 +1,154 @@
+# 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.Web.Feed.FeedView
+
+ 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} = Pleroma.Web.CommonAPI.post(user, %{"status" => "yeah #PleromaArt"})
+
+ object = Pleroma.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} =
+ Pleroma.Web.CommonAPI.post(user, %{"status" => "42 This is :moominmamma #PleromaArt"})
+
+ {:ok, _activity3} = Pleroma.Web.CommonAPI.post(user, %{"status" => "This is :moominmamma"})
+
+ response =
+ conn
+ |> put_req_header("content-type", "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]
+ end
+
+ test "gets a feed (RSS)", %{conn: conn} do
+ Pleroma.Config.put(
+ [:feed, :post_title],
+ %{max_length: 25, omission: "..."}
+ )
+
+ user = insert(:user)
+ {:ok, activity1} = Pleroma.Web.CommonAPI.post(user, %{"status" => "yeah #PleromaArt"})
+
+ object = Pleroma.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} =
+ Pleroma.Web.CommonAPI.post(user, %{"status" => "42 This is :moominmamma #PleromaArt"})
+
+ {:ok, _activity3} = Pleroma.Web.CommonAPI.post(user, %{"status" => "This is :moominmamma"})
+
+ response =
+ conn
+ |> put_req_header("content-type", "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(activity1.data["published"]),
+ FeedView.pub_date(activity2.data["published"])
+ ]
+
+ assert xpath(xml, ~x"//channel/item/enclosure/@url"sl) == [
+ "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4"
+ ]
+
+ obj1 = Pleroma.Object.normalize(activity1)
+ obj2 = Pleroma.Object.normalize(activity2)
+
+ assert xpath(xml, ~x"//channel/item/description/text()"sl) == [
+ HtmlEntities.decode(FeedView.activity_content(obj2)),
+ HtmlEntities.decode(FeedView.activity_content(obj1))
+ ]
+
+ response =
+ conn
+ |> put_req_header("content-type", "application/atom+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."
+ end
+end
diff --git a/test/web/feed/user_controller_test.exs b/test/web/feed/user_controller_test.exs
new file mode 100644
index 000000000..00c50f003
--- /dev/null
+++ b/test/web/feed/user_controller_test.exs
@@ -0,0 +1,137 @@
+# 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
+
+ clear_config([:instance, :federating]) do
+ Config.put([:instance, :federating], true)
+ end
+
+ describe "feed" do
+ clear_config([:feed])
+
+ test "gets a 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("content-type", "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"]
+ end
+
+ test "returns 404 for a missing feed", %{conn: conn} do
+ conn =
+ conn
+ |> put_req_header("content-type", "application/atom+xml")
+ |> get(user_feed_path(conn, :feed, "nonexisting"))
+
+ assert response(conn, 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
+end
diff --git a/test/web/instances/instance_test.exs b/test/web/instances/instance_test.exs
index e54d708ad..a3c93b986 100644
--- a/test/web/instances/instance_test.exs
+++ b/test/web/instances/instance_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Instances.InstanceTest do
diff --git a/test/web/instances/instances_test.exs b/test/web/instances/instances_test.exs
index 65b03b155..c5d6abc9c 100644
--- a/test/web/instances/instances_test.exs
+++ b/test/web/instances/instances_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.InstancesTest do
diff --git a/test/web/masto_fe_controller_test.exs b/test/web/masto_fe_controller_test.exs
index b5dbd4a25..9a2d76e0b 100644
--- a/test/web/masto_fe_controller_test.exs
+++ b/test/web/masto_fe_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MastoFEController do
@@ -18,6 +18,7 @@ defmodule Pleroma.Web.MastodonAPI.MastoFEController do
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)
@@ -63,12 +64,12 @@ defmodule Pleroma.Web.MastodonAPI.MastoFEController do
end
test "does not redirect logged in users to the login page", %{conn: conn, path: path} do
- token = insert(:oauth_token)
+ token = insert(:oauth_token, scopes: ["read"])
conn =
conn
|> assign(:user, token.user)
- |> put_session(:oauth_token, token.token)
+ |> assign(:token, token)
|> get(path)
assert conn.status == 200
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
index 77cfce4fa..cba68859e 100644
--- 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,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
@@ -12,13 +12,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
clear_config([:instance, :max_account_fields])
describe "updating credentials" do
- test "sets user settings in a generic way", %{conn: conn} do
- user = insert(:user)
+ setup do: oauth_access(["write:accounts"])
+ test "sets user settings in a generic way", %{conn: conn} do
res_conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{
+ patch(conn, "/api/v1/accounts/update_credentials", %{
"pleroma_settings_store" => %{
pleroma_fe: %{
theme: "bla"
@@ -26,10 +24,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
}
})
- assert user = json_response(res_conn, 200)
- assert user["pleroma"]["settings_store"] == %{"pleroma_fe" => %{"theme" => "bla"}}
+ assert user_data = json_response(res_conn, 200)
+ assert user_data["pleroma"]["settings_store"] == %{"pleroma_fe" => %{"theme" => "bla"}}
- user = Repo.get(User, user["id"])
+ user = Repo.get(User, user_data["id"])
res_conn =
conn
@@ -42,15 +40,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
}
})
- assert user = json_response(res_conn, 200)
+ assert user_data = json_response(res_conn, 200)
- assert user["pleroma"]["settings_store"] ==
+ assert user_data["pleroma"]["settings_store"] ==
%{
"pleroma_fe" => %{"theme" => "bla"},
"masto_fe" => %{"theme" => "bla"}
}
- user = Repo.get(User, user["id"])
+ user = Repo.get(User, user_data["id"])
res_conn =
conn
@@ -63,9 +61,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
}
})
- assert user = json_response(res_conn, 200)
+ assert user_data = json_response(res_conn, 200)
- assert user["pleroma"]["settings_store"] ==
+ assert user_data["pleroma"]["settings_store"] ==
%{
"pleroma_fe" => %{"theme" => "bla"},
"masto_fe" => %{"theme" => "blub"}
@@ -73,97 +71,67 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
end
test "updates the user's bio", %{conn: conn} do
- user = insert(:user)
user2 = insert(:user)
conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{
+ patch(conn, "/api/v1/accounts/update_credentials", %{
"note" => "I drink #cofe with @#{user2.nickname}"
})
- assert user = json_response(conn, 200)
+ assert user_data = json_response(conn, 200)
- assert user["note"] ==
+ 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 data-user="#{
user2.id
}" class="u-url mention" href="#{user2.ap_id}" rel="ugc">@<span>#{user2.nickname}</span></a></span>)
end
test "updates the user's locking status", %{conn: conn} do
- user = insert(:user)
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{locked: "true"})
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{locked: "true"})
-
- assert user = json_response(conn, 200)
- assert user["locked"] == true
+ assert user_data = json_response(conn, 200)
+ assert user_data["locked"] == true
end
- test "updates the user's allow_following_move", %{conn: conn} do
- user = insert(:user)
-
+ test "updates the user's allow_following_move", %{user: user, conn: conn} do
assert user.allow_following_move == true
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{allow_following_move: "false"})
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{allow_following_move: "false"})
assert refresh_record(user).allow_following_move == false
- assert user = json_response(conn, 200)
- assert user["pleroma"]["allow_following_move"] == false
+ assert user_data = json_response(conn, 200)
+ assert user_data["pleroma"]["allow_following_move"] == false
end
test "updates the user's default scope", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{default_scope: "cofe"})
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{default_scope: "cofe"})
- assert user = json_response(conn, 200)
- assert user["source"]["privacy"] == "cofe"
+ assert user_data = json_response(conn, 200)
+ assert user_data["source"]["privacy"] == "cofe"
end
test "updates the user's hide_followers status", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{hide_followers: "true"})
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_followers: "true"})
- assert user = json_response(conn, 200)
- assert user["pleroma"]["hide_followers"] == true
+ assert user_data = json_response(conn, 200)
+ assert user_data["pleroma"]["hide_followers"] == true
end
test "updates the user's hide_followers_count and hide_follows_count", %{conn: conn} do
- user = insert(:user)
-
conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{
+ patch(conn, "/api/v1/accounts/update_credentials", %{
hide_followers_count: "true",
hide_follows_count: "true"
})
- assert user = json_response(conn, 200)
- assert user["pleroma"]["hide_followers_count"] == true
- assert user["pleroma"]["hide_follows_count"] == true
+ assert user_data = json_response(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", %{conn: conn} do
- user = insert(:user)
-
+ test "updates the user's skip_thread_containment option", %{user: user, conn: conn} do
response =
conn
- |> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{skip_thread_containment: "true"})
|> json_response(200)
@@ -172,104 +140,68 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
end
test "updates the user's hide_follows status", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{hide_follows: "true"})
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_follows: "true"})
- assert user = json_response(conn, 200)
- assert user["pleroma"]["hide_follows"] == true
+ assert user_data = json_response(conn, 200)
+ assert user_data["pleroma"]["hide_follows"] == true
end
test "updates the user's hide_favorites status", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{hide_favorites: "true"})
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_favorites: "true"})
- assert user = json_response(conn, 200)
- assert user["pleroma"]["hide_favorites"] == true
+ assert user_data = json_response(conn, 200)
+ assert user_data["pleroma"]["hide_favorites"] == true
end
test "updates the user's show_role status", %{conn: conn} do
- user = insert(:user)
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{show_role: "false"})
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{show_role: "false"})
-
- assert user = json_response(conn, 200)
- assert user["source"]["pleroma"]["show_role"] == false
+ assert user_data = json_response(conn, 200)
+ assert user_data["source"]["pleroma"]["show_role"] == false
end
test "updates the user's no_rich_text status", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{no_rich_text: "true"})
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{no_rich_text: "true"})
- assert user = json_response(conn, 200)
- assert user["source"]["pleroma"]["no_rich_text"] == true
+ assert user_data = json_response(conn, 200)
+ assert user_data["source"]["pleroma"]["no_rich_text"] == true
end
test "updates the user's name", %{conn: conn} do
- user = insert(:user)
-
conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"})
+ patch(conn, "/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"})
- assert user = json_response(conn, 200)
- assert user["display_name"] == "markorepairs"
+ assert user_data = json_response(conn, 200)
+ assert user_data["display_name"] == "markorepairs"
end
- test "updates the user's avatar", %{conn: conn} do
- user = insert(:user)
-
+ 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"
}
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{"avatar" => new_avatar})
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar})
assert user_response = json_response(conn, 200)
assert user_response["avatar"] != User.avatar_url(user)
end
- test "updates the user's banner", %{conn: conn} do
- user = insert(:user)
-
+ 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"
}
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{"header" => new_header})
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header})
assert user_response = json_response(conn, 200)
assert user_response["header"] != User.banner_url(user)
end
test "updates the user's background", %{conn: conn} do
- user = insert(:user)
-
new_header = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
@@ -277,9 +209,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
}
conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{
+ patch(conn, "/api/v1/accounts/update_credentials", %{
"pleroma_background_image" => new_header
})
@@ -287,13 +217,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
assert user_response["pleroma"]["background_image"]
end
- test "requires 'write:accounts' permission", %{conn: conn} do
+ 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 =
- conn
+ build_conn()
|> put_req_header("authorization", "Bearer #{token.token}")
|> patch("/api/v1/accounts/update_credentials", %{})
@@ -306,53 +236,44 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
end
end
- test "updates profile emojos", %{conn: conn} do
- user = insert(:user)
-
+ test "updates profile emojos", %{user: user, conn: conn} do
note = "*sips :blank:*"
name = "I am :firefox:"
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{
+ ret_conn =
+ patch(conn, "/api/v1/accounts/update_credentials", %{
"note" => note,
"display_name" => name
})
- assert json_response(conn, 200)
+ assert json_response(ret_conn, 200)
- conn =
- conn
- |> get("/api/v1/accounts/#{user.id}")
+ conn = get(conn, "/api/v1/accounts/#{user.id}")
- assert user = json_response(conn, 200)
+ assert user_data = json_response(conn, 200)
- assert user["note"] == note
- assert user["display_name"] == name
- assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = user["emojis"]
+ 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
- user = insert(:user)
-
fields = [
%{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "<script>bar</script>"},
%{"name" => "link", "value" => "cofe.io"}
]
- account =
+ account_data =
conn
- |> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(200)
- assert account["fields"] == [
- %{"name" => "foo", "value" => "bar"},
+ assert account_data["fields"] == [
+ %{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "bar"},
%{"name" => "link", "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>)}
]
- assert account["source"]["fields"] == [
+ assert account_data["source"]["fields"] == [
%{
"name" => "<a href=\"http://google.com\">foo</a>",
"value" => "<script>bar</script>"
@@ -372,12 +293,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
account =
conn
|> put_req_header("content-type", "application/x-www-form-urlencoded")
- |> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", fields)
|> json_response(200)
assert account["fields"] == [
- %{"name" => "foo", "value" => "bar"},
+ %{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "bar"},
%{"name" => "link", "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>)}
]
@@ -398,7 +318,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
assert %{"error" => "Invalid request"} ==
conn
- |> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(403)
@@ -408,7 +327,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
assert %{"error" => "Invalid request"} ==
conn
- |> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(403)
@@ -421,7 +339,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
assert %{"error" => "Invalid request"} ==
conn
- |> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(403)
@@ -432,7 +349,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
account =
conn
- |> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(200)
diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs
index 585cb8a9e..7efccd9c4 100644
--- a/test/web/mastodon_api/controllers/account_controller_test.exs
+++ b/test/web/mastodon_api/controllers/account_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
@@ -15,6 +15,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
import Pleroma.Factory
describe "account fetching" do
+ clear_config([:instance, :limit_to_local_content])
+
test "works by id" do
user = insert(:user)
@@ -44,7 +46,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
end
test "works by nickname for remote users" do
- limit_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
Pleroma.Config.put([:instance, :limit_to_local_content], false)
user = insert(:user, nickname: "user@example.com", local: false)
@@ -52,13 +53,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
build_conn()
|> get("/api/v1/accounts/#{user.nickname}")
- Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local)
assert %{"id" => id} = json_response(conn, 200)
assert id == user.id
end
test "respects limit_to_local_content == :all for remote user nicknames" do
- limit_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
Pleroma.Config.put([:instance, :limit_to_local_content], :all)
user = insert(:user, nickname: "user@example.com", local: false)
@@ -67,12 +66,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
build_conn()
|> get("/api/v1/accounts/#{user.nickname}")
- Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local)
assert json_response(conn, 404)
end
test "respects limit_to_local_content == :unauthenticated for remote user nicknames" do
- limit_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
user = insert(:user, nickname: "user@example.com", local: false)
@@ -87,9 +84,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
conn =
build_conn()
|> assign(:user, reading_user)
+ |> assign(:token, insert(:oauth_token, user: reading_user, scopes: ["read:accounts"]))
|> get("/api/v1/accounts/#{user.nickname}")
- Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local)
assert %{"id" => id} = json_response(conn, 200)
assert id == user.id
end
@@ -144,12 +141,46 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
end
describe "user timelines" do
- test "gets a users statuses", %{conn: conn} do
+ setup do: oauth_access(["read:statuses"])
+
+ 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)
+
+ resp = get(conn, "/api/v1/accounts/#{user_two.id}/statuses")
+
+ assert [%{"id" => id}] = json_response(resp, 200)
+ 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
+ resp = get(conn, "/api/v1/accounts/#{user_two.id}/statuses")
+
+ assert [%{"id" => id}] = json_response(resp, 200)
+ assert id == activity.id
+
+ # Third user's timeline includes the repeat when viewed by unauthenticated user
+ resp = get(build_conn(), "/api/v1/accounts/#{user_three.id}/statuses")
+ assert [%{"id" => id}] = json_response(resp, 200)
+ 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(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, _user_three} = User.follow(user_three, user_one)
{:ok, activity} = CommonAPI.post(user_one, %{"status" => "HI!!!"})
@@ -162,9 +193,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
{:ok, private_activity} =
CommonAPI.post(user_one, %{"status" => "private", "visibility" => "private"})
- resp =
- conn
- |> get("/api/v1/accounts/#{user_one.id}/statuses")
+ resp = get(conn, "/api/v1/accounts/#{user_one.id}/statuses")
assert [%{"id" => id}] = json_response(resp, 200)
assert id == to_string(activity.id)
@@ -172,6 +201,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
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")
assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200)
@@ -181,6 +211,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
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")
assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200)
@@ -192,9 +223,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
note = insert(:note_activity)
user = User.get_cached_by_ap_id(note.data["actor"])
- conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
+ conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?pinned=true")
assert json_response(conn, 200) == []
end
@@ -213,63 +242,51 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
{:ok, image_post} = CommonAPI.post(user, %{"status" => "cofe", "media_ids" => [media_id]})
- conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "true"})
+ conn = get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "true"})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(image_post.id)
- conn =
- build_conn()
- |> get("/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "1"})
+ conn = get(build_conn(), "/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "1"})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(image_post.id)
end
- test "gets a user's statuses without reblogs", %{conn: conn} do
- user = insert(:user)
+ test "gets a user's statuses without reblogs", %{user: user, conn: conn} do
{:ok, post} = CommonAPI.post(user, %{"status" => "HI!!!"})
{:ok, _, _} = CommonAPI.repeat(post.id, user)
- conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "true"})
+ conn = get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "true"})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(post.id)
- conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "1"})
+ conn = get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "1"})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(post.id)
end
- test "filters user's statuses by a hashtag", %{conn: conn} do
- user = insert(:user)
+ test "filters user's statuses by a hashtag", %{user: user, conn: conn} do
{:ok, post} = CommonAPI.post(user, %{"status" => "#hashtag"})
{:ok, _post} = CommonAPI.post(user, %{"status" => "hashtag"})
- conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/statuses", %{"tagged" => "hashtag"})
+ conn = get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"tagged" => "hashtag"})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(post.id)
end
- test "the user views their own timelines and excludes direct messages", %{conn: conn} do
- user = insert(:user)
+ test "the user views their own timelines and excludes direct messages", %{
+ user: user,
+ conn: conn
+ } do
{:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
{:ok, _direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_visibilities" => ["direct"]})
+ get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"exclude_visibilities" => ["direct"]})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(public_activity.id)
@@ -277,46 +294,42 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
end
describe "followers" do
- test "getting followers", %{conn: conn} do
- user = insert(:user)
+ setup do: oauth_access(["read:accounts"])
+
+ test "getting followers", %{user: user, conn: conn} do
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
- conn =
- conn
- |> get("/api/v1/accounts/#{other_user.id}/followers")
+ conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers")
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(user.id)
end
- test "getting followers, hide_followers", %{conn: conn} do
- user = insert(:user)
+ 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 =
- conn
- |> get("/api/v1/accounts/#{other_user.id}/followers")
+ conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers")
assert [] == json_response(conn, 200)
end
- test "getting followers, hide_followers, same user requesting", %{conn: conn} do
+ 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 =
- 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(conn, 200)
end
- test "getting followers, pagination", %{conn: conn} do
- user = insert(:user)
+ test "getting followers, pagination", %{user: user, conn: conn} do
follower1 = insert(:user)
follower2 = insert(:user)
follower3 = insert(:user)
@@ -324,29 +337,19 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
{:ok, _} = User.follow(follower2, user)
{:ok, _} = User.follow(follower3, user)
- conn =
- conn
- |> assign(:user, user)
-
- res_conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/followers?since_id=#{follower1.id}")
+ res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?since_id=#{follower1.id}")
assert [%{"id" => id3}, %{"id" => id2}] = json_response(res_conn, 200)
assert id3 == follower3.id
assert id2 == follower2.id
- res_conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/followers?max_id=#{follower3.id}")
+ res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?max_id=#{follower3.id}")
assert [%{"id" => id2}, %{"id" => id1}] = json_response(res_conn, 200)
assert id2 == follower2.id
assert id1 == follower1.id
- res_conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/followers?limit=1&max_id=#{follower3.id}")
+ res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?limit=1&max_id=#{follower3.id}")
assert [%{"id" => id2}] = json_response(res_conn, 200)
assert id2 == follower2.id
@@ -358,46 +361,47 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
end
describe "following" do
- test "getting following", %{conn: conn} do
- user = insert(:user)
+ 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 =
- conn
- |> get("/api/v1/accounts/#{user.id}/following")
+ conn = get(conn, "/api/v1/accounts/#{user.id}/following")
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(other_user.id)
end
- test "getting following, hide_follows", %{conn: conn} do
+ 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 =
- 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(conn, 200)
end
- test "getting following, hide_follows, same user requesting", %{conn: conn} do
+ 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 =
- 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(conn, 200)
end
- test "getting following, pagination", %{conn: conn} do
- user = insert(:user)
+ test "getting following, pagination", %{user: user, conn: conn} do
following1 = insert(:user)
following2 = insert(:user)
following3 = insert(:user)
@@ -405,29 +409,20 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
{:ok, _} = User.follow(user, following2)
{:ok, _} = User.follow(user, following3)
- conn =
- conn
- |> assign(:user, user)
-
- res_conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/following?since_id=#{following1.id}")
+ res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?since_id=#{following1.id}")
assert [%{"id" => id3}, %{"id" => id2}] = json_response(res_conn, 200)
assert id3 == following3.id
assert id2 == following2.id
- res_conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/following?max_id=#{following3.id}")
+ res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?max_id=#{following3.id}")
assert [%{"id" => id2}, %{"id" => id1}] = json_response(res_conn, 200)
assert id2 == following2.id
assert id1 == following1.id
res_conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/following?limit=1&max_id=#{following3.id}")
+ get(conn, "/api/v1/accounts/#{user.id}/following?limit=1&max_id=#{following3.id}")
assert [%{"id" => id2}] = json_response(res_conn, 200)
assert id2 == following2.id
@@ -439,82 +434,62 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
end
describe "follow/unfollow" do
+ setup do: oauth_access(["follow"])
+
test "following / unfollowing a user", %{conn: conn} do
- user = insert(:user)
other_user = insert(:user)
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/accounts/#{other_user.id}/follow")
-
- assert %{"id" => _id, "following" => true} = json_response(conn, 200)
+ ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/follow")
- user = User.get_cached_by_id(user.id)
+ assert %{"id" => _id, "following" => true} = json_response(ret_conn, 200)
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/v1/accounts/#{other_user.id}/unfollow")
+ ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/unfollow")
- assert %{"id" => _id, "following" => false} = json_response(conn, 200)
+ assert %{"id" => _id, "following" => false} = json_response(ret_conn, 200)
- user = User.get_cached_by_id(user.id)
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/v1/follows", %{"uri" => other_user.nickname})
+ conn = post(conn, "/api/v1/follows", %{"uri" => other_user.nickname})
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(other_user.id)
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(:ok)
+
+ assert %{"id" => ^other_user_id, "following" => false, "requested" => false} =
+ conn |> post("/api/v1/accounts/#{other_user_id}/unfollow") |> json_response(:ok)
+ end
+
test "following without reblogs" do
- follower = insert(:user)
+ %{conn: conn} = oauth_access(["follow", "read:statuses"])
followed = insert(:user)
other_user = insert(:user)
- conn =
- build_conn()
- |> assign(:user, follower)
- |> post("/api/v1/accounts/#{followed.id}/follow?reblogs=false")
+ ret_conn = post(conn, "/api/v1/accounts/#{followed.id}/follow?reblogs=false")
- assert %{"showing_reblogs" => false} = json_response(conn, 200)
+ assert %{"showing_reblogs" => false} = json_response(ret_conn, 200)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "hey"})
{:ok, reblog, _} = CommonAPI.repeat(activity.id, followed)
- conn =
- build_conn()
- |> assign(:user, User.get_cached_by_id(follower.id))
- |> get("/api/v1/timelines/home")
+ ret_conn = get(conn, "/api/v1/timelines/home")
- assert [] == json_response(conn, 200)
+ assert [] == json_response(ret_conn, 200)
- conn =
- build_conn()
- |> assign(:user, User.get_cached_by_id(follower.id))
- |> post("/api/v1/accounts/#{followed.id}/follow?reblogs=true")
+ ret_conn = post(conn, "/api/v1/accounts/#{followed.id}/follow?reblogs=true")
- assert %{"showing_reblogs" => true} = json_response(conn, 200)
+ assert %{"showing_reblogs" => true} = json_response(ret_conn, 200)
- conn =
- build_conn()
- |> assign(:user, User.get_cached_by_id(follower.id))
- |> get("/api/v1/timelines/home")
+ conn = get(conn, "/api/v1/timelines/home")
expected_activity_id = reblog.id
assert [%{"id" => ^expected_activity_id}] = json_response(conn, 200)
end
- test "following / unfollowing errors" do
- user = insert(:user)
-
- conn =
- build_conn()
- |> assign(:user, user)
-
+ test "following / unfollowing errors", %{user: user, conn: conn} do
# self follow
conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow")
assert %{"error" => "Record not found"} = json_response(conn_res, 404)
@@ -544,47 +519,34 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
end
describe "mute/unmute" do
+ setup do: oauth_access(["write:mutes"])
+
test "with notifications", %{conn: conn} do
- user = insert(:user)
other_user = insert(:user)
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/accounts/#{other_user.id}/mute")
+ ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/mute")
- response = json_response(conn, 200)
+ response = json_response(ret_conn, 200)
assert %{"id" => _id, "muting" => true, "muting_notifications" => true} = response
- user = User.get_cached_by_id(user.id)
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/v1/accounts/#{other_user.id}/unmute")
+ conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute")
response = json_response(conn, 200)
assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response
end
test "without notifications", %{conn: conn} do
- user = insert(:user)
other_user = insert(:user)
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"})
+ ret_conn =
+ post(conn, "/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"})
- response = json_response(conn, 200)
+ response = json_response(ret_conn, 200)
assert %{"id" => _id, "muting" => true, "muting_notifications" => false} = response
- user = User.get_cached_by_id(user.id)
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/v1/accounts/#{other_user.id}/unmute")
+ conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute")
response = json_response(conn, 200)
assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response
@@ -595,8 +557,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
setup do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
+ %{conn: conn} = oauth_access(["read:statuses"], user: user)
- [user: user, activity: activity]
+ [conn: conn, user: user, activity: activity]
end
test "returns pinned statuses", %{conn: conn, user: user, activity: activity} do
@@ -604,7 +567,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
result =
conn
- |> assign(:user, user)
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response(200)
@@ -614,23 +576,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
end
end
- test "blocking / unblocking a user", %{conn: conn} do
- user = insert(:user)
+ test "blocking / unblocking a user" do
+ %{conn: conn} = oauth_access(["follow"])
other_user = insert(:user)
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/accounts/#{other_user.id}/block")
+ ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/block")
- assert %{"id" => _id, "blocking" => true} = json_response(conn, 200)
+ assert %{"id" => _id, "blocking" => true} = json_response(ret_conn, 200)
- user = User.get_cached_by_id(user.id)
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/v1/accounts/#{other_user.id}/unblock")
+ conn = post(conn, "/api/v1/accounts/#{other_user.id}/unblock")
assert %{"id" => _id, "blocking" => false} = json_response(conn, 200)
end
@@ -647,10 +601,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
[valid_params: valid_params]
end
+ clear_config([:instance, :account_activation_required])
+
test "Account registration via Application", %{conn: conn} do
conn =
- conn
- |> post("/api/v1/apps", %{
+ post(conn, "/api/v1/apps", %{
client_name: "client_name",
redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
scopes: "read, write, follow"
@@ -667,8 +622,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
} = json_response(conn, 200)
conn =
- conn
- |> post("/oauth/token", %{
+ post(conn, "/oauth/token", %{
grant_type: "client_credentials",
client_id: client_id,
client_secret: client_secret
@@ -721,17 +675,102 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
assert json_response(res, 400) == %{"error" => "{\"email\":[\"has already been taken\"]}"}
end
- test "rate limit", %{conn: conn} do
+ test "returns bad_request if missing required params", %{
+ 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 = post(conn, "/api/v1/accounts", valid_params)
+ assert json_response(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(400)
+
+ assert res == %{"error" => "Missing parameters"}
+ end)
+ end
+
+ clear_config([:instance, :account_activation_required])
+
+ test "returns bad_request if missing email params when :account_activation_required is enabled",
+ %{conn: conn, valid_params: valid_params} do
+ Pleroma.Config.put([:instance, :account_activation_required], true)
+
+ app_token = insert(:oauth_token, user: nil)
+ conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token)
+
+ res =
+ conn
+ |> Map.put(:remote_ip, {127, 0, 0, 5})
+ |> post("/api/v1/accounts", Map.delete(valid_params, :email))
+
+ assert json_response(res, 400) == %{"error" => "Missing parameters"}
+
+ res =
+ conn
+ |> Map.put(:remote_ip, {127, 0, 0, 6})
+ |> post("/api/v1/accounts", Map.put(valid_params, :email, ""))
+
+ assert json_response(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
+ |> Map.put(:remote_ip, {127, 0, 0, 7})
+ |> post("/api/v1/accounts", Map.delete(valid_params, :email))
+
+ assert json_response(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
+ |> Map.put(:remote_ip, {127, 0, 0, 8})
+ |> post("/api/v1/accounts", Map.put(valid_params, :email, ""))
+
+ assert json_response(res, 200)
+ end
+
+ test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_params} do
+ conn = put_req_header(conn, "authorization", "Bearer " <> "invalid-token")
+
+ res = post(conn, "/api/v1/accounts", valid_params)
+ assert json_response(res, 403) == %{"error" => "Invalid credentials"}
+ end
+ end
+
+ describe "create account by app / rate limit" do
+ clear_config([:rate_limit, :app_account_creation]) do
+ Pleroma.Config.put([:rate_limit, :app_account_creation], {10_000, 2})
+ end
+
+ test "respects rate limit setting", %{conn: conn} do
app_token = insert(:oauth_token, user: nil)
conn =
- put_req_header(conn, "authorization", "Bearer " <> app_token.token)
+ conn
+ |> put_req_header("authorization", "Bearer " <> app_token.token)
|> Map.put(:remote_ip, {15, 15, 15, 15})
- for i <- 1..5 do
+ for i <- 1..2 do
conn =
- conn
- |> post("/api/v1/accounts", %{
+ post(conn, "/api/v1/accounts", %{
username: "#{i}lain",
email: "#{i}lain@example.org",
password: "PlzDontHackLain",
@@ -754,8 +793,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
end
conn =
- conn
- |> post("/api/v1/accounts", %{
+ post(conn, "/api/v1/accounts", %{
username: "6lain",
email: "6lain@example.org",
password: "PlzDontHackLain",
@@ -764,53 +802,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
assert json_response(conn, :too_many_requests) == %{"error" => "Throttled"}
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)
-
- res = post(conn, "/api/v1/accounts", valid_params)
- assert json_response(res, 200)
-
- [{127, 0, 0, 1}, {127, 0, 0, 2}, {127, 0, 0, 3}, {127, 0, 0, 4}]
- |> Stream.zip(valid_params)
- |> Enum.each(fn {ip, {attr, _}} ->
- res =
- conn
- |> Map.put(:remote_ip, ip)
- |> post("/api/v1/accounts", Map.delete(valid_params, attr))
- |> json_response(400)
-
- assert res == %{"error" => "Missing parameters"}
- end)
- end
-
- test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_params} do
- conn =
- conn
- |> put_req_header("authorization", "Bearer " <> "invalid-token")
-
- res = post(conn, "/api/v1/accounts", valid_params)
- assert json_response(res, 403) == %{"error" => "Invalid credentials"}
- end
end
describe "GET /api/v1/accounts/:id/lists - account_lists" do
- test "returns lists to which the account belongs", %{conn: conn} do
- user = insert(:user)
+ 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{} = list} = Pleroma.List.create("Test List", user)
{:ok, %{following: _following}} = Pleroma.List.follow(list, other_user)
res =
conn
- |> assign(:user, user)
|> get("/api/v1/accounts/#{other_user.id}/lists")
|> json_response(200)
@@ -819,13 +821,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
end
describe "verify_credentials" do
- test "verify_credentials", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/accounts/verify_credentials")
+ test "verify_credentials" do
+ %{user: user, conn: conn} = oauth_access(["read:accounts"])
+ conn = get(conn, "/api/v1/accounts/verify_credentials")
response = json_response(conn, 200)
@@ -834,25 +832,21 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
assert id == to_string(user.id)
end
- test "verify_credentials default scope unlisted", %{conn: conn} do
+ test "verify_credentials default scope unlisted" do
user = insert(:user, default_scope: "unlisted")
+ %{conn: conn} = oauth_access(["read:accounts"], user: user)
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/accounts/verify_credentials")
+ conn = get(conn, "/api/v1/accounts/verify_credentials")
assert %{"id" => id, "source" => %{"privacy" => "unlisted"}} = json_response(conn, 200)
assert id == to_string(user.id)
end
- test "locked accounts", %{conn: conn} do
+ test "locked accounts" do
user = insert(:user, default_scope: "private")
+ %{conn: conn} = oauth_access(["read:accounts"], user: user)
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/accounts/verify_credentials")
+ conn = get(conn, "/api/v1/accounts/verify_credentials")
assert %{"id" => id, "source" => %{"privacy" => "private"}} = json_response(conn, 200)
assert id == to_string(user.id)
@@ -860,15 +854,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
end
describe "user relationships" do
- test "returns the relationships for the current user", %{conn: conn} do
- user = insert(:user)
+ setup do: oauth_access(["read:follows"])
+
+ test "returns the relationships for the current user", %{user: user, conn: conn} do
other_user = insert(:user)
- {:ok, user} = User.follow(user, other_user)
+ {:ok, _user} = User.follow(user, other_user)
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/accounts/relationships", %{"id" => [other_user.id]})
+ conn = get(conn, "/api/v1/accounts/relationships", %{"id" => [other_user.id]})
assert [relationship] = json_response(conn, 200)
@@ -876,37 +868,29 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
end
test "returns an empty list on a bad request", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/accounts/relationships", %{})
+ conn = get(conn, "/api/v1/accounts/relationships", %{})
assert [] = json_response(conn, 200)
end
end
- test "getting a list of mutes", %{conn: conn} do
- user = insert(:user)
+ test "getting a list of mutes" do
+ %{user: user, conn: conn} = oauth_access(["read:mutes"])
other_user = insert(:user)
- {:ok, user} = User.mute(user, other_user)
+ {:ok, _user_relationships} = User.mute(user, other_user)
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/mutes")
+ conn = get(conn, "/api/v1/mutes")
other_user_id = to_string(other_user.id)
assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
end
- test "getting a list of blocks", %{conn: conn} do
- user = insert(:user)
+ test "getting a list of blocks" do
+ %{user: user, conn: conn} = oauth_access(["read:blocks"])
other_user = insert(:user)
- {:ok, user} = User.block(user, other_user)
+ {:ok, _user_relationship} = User.block(user, other_user)
conn =
conn
diff --git a/test/web/mastodon_api/controllers/app_controller_test.exs b/test/web/mastodon_api/controllers/app_controller_test.exs
index 51788155b..77d234d67 100644
--- a/test/web/mastodon_api/controllers/app_controller_test.exs
+++ b/test/web/mastodon_api/controllers/app_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AppControllerTest do
diff --git a/test/web/mastodon_api/controllers/auth_controller_test.exs b/test/web/mastodon_api/controllers/auth_controller_test.exs
index 98b2a82e7..a485f8e41 100644
--- a/test/web/mastodon_api/controllers/auth_controller_test.exs
+++ b/test/web/mastodon_api/controllers/auth_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AuthControllerTest do
@@ -85,6 +85,37 @@ defmodule Pleroma.Web.MastodonAPI.AuthControllerTest do
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}")
+ |> json_response(:no_content)
+
+ 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}")
+ |> json_response(:no_content)
+ end
+ end
+
describe "POST /auth/password, with invalid parameters" do
setup do
user = insert(:user)
diff --git a/test/web/mastodon_api/controllers/conversation_controller_test.exs b/test/web/mastodon_api/controllers/conversation_controller_test.exs
index 2a1223b18..801b0259b 100644
--- a/test/web/mastodon_api/controllers/conversation_controller_test.exs
+++ b/test/web/mastodon_api/controllers/conversation_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
@@ -10,8 +10,9 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
import Pleroma.Factory
- test "returns a list of conversations", %{conn: conn} do
- user_one = insert(:user)
+ setup do: oauth_access(["read:statuses"])
+
+ test "returns a list of conversations", %{user: user_one, conn: conn} do
user_two = insert(:user)
user_three = insert(:user)
@@ -33,10 +34,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
"visibility" => "private"
})
- res_conn =
- conn
- |> assign(:user, user_one)
- |> get("/api/v1/conversations")
+ res_conn = get(conn, "/api/v1/conversations")
assert response = json_response(res_conn, 200)
@@ -59,8 +57,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0
end
- test "filters conversations by recipients", %{conn: conn} do
- user_one = insert(:user)
+ test "filters conversations by recipients", %{user: user_one, conn: conn} do
user_two = insert(:user)
user_three = insert(:user)
@@ -96,7 +93,6 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
[conversation1, conversation2] =
conn
- |> assign(:user, user_one)
|> get("/api/v1/conversations", %{"recipients" => [user_two.id]})
|> json_response(200)
@@ -105,15 +101,13 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
[conversation1] =
conn
- |> assign(:user, user_one)
|> get("/api/v1/conversations", %{"recipients" => [user_two.id, user_three.id]})
|> json_response(200)
assert conversation1["last_status"]["id"] == direct3.id
end
- test "updates the last_status on reply", %{conn: conn} do
- user_one = insert(:user)
+ test "updates the last_status on reply", %{user: user_one, conn: conn} do
user_two = insert(:user)
{:ok, direct} =
@@ -131,15 +125,13 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
[%{"last_status" => res_last_status}] =
conn
- |> assign(:user, user_one)
|> get("/api/v1/conversations")
|> json_response(200)
assert res_last_status["id"] == direct_reply.id
end
- test "the user marks a conversation as read", %{conn: conn} do
- user_one = insert(:user)
+ test "the user marks a conversation as read", %{user: user_one, conn: conn} do
user_two = insert(:user)
{:ok, direct} =
@@ -151,15 +143,21 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
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
- [%{"id" => direct_conversation_id, "unread" => true}] =
- conn
+ 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(200)
%{"unread" => false} =
- conn
- |> assign(:user, user_two)
+ user_two_conn
|> post("/api/v1/conversations/#{direct_conversation_id}/read")
|> json_response(200)
@@ -176,7 +174,6 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
[%{"unread" => true}] =
conn
- |> assign(:user, user_one)
|> get("/api/v1/conversations")
|> json_response(200)
@@ -195,8 +192,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0
end
- test "(vanilla) Mastodon frontend behaviour", %{conn: conn} do
- user_one = insert(:user)
+ test "(vanilla) Mastodon frontend behaviour", %{user: user_one, conn: conn} do
user_two = insert(:user)
{:ok, direct} =
@@ -205,10 +201,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
"visibility" => "direct"
})
- res_conn =
- conn
- |> assign(:user, user_one)
- |> get("/api/v1/statuses/#{direct.id}/context")
+ res_conn = get(conn, "/api/v1/statuses/#{direct.id}/context")
assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200)
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
index 2d988b0b8..6567a0667 100644
--- a/test/web/mastodon_api/controllers/custom_emoji_controller_test.exs
+++ b/test/web/mastodon_api/controllers/custom_emoji_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.CustomEmojiControllerTest do
diff --git a/test/web/mastodon_api/controllers/domain_block_controller_test.exs b/test/web/mastodon_api/controllers/domain_block_controller_test.exs
index 25a279cdc..8d24b3b88 100644
--- a/test/web/mastodon_api/controllers/domain_block_controller_test.exs
+++ b/test/web/mastodon_api/controllers/domain_block_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
@@ -9,31 +9,25 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
import Pleroma.Factory
- test "blocking / unblocking a domain", %{conn: conn} do
- user = insert(:user)
+ test "blocking / unblocking a domain" do
+ %{user: user, conn: conn} = oauth_access(["write:blocks"])
other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"})
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
+ ret_conn = post(conn, "/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
- assert %{} = json_response(conn, 200)
+ assert %{} = json_response(ret_conn, 200)
user = User.get_cached_by_ap_id(user.ap_id)
assert User.blocks?(user, other_user)
- conn =
- build_conn()
- |> assign(:user, user)
- |> delete("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
+ ret_conn = delete(conn, "/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
- assert %{} = json_response(conn, 200)
+ assert %{} = json_response(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", %{conn: conn} do
- user = insert(:user)
+ 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")
diff --git a/test/web/mastodon_api/controllers/filter_controller_test.exs b/test/web/mastodon_api/controllers/filter_controller_test.exs
index 550689788..97ab005e0 100644
--- a/test/web/mastodon_api/controllers/filter_controller_test.exs
+++ b/test/web/mastodon_api/controllers/filter_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
@@ -7,20 +7,15 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
alias Pleroma.Web.MastodonAPI.FilterView
- import Pleroma.Factory
-
- test "creating a filter", %{conn: conn} do
- user = insert(:user)
+ test "creating a filter" do
+ %{conn: conn} = oauth_access(["write:filters"])
filter = %Pleroma.Filter{
phrase: "knights",
context: ["home"]
}
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context})
+ conn = post(conn, "/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context})
assert response = json_response(conn, 200)
assert response["phrase"] == filter.phrase
@@ -30,8 +25,8 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
assert response["id"] != ""
end
- test "fetching a list of filters", %{conn: conn} do
- user = insert(:user)
+ test "fetching a list of filters" do
+ %{user: user, conn: conn} = oauth_access(["read:filters"])
query_one = %Pleroma.Filter{
user_id: user.id,
@@ -52,7 +47,6 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
response =
conn
- |> assign(:user, user)
|> get("/api/v1/filters")
|> json_response(200)
@@ -64,8 +58,8 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
)
end
- test "get a filter", %{conn: conn} do
- user = insert(:user)
+ test "get a filter" do
+ %{user: user, conn: conn} = oauth_access(["read:filters"])
query = %Pleroma.Filter{
user_id: user.id,
@@ -76,16 +70,13 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
{:ok, filter} = Pleroma.Filter.create(query)
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/filters/#{filter.filter_id}")
+ conn = get(conn, "/api/v1/filters/#{filter.filter_id}")
assert _response = json_response(conn, 200)
end
- test "update a filter", %{conn: conn} do
- user = insert(:user)
+ test "update a filter" do
+ %{user: user, conn: conn} = oauth_access(["write:filters"])
query = %Pleroma.Filter{
user_id: user.id,
@@ -102,9 +93,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
}
conn =
- conn
- |> assign(:user, user)
- |> put("/api/v1/filters/#{query.filter_id}", %{
+ put(conn, "/api/v1/filters/#{query.filter_id}", %{
phrase: new.phrase,
context: new.context
})
@@ -114,8 +103,8 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
assert response["context"] == new.context
end
- test "delete a filter", %{conn: conn} do
- user = insert(:user)
+ test "delete a filter" do
+ %{user: user, conn: conn} = oauth_access(["write:filters"])
query = %Pleroma.Filter{
user_id: user.id,
@@ -126,10 +115,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
{:ok, filter} = Pleroma.Filter.create(query)
- conn =
- conn
- |> assign(:user, user)
- |> delete("/api/v1/filters/#{filter.filter_id}")
+ conn = delete(conn, "/api/v1/filters/#{filter.filter_id}")
assert response = json_response(conn, 200)
assert response == %{}
diff --git a/test/web/mastodon_api/controllers/follow_request_controller_test.exs b/test/web/mastodon_api/controllers/follow_request_controller_test.exs
index 288cd9029..dd848821a 100644
--- a/test/web/mastodon_api/controllers/follow_request_controller_test.exs
+++ b/test/web/mastodon_api/controllers/follow_request_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do
@@ -11,8 +11,13 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do
import Pleroma.Factory
describe "locked accounts" do
- test "/api/v1/follow_requests works" 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} = ActivityPub.follow(other_user, user)
@@ -20,17 +25,13 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do
assert User.following?(other_user, user) == false
- conn =
- build_conn()
- |> assign(:user, user)
- |> get("/api/v1/follow_requests")
+ conn = get(conn, "/api/v1/follow_requests")
assert [relationship] = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
end
- test "/api/v1/follow_requests/:id/authorize works" do
- user = insert(:user, locked: true)
+ test "/api/v1/follow_requests/:id/authorize works", %{user: user, conn: conn} do
other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, user)
@@ -41,10 +42,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do
assert User.following?(other_user, user) == false
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/v1/follow_requests/#{other_user.id}/authorize")
+ conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/authorize")
assert relationship = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
@@ -55,18 +53,14 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do
assert User.following?(other_user, user) == true
end
- test "/api/v1/follow_requests/:id/reject works" do
- user = insert(:user, locked: true)
+ test "/api/v1/follow_requests/:id/reject works", %{user: user, conn: conn} do
other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, user)
user = User.get_cached_by_id(user.id)
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/v1/follow_requests/#{other_user.id}/reject")
+ conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/reject")
assert relationship = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
diff --git a/test/web/mastodon_api/controllers/instance_controller_test.exs b/test/web/mastodon_api/controllers/instance_controller_test.exs
index e00de6b18..2737dcaba 100644
--- a/test/web/mastodon_api/controllers/instance_controller_test.exs
+++ b/test/web/mastodon_api/controllers/instance_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
diff --git a/test/web/mastodon_api/controllers/list_controller_test.exs b/test/web/mastodon_api/controllers/list_controller_test.exs
index 093506309..c9c4cbb49 100644
--- a/test/web/mastodon_api/controllers/list_controller_test.exs
+++ b/test/web/mastodon_api/controllers/list_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ListControllerTest do
@@ -9,44 +9,35 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do
import Pleroma.Factory
- test "creating a list", %{conn: conn} do
- user = insert(:user)
+ test "creating a list" do
+ %{conn: conn} = oauth_access(["write:lists"])
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/lists", %{"title" => "cuties"})
+ conn = post(conn, "/api/v1/lists", %{"title" => "cuties"})
assert %{"title" => title} = json_response(conn, 200)
assert title == "cuties"
end
- test "renders error for invalid params", %{conn: conn} do
- user = insert(:user)
+ test "renders error for invalid params" do
+ %{conn: conn} = oauth_access(["write:lists"])
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/lists", %{"title" => nil})
+ conn = post(conn, "/api/v1/lists", %{"title" => nil})
assert %{"error" => "can't be blank"} == json_response(conn, :unprocessable_entity)
end
- test "listing a user's lists", %{conn: conn} do
- user = insert(:user)
+ test "listing a user's lists" do
+ %{conn: conn} = oauth_access(["read:lists", "write:lists"])
conn
- |> assign(:user, user)
|> post("/api/v1/lists", %{"title" => "cuties"})
+ |> json_response(:ok)
conn
- |> assign(:user, user)
|> post("/api/v1/lists", %{"title" => "cofe"})
+ |> json_response(:ok)
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/lists")
+ conn = get(conn, "/api/v1/lists")
assert [
%{"id" => _, "title" => "cofe"},
@@ -54,41 +45,35 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do
] = json_response(conn, :ok)
end
- test "adding users to a list", %{conn: conn} do
- user = insert(:user)
+ 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)
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
+ conn = post(conn, "/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
assert %{} == json_response(conn, 200)
%Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
assert following == [other_user.follower_address]
end
- test "removing users from a list", %{conn: conn} do
- user = insert(:user)
+ test "removing users from a list" 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)
- conn =
- conn
- |> assign(:user, user)
- |> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
+ conn = delete(conn, "/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
assert %{} == json_response(conn, 200)
%Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
assert following == [third_user.follower_address]
end
- test "listing users in a list", %{conn: conn} do
- user = insert(:user)
+ 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)
@@ -102,8 +87,8 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do
assert id == to_string(other_user.id)
end
- test "retrieving a list", %{conn: conn} do
- user = insert(:user)
+ test "retrieving a list" do
+ %{user: user, conn: conn} = oauth_access(["read:lists"])
{:ok, list} = Pleroma.List.create("name", user)
conn =
@@ -115,32 +100,26 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do
assert id == to_string(list.id)
end
- test "renders 404 if list is not found", %{conn: conn} do
- user = insert(:user)
+ test "renders 404 if list is not found" do
+ %{conn: conn} = oauth_access(["read:lists"])
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/lists/666")
+ conn = get(conn, "/api/v1/lists/666")
assert %{"error" => "List not found"} = json_response(conn, :not_found)
end
- test "renaming a list", %{conn: conn} do
- user = insert(:user)
+ test "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("/api/v1/lists/#{list.id}", %{"title" => "newname"})
+ conn = put(conn, "/api/v1/lists/#{list.id}", %{"title" => "newname"})
assert %{"title" => name} = json_response(conn, 200)
assert name == "newname"
end
- test "validates title when renaming a list", %{conn: conn} do
- user = insert(:user)
+ test "validates title when renaming a list" do
+ %{user: user, conn: conn} = oauth_access(["write:lists"])
{:ok, list} = Pleroma.List.create("name", user)
conn =
@@ -151,14 +130,11 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do
assert %{"error" => "can't be blank"} == json_response(conn, :unprocessable_entity)
end
- test "deleting a list", %{conn: conn} do
- user = insert(:user)
+ test "deleting a list" do
+ %{user: user, conn: conn} = oauth_access(["write:lists"])
{:ok, list} = Pleroma.List.create("name", user)
- conn =
- conn
- |> assign(:user, user)
- |> delete("/api/v1/lists/#{list.id}")
+ conn = delete(conn, "/api/v1/lists/#{list.id}")
assert %{} = json_response(conn, 200)
assert is_nil(Repo.get(Pleroma.List, list.id))
diff --git a/test/web/mastodon_api/controllers/marker_controller_test.exs b/test/web/mastodon_api/controllers/marker_controller_test.exs
index 1fcad873d..919f295bd 100644
--- a/test/web/mastodon_api/controllers/marker_controller_test.exs
+++ b/test/web/mastodon_api/controllers/marker_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
diff --git a/test/web/mastodon_api/controllers/media_controller_test.exs b/test/web/mastodon_api/controllers/media_controller_test.exs
index 06c6a1cb3..203fa73b0 100644
--- a/test/web/mastodon_api/controllers/media_controller_test.exs
+++ b/test/web/mastodon_api/controllers/media_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do
@@ -9,23 +9,17 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
- import Pleroma.Factory
+ setup do: oauth_access(["write:media"])
describe "media upload" do
setup do
- user = insert(:user)
-
- conn =
- build_conn()
- |> assign(:user, user)
-
image = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
- [conn: conn, image: image]
+ [image: image]
end
clear_config([:media_proxy])
@@ -49,9 +43,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do
end
describe "PUT /api/v1/media/:id" do
- setup do
- actor = insert(:user)
-
+ setup %{user: actor} do
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
@@ -65,13 +57,12 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do
description: "test-m"
)
- [actor: actor, object: object]
+ [object: object]
end
- test "updates name of media", %{conn: conn, actor: actor, object: object} do
+ test "updates name of media", %{conn: conn, object: object} do
media =
conn
- |> assign(:user, actor)
|> put("/api/v1/media/#{object.id}", %{"description" => "test-media"})
|> json_response(:ok)
@@ -79,10 +70,9 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do
assert refresh_record(object).data["name"] == "test-media"
end
- test "returns error wheb request is bad", %{conn: conn, actor: actor, object: object} do
+ test "returns error when request is bad", %{conn: conn, object: object} do
media =
conn
- |> assign(:user, actor)
|> put("/api/v1/media/#{object.id}", %{})
|> json_response(400)
diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs
index c0b3621de..3bc9aff16 100644
--- a/test/web/mastodon_api/controllers/notification_controller_test.exs
+++ b/test/web/mastodon_api/controllers/notification_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
@@ -12,8 +12,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
import Pleroma.Factory
- test "list of notifications", %{conn: conn} do
- user = insert(:user)
+ 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}"})
@@ -34,18 +34,15 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
assert response == expected_response
end
- test "getting a single notification", %{conn: conn} do
- user = insert(:user)
+ 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 =
- conn
- |> assign(:user, user)
- |> get("/api/v1/notifications/#{notification.id}")
+ conn = get(conn, "/api/v1/notifications/#{notification.id}")
expected_response =
"hi <span class=\"h-card\"><a data-user=\"#{user.id}\" class=\"u-url mention\" href=\"#{
@@ -56,8 +53,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
assert response == expected_response
end
- test "dismissing a single notification", %{conn: conn} do
- user = insert(:user)
+ 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}"})
@@ -72,32 +69,26 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
assert %{} = json_response(conn, 200)
end
- test "clearing all notifications", %{conn: conn} do
- user = insert(:user)
+ 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)
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/notifications/clear")
+ ret_conn = post(conn, "/api/v1/notifications/clear")
- assert %{} = json_response(conn, 200)
+ assert %{} = json_response(ret_conn, 200)
- conn =
- build_conn()
- |> assign(:user, user)
- |> get("/api/v1/notifications")
+ ret_conn = get(conn, "/api/v1/notifications")
- assert all = json_response(conn, 200)
+ assert all = json_response(ret_conn, 200)
assert all == []
end
- test "paginates notifications using min_id, since_id, max_id, and limit", %{conn: conn} do
- user = insert(:user)
+ 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}"})
@@ -137,59 +128,148 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
end
- test "filters notifications using exclude_visibilities", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, public_activity} =
- CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "public"})
+ describe "exclude_visibilities" do
+ test "filters notifications for mentions" do
+ %{user: user, conn: conn} = oauth_access(["read:notifications"])
+ other_user = insert(:user)
- {: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"})
-
- conn = assign(conn, :user, user)
-
- conn_res =
- get(conn, "/api/v1/notifications", %{
- exclude_visibilities: ["public", "unlisted", "private"]
- })
-
- assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
- assert id == direct_activity.id
-
- conn_res =
- get(conn, "/api/v1/notifications", %{
- exclude_visibilities: ["public", "unlisted", "direct"]
- })
-
- assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
- assert id == private_activity.id
-
- conn_res =
- get(conn, "/api/v1/notifications", %{
- exclude_visibilities: ["public", "private", "direct"]
- })
-
- assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
- assert id == unlisted_activity.id
-
- conn_res =
- get(conn, "/api/v1/notifications", %{
- exclude_visibilities: ["unlisted", "private", "direct"]
- })
+ {: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"})
+
+ conn_res =
+ get(conn, "/api/v1/notifications", %{
+ exclude_visibilities: ["public", "unlisted", "private"]
+ })
+
+ assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
+ assert id == direct_activity.id
+
+ conn_res =
+ get(conn, "/api/v1/notifications", %{
+ exclude_visibilities: ["public", "unlisted", "direct"]
+ })
- assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
- assert id == public_activity.id
+ assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
+ assert id == private_activity.id
+
+ conn_res =
+ get(conn, "/api/v1/notifications", %{
+ exclude_visibilities: ["public", "private", "direct"]
+ })
+
+ assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
+ assert id == unlisted_activity.id
+
+ conn_res =
+ get(conn, "/api/v1/notifications", %{
+ exclude_visibilities: ["unlisted", "private", "direct"]
+ })
+
+ assert [%{"status" => %{"id" => id}}] = json_response(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(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(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(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(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(200)
+ |> Enum.map(& &1["status"]["id"])
+
+ assert public_activity.id in activity_ids
+ refute unlisted_activity.id in activity_ids
+ end
end
- test "filters notifications using exclude_types", %{conn: conn} do
- user = insert(:user)
+ 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}"})
@@ -203,8 +283,6 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
reblog_notification_id = get_notification_id_by_activity(reblog_activity)
follow_notification_id = get_notification_id_by_activity(follow_activity)
- conn = assign(conn, :user, user)
-
conn_res =
get(conn, "/api/v1/notifications", %{exclude_types: ["mention", "favourite", "reblog"]})
@@ -226,8 +304,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
assert [%{"id" => ^reblog_notification_id}] = json_response(conn_res, 200)
end
- test "destroy multiple", %{conn: conn} do
- user = insert(:user)
+ 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}"})
@@ -240,8 +318,6 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
notification3_id = get_notification_id_by_activity(activity3)
notification4_id = get_notification_id_by_activity(activity4)
- conn = assign(conn, :user, user)
-
result =
conn
|> get("/api/v1/notifications")
@@ -252,6 +328,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
conn2 =
conn
|> assign(:user, other_user)
+ |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:notifications"]))
result =
conn2
@@ -276,71 +353,134 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
end
- test "doesn't see notifications after muting user with notifications", %{conn: conn} do
- user = insert(:user)
+ 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}"})
- conn = assign(conn, :user, user)
-
- conn = get(conn, "/api/v1/notifications")
+ ret_conn = get(conn, "/api/v1/notifications")
- assert length(json_response(conn, 200)) == 1
+ assert length(json_response(ret_conn, 200)) == 1
- {:ok, user} = User.mute(user, user2)
+ {:ok, _user_relationships} = User.mute(user, user2)
- conn = assign(build_conn(), :user, user)
conn = get(conn, "/api/v1/notifications")
assert json_response(conn, 200) == []
end
- test "see notifications after muting user without notifications", %{conn: conn} do
- user = insert(:user)
+ 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}"})
- conn = assign(conn, :user, user)
-
- conn = get(conn, "/api/v1/notifications")
+ ret_conn = get(conn, "/api/v1/notifications")
- assert length(json_response(conn, 200)) == 1
+ assert length(json_response(ret_conn, 200)) == 1
- {:ok, user} = User.mute(user, user2, false)
+ {:ok, _user_relationships} = User.mute(user, user2, false)
- conn = assign(build_conn(), :user, user)
conn = get(conn, "/api/v1/notifications")
assert length(json_response(conn, 200)) == 1
end
- test "see notifications after muting user with notifications and with_muted parameter", %{
- conn: conn
- } do
- user = insert(:user)
+ 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}"})
- conn = assign(conn, :user, user)
+ ret_conn = get(conn, "/api/v1/notifications")
- conn = get(conn, "/api/v1/notifications")
+ assert length(json_response(ret_conn, 200)) == 1
+
+ {:ok, _user_relationships} = User.mute(user, user2)
+
+ conn = get(conn, "/api/v1/notifications", %{"with_muted" => "true"})
assert length(json_response(conn, 200)) == 1
+ end
- {:ok, user} = User.mute(user, user2)
+ test "see move notifications with `with_move` parameter" do
+ old_user = insert(:user)
+ new_user = insert(:user, also_known_as: [old_user.ap_id])
+ %{user: follower, conn: conn} = oauth_access(["read:notifications"])
- conn = assign(build_conn(), :user, user)
- conn = get(conn, "/api/v1/notifications", %{"with_muted" => "true"})
+ User.follow(follower, old_user)
+ Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user)
+ Pleroma.Tests.ObanHelpers.perform_all()
+
+ ret_conn = get(conn, "/api/v1/notifications")
+
+ assert json_response(ret_conn, 200) == []
+
+ conn = get(conn, "/api/v1/notifications", %{"with_move" => "true"})
assert length(json_response(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", %{media_only: true})
+
+ assert [link_header] = get_resp_header(conn, "link")
+ assert link_header =~ ~r/media_only=true/
+ 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(200)
+
+ assert %{"error" => "Account is not found"} =
+ conn
+ |> assign(:user, user)
+ |> get("/api/v1/notifications", %{account_id: "cofe"})
+ |> json_response(404)
+ end
+ end
+
defp get_notification_id_by_activity(%{id: id}) do
Notification
|> Repo.get_by(activity_id: id)
diff --git a/test/web/mastodon_api/controllers/poll_controller_test.exs b/test/web/mastodon_api/controllers/poll_controller_test.exs
index 40cf3e879..88b13a25a 100644
--- a/test/web/mastodon_api/controllers/poll_controller_test.exs
+++ b/test/web/mastodon_api/controllers/poll_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.PollControllerTest do
@@ -11,9 +11,9 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do
import Pleroma.Factory
describe "GET /api/v1/polls/:id" do
- test "returns poll entity for object id", %{conn: conn} do
- user = insert(:user)
+ 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",
@@ -22,10 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do
object = Object.normalize(activity)
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/polls/#{object.id}")
+ conn = get(conn, "/api/v1/polls/#{object.id}")
response = json_response(conn, 200)
id = to_string(object.id)
@@ -33,11 +30,10 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do
end
test "does not expose polls for private statuses", %{conn: conn} do
- user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
- CommonAPI.post(user, %{
+ CommonAPI.post(other_user, %{
"status" => "Pleroma does",
"poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20},
"visibility" => "private"
@@ -45,22 +41,20 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do
object = Object.normalize(activity)
- conn =
- conn
- |> assign(:user, other_user)
- |> get("/api/v1/polls/#{object.id}")
+ conn = get(conn, "/api/v1/polls/#{object.id}")
assert json_response(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
- user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
- CommonAPI.post(user, %{
+ CommonAPI.post(other_user, %{
"status" => "A very delicious sandwich",
"poll" => %{
"options" => ["Lettuce", "Grilled Bacon", "Tomato"],
@@ -71,10 +65,7 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do
object = Object.normalize(activity)
- conn =
- conn
- |> assign(:user, other_user)
- |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]})
+ conn = post(conn, "/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]})
assert json_response(conn, 200)
object = Object.get_by_id(object.id)
@@ -84,9 +75,7 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do
end)
end
- test "author can't vote", %{conn: conn} do
- user = insert(:user)
-
+ test "author can't vote", %{user: user, conn: conn} do
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "Am I cute?",
@@ -96,7 +85,6 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do
object = Object.normalize(activity)
assert conn
- |> assign(:user, user)
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]})
|> json_response(422) == %{"error" => "Poll's author can't vote"}
@@ -106,11 +94,10 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do
end
test "does not allow multiple choices on a single-choice question", %{conn: conn} do
- user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
- CommonAPI.post(user, %{
+ CommonAPI.post(other_user, %{
"status" => "The glass is",
"poll" => %{"options" => ["half empty", "half full"], "expires_in" => 20}
})
@@ -118,7 +105,6 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do
object = Object.normalize(activity)
assert conn
- |> assign(:user, other_user)
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]})
|> json_response(422) == %{"error" => "Too many choices"}
@@ -130,42 +116,32 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do
end
test "does not allow choice index to be greater than options count", %{conn: conn} do
- user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
- CommonAPI.post(user, %{
+ CommonAPI.post(other_user, %{
"status" => "Am I cute?",
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
})
object = Object.normalize(activity)
- conn =
- conn
- |> assign(:user, other_user)
- |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [2]})
+ conn = post(conn, "/api/v1/polls/#{object.id}/votes", %{"choices" => [2]})
assert json_response(conn, 422) == %{"error" => "Invalid indices"}
end
test "returns 404 error when object is not exist", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/polls/1/votes", %{"choices" => [0]})
+ conn = post(conn, "/api/v1/polls/1/votes", %{"choices" => [0]})
assert json_response(conn, 404) == %{"error" => "Record not found"}
end
test "returns 404 when poll is private and not available for user", %{conn: conn} do
- user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
- CommonAPI.post(user, %{
+ CommonAPI.post(other_user, %{
"status" => "Am I cute?",
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20},
"visibility" => "private"
@@ -173,10 +149,7 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do
object = Object.normalize(activity)
- conn =
- conn
- |> assign(:user, other_user)
- |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0]})
+ conn = post(conn, "/api/v1/polls/#{object.id}/votes", %{"choices" => [0]})
assert json_response(conn, 404) == %{"error" => "Record not found"}
end
diff --git a/test/web/mastodon_api/controllers/report_controller_test.exs b/test/web/mastodon_api/controllers/report_controller_test.exs
index 979ca48f3..34ec8119e 100644
--- a/test/web/mastodon_api/controllers/report_controller_test.exs
+++ b/test/web/mastodon_api/controllers/report_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
@@ -9,32 +9,30 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
import Pleroma.Factory
+ setup do: oauth_access(["write:reports"])
+
setup do
- reporter = insert(:user)
target_user = insert(:user)
{:ok, activity} = CommonAPI.post(target_user, %{"status" => "foobar"})
- [reporter: reporter, target_user: target_user, activity: activity]
+ [target_user: target_user, activity: activity]
end
- test "submit a basic report", %{conn: conn, reporter: reporter, target_user: target_user} do
+ test "submit a basic report", %{conn: conn, target_user: target_user} do
assert %{"action_taken" => false, "id" => _} =
conn
- |> assign(:user, reporter)
|> post("/api/v1/reports", %{"account_id" => target_user.id})
|> json_response(200)
end
test "submit a report with statuses and comment", %{
conn: conn,
- reporter: reporter,
target_user: target_user,
activity: activity
} do
assert %{"action_taken" => false, "id" => _} =
conn
- |> assign(:user, reporter)
|> post("/api/v1/reports", %{
"account_id" => target_user.id,
"status_ids" => [activity.id],
@@ -46,19 +44,16 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
test "account_id is required", %{
conn: conn,
- reporter: reporter,
activity: activity
} do
assert %{"error" => "Valid `account_id` required"} =
conn
- |> assign(:user, reporter)
|> post("/api/v1/reports", %{"status_ids" => [activity.id]})
|> json_response(400)
end
test "comment must be up to the size specified in the config", %{
conn: conn,
- reporter: reporter,
target_user: target_user
} do
max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000)
@@ -68,21 +63,25 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
assert ^error =
conn
- |> assign(:user, reporter)
|> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment})
|> json_response(400)
end
test "returns error when account is not exist", %{
conn: conn,
- reporter: reporter,
activity: activity
} do
- conn =
- conn
- |> assign(:user, reporter)
- |> post("/api/v1/reports", %{"status_ids" => [activity.id], "account_id" => "foo"})
+ conn = post(conn, "/api/v1/reports", %{"status_ids" => [activity.id], "account_id" => "foo"})
assert json_response(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
+ |> post("/api/v1/reports", %{"account_id" => target_user.id})
+ |> json_response(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
index ae5fee2bc..3cd08c189 100644
--- a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs
+++ b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do
@@ -9,91 +9,106 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do
alias Pleroma.ScheduledActivity
import Pleroma.Factory
+ import Ecto.Query
+
+ clear_config([ScheduledActivity, :enabled])
+
+ test "shows scheduled activities" do
+ %{user: user, conn: conn} = oauth_access(["read:statuses"])
- test "shows scheduled activities", %{conn: conn} do
- user = insert(:user)
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()
- conn =
- conn
- |> assign(:user, user)
-
# min_id
- conn_res =
- conn
- |> get("/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}")
+ conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}")
result = json_response(conn_res, 200)
assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
# since_id
- conn_res =
- conn
- |> get("/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}")
+ conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}")
result = json_response(conn_res, 200)
assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result
# max_id
- conn_res =
- conn
- |> get("/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}")
+ conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}")
result = json_response(conn_res, 200)
assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
end
- test "shows a scheduled activity", %{conn: conn} do
- user = insert(:user)
+ test "shows a scheduled activity" do
+ %{user: user, conn: conn} = oauth_access(["read:statuses"])
scheduled_activity = insert(:scheduled_activity, user: user)
- res_conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
+ res_conn = get(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}")
assert %{"id" => scheduled_activity_id} = json_response(res_conn, 200)
assert scheduled_activity_id == scheduled_activity.id |> to_string()
- res_conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/scheduled_statuses/404")
+ res_conn = get(conn, "/api/v1/scheduled_statuses/404")
assert %{"error" => "Record not found"} = json_response(res_conn, 404)
end
- test "updates a scheduled activity", %{conn: conn} do
- user = insert(:user)
- scheduled_activity = insert(:scheduled_activity, user: user)
+ test "updates a scheduled activity" do
+ Pleroma.Config.put([ScheduledActivity, :enabled], true)
+ %{user: user, conn: conn} = oauth_access(["write:statuses"])
- new_scheduled_at =
- NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
+ 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 = Timex.shift(NaiveDateTime.utc_now(), minutes: 120)
res_conn =
- conn
- |> assign(:user, user)
- |> put("/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{
+ put(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{
scheduled_at: new_scheduled_at
})
assert %{"scheduled_at" => expected_scheduled_at} = json_response(res_conn, 200)
assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at)
+ job = refresh_record(job)
- res_conn =
- conn
- |> assign(:user, user)
- |> put("/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at})
+ assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(new_scheduled_at)
+
+ res_conn = put(conn, "/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at})
assert %{"error" => "Record not found"} = json_response(res_conn, 404)
end
- test "deletes a scheduled activity", %{conn: conn} do
- user = insert(:user)
- scheduled_activity = insert(:scheduled_activity, user: user)
+ 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
@@ -101,7 +116,8 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do
|> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
assert %{} = json_response(res_conn, 200)
- assert nil == Repo.get(ScheduledActivity, scheduled_activity.id)
+ refute Repo.get(ScheduledActivity, scheduled_activity.id)
+ refute Repo.get(Oban.Job, job.id)
res_conn =
conn
diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs
index 7953fad62..11133ff66 100644
--- a/test/web/mastodon_api/controllers/search_controller_test.exs
+++ b/test/web/mastodon_api/controllers/search_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
@@ -53,7 +53,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
{:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
results =
- get(conn, "/api/v2/search", %{"q" => "2hu #private"})
+ conn
+ |> get("/api/v2/search", %{"q" => "2hu #private"})
|> json_response(200)
[account | _] = results["accounts"]
@@ -73,17 +74,39 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
[status] = results["statuses"]
assert status["id"] == to_string(activity.id)
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(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 = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
results =
conn
- |> assign(:user, user)
|> get("/api/v1/accounts/search", %{"q" => "shp"})
|> json_response(200)
@@ -94,7 +117,6 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
results =
conn
- |> assign(:user, user)
|> get("/api/v1/accounts/search", %{"q" => "2hu"})
|> json_response(200)
@@ -104,11 +126,10 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
end
test "returns account if query contains a space", %{conn: conn} do
- user = insert(:user, %{nickname: "shp@shitposter.club"})
+ insert(:user, %{nickname: "shp@shitposter.club"})
results =
conn
- |> assign(:user, user)
|> get("/api/v1/accounts/search", %{"q" => "shp@shitposter.club xxx "})
|> json_response(200)
@@ -150,11 +171,10 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
{:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
- conn =
+ results =
conn
|> get("/api/v1/search", %{"q" => "2hu"})
-
- assert results = json_response(conn, 200)
+ |> json_response(200)
[account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id)
@@ -165,15 +185,19 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
assert status["id"] == to_string(activity.id)
end
- test "search fetches remote statuses", %{conn: conn} do
+ test "search fetches remote statuses and prefers them over other results", %{conn: conn} do
capture_log(fn ->
- conn =
+ {:ok, %{id: activity_id}} =
+ CommonAPI.post(insert(:user), %{
+ "status" => "check out https://shitposter.club/notice/2827873"
+ })
+
+ results =
conn
|> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"})
+ |> json_response(200)
- assert results = json_response(conn, 200)
-
- [status] = results["statuses"]
+ [status, %{"id" => ^activity_id}] = results["statuses"]
assert status["uri"] ==
"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
@@ -188,11 +212,10 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
})
capture_log(fn ->
- conn =
+ results =
conn
|> get("/api/v1/search", %{"q" => Object.normalize(activity).data["id"]})
-
- assert results = json_response(conn, 200)
+ |> json_response(200)
[] = results["statuses"]
end)
@@ -201,22 +224,23 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
test "search fetches remote accounts", %{conn: conn} do
user = insert(:user)
- conn =
+ results =
conn
|> assign(:user, user)
+ |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"]))
|> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "true"})
+ |> json_response(200)
- assert results = json_response(conn, 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
- conn =
+ results =
conn
|> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "false"})
+ |> json_response(200)
- assert results = json_response(conn, 200)
assert [] == results["accounts"]
end
diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs
index 2ce201e2e..f36552041 100644
--- a/test/web/mastodon_api/controllers/status_controller_test.exs
+++ b/test/web/mastodon_api/controllers/status_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
@@ -18,30 +18,20 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
- import ExUnit.CaptureLog
clear_config([:instance, :federating])
clear_config([:instance, :allow_relay])
+ clear_config([:rich_media, :enabled])
describe "posting statuses" do
- setup do
- user = insert(:user)
-
- conn =
- build_conn()
- |> assign(:user, user)
-
- [conn: conn]
- end
+ setup do: oauth_access(["write:statuses"])
test "posting a status does not increment reblog_count when relaying", %{conn: conn} do
Pleroma.Config.put([:instance, :federating], true)
Pleroma.Config.get([:instance, :allow_relay], true)
- user = insert(:user)
response =
conn
- |> assign(:user, user)
|> post("api/v1/statuses", %{
"content_type" => "text/plain",
"source" => "Pleroma FE",
@@ -55,7 +45,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
response =
conn
- |> assign(:user, user)
|> get("api/v1/statuses/#{response["id"]}", %{})
|> json_response(200)
@@ -133,9 +122,33 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
NaiveDateTime.to_iso8601(expiration.scheduled_at)
end
- test "posting an undefined status with an attachment", %{conn: conn} do
- user = insert(:user)
+ test "it fails to create a status if `expires_in` is less or equal than an hour", %{
+ conn: conn
+ } do
+ # 1 hour
+ expires_in = 60 * 60
+
+ assert %{"error" => "Expiry date is too soon"} =
+ conn
+ |> post("api/v1/statuses", %{
+ "status" => "oolong",
+ "expires_in" => expires_in
+ })
+ |> json_response(422)
+ # 30 minutes
+ expires_in = 30 * 60
+
+ assert %{"error" => "Expiry date is too soon"} =
+ conn
+ |> post("api/v1/statuses", %{
+ "status" => "oolong",
+ "expires_in" => expires_in
+ })
+ |> json_response(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"),
@@ -145,17 +158,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{
+ post(conn, "/api/v1/statuses", %{
"media_ids" => [to_string(upload.id)]
})
assert json_response(conn, 200)
end
- test "replying to a status", %{conn: conn} do
- user = insert(:user)
+ test "replying to a status", %{user: user, conn: conn} do
{:ok, replied_to} = CommonAPI.post(user, %{"status" => "cofe"})
conn =
@@ -170,8 +180,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
end
- test "replying to a direct message with visibility other than direct", %{conn: conn} do
- user = insert(:user)
+ 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 ->
@@ -188,18 +200,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end
test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
- conn =
- conn
- |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
+ conn = post(conn, "/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a sensitive status", %{conn: conn} do
- conn =
- conn
- |> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
+ conn = post(conn, "/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
assert %{"content" => "cofe", "id" => id, "sensitive" => true} = json_response(conn, 200)
assert Activity.get_by_id(id)
@@ -207,8 +215,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
test "posting a fake status", %{conn: conn} do
real_conn =
- conn
- |> post("/api/v1/statuses", %{
+ post(conn, "/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"
})
@@ -227,8 +234,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> Kernel.put_in(["pleroma", "conversation_id"], nil)
fake_conn =
- conn
- |> post("/api/v1/statuses", %{
+ post(conn, "/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
@@ -255,8 +261,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
Config.put([:rich_media, :enabled], true)
conn =
- conn
- |> post("/api/v1/statuses", %{
+ post(conn, "/api/v1/statuses", %{
"status" => "https://example.com/ogp"
})
@@ -268,9 +273,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
user2 = insert(:user)
content = "direct cofe @#{user2.nickname}"
- conn =
- conn
- |> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"})
+ conn = post(conn, "api/v1/statuses", %{"status" => content, "visibility" => "direct"})
assert %{"id" => id} = response = json_response(conn, 200)
assert response["visibility"] == "direct"
@@ -283,14 +286,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end
describe "posting scheduled statuses" do
+ setup do: oauth_access(["write:statuses"])
+
test "creates a scheduled activity", %{conn: conn} do
- user = insert(:user)
scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{
+ post(conn, "/api/v1/statuses", %{
"status" => "scheduled",
"scheduled_at" => scheduled_at
})
@@ -300,8 +302,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert [] == Repo.all(Activity)
end
- test "creates a scheduled activity with a media attachment", %{conn: conn} do
- user = insert(:user)
+ test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
file = %Plug.Upload{
@@ -313,9 +314,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{
+ post(conn, "/api/v1/statuses", %{
"media_ids" => [to_string(upload.id)],
"status" => "scheduled",
"scheduled_at" => scheduled_at
@@ -327,15 +326,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
%{conn: conn} do
- user = insert(:user)
-
scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{
+ post(conn, "/api/v1/statuses", %{
"status" => "not scheduled",
"scheduled_at" => scheduled_at
})
@@ -344,9 +339,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert [] == Repo.all(ScheduledActivity)
end
- test "returns error when daily user limit is exceeded", %{conn: conn} do
- user = insert(:user)
-
+ test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do
today =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(6), :millisecond)
@@ -356,17 +349,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, attrs)
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
+ conn = post(conn, "/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
assert %{"error" => "daily limit exceeded"} == json_response(conn, 422)
end
- test "returns error when total user limit is exceeded", %{conn: conn} do
- user = insert(:user)
-
+ test "returns error when total user limit is exceeded", %{user: user, conn: conn} do
today =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(6), :millisecond)
@@ -383,23 +371,20 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
{:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
+ post(conn, "/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
assert %{"error" => "total limit exceeded"} == json_response(conn, 422)
end
end
describe "posting polls" do
+ setup do: oauth_access(["write:statuses"])
+
test "posting a poll", %{conn: conn} do
- user = insert(:user)
time = NaiveDateTime.utc_now()
conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{
+ post(conn, "/api/v1/statuses", %{
"status" => "Who is the #bestgrill?",
"poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420}
})
@@ -412,16 +397,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
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
- user = insert(:user)
limit = Config.get([:instance, :poll_limits, :max_options])
conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{
+ post(conn, "/api/v1/statuses", %{
"status" => "desu~",
"poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
})
@@ -431,13 +418,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end
test "option character limit is enforced", %{conn: conn} do
- user = insert(:user)
limit = Config.get([:instance, :poll_limits, :max_option_chars])
conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{
+ post(conn, "/api/v1/statuses", %{
"status" => "...",
"poll" => %{
"options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
@@ -450,13 +434,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end
test "minimal date limit is enforced", %{conn: conn} do
- user = insert(:user)
limit = Config.get([:instance, :poll_limits, :min_expiration])
conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{
+ post(conn, "/api/v1/statuses", %{
"status" => "imagine arbitrary limits",
"poll" => %{
"options" => ["this post was made by pleroma gang"],
@@ -469,13 +450,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end
test "maximum date limit is enforced", %{conn: conn} do
- user = insert(:user)
limit = Config.get([:instance, :poll_limits, :max_expiration])
conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{
+ post(conn, "/api/v1/statuses", %{
"status" => "imagine arbitrary limits",
"poll" => %{
"options" => ["this post was made by pleroma gang"],
@@ -488,19 +466,27 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end
end
- test "get a status", %{conn: conn} do
+ test "get a status" do
+ %{conn: conn} = oauth_access(["read:statuses"])
activity = insert(:note_activity)
- conn =
- conn
- |> get("/api/v1/statuses/#{activity.id}")
+ conn = get(conn, "/api/v1/statuses/#{activity.id}")
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(activity.id)
end
- test "get a direct status", %{conn: conn} do
- user = insert(:user)
+ 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(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} =
@@ -517,7 +503,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert res["pleroma"]["direct_conversation_id"] == participation.id
end
- test "get statuses by IDs", %{conn: conn} do
+ test "get statuses by IDs" do
+ %{conn: conn} = oauth_access(["read:statuses"])
%{id: id1} = insert(:note_activity)
%{id: id2} = insert(:note_activity)
@@ -528,9 +515,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end
describe "deleting a status" do
- test "when you created it", %{conn: conn} do
- activity = insert(:note_activity)
- author = User.get_cached_by_ap_id(activity.data["actor"])
+ test "when you created it" do
+ %{user: author, conn: conn} = oauth_access(["write:statuses"])
+ activity = insert(:note_activity, user: author)
conn =
conn
@@ -542,14 +529,23 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
refute Activity.get_by_id(activity.id)
end
- test "when you didn't create it", %{conn: conn} do
- activity = insert(:note_activity)
- user = insert(:user)
+ 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, user)
- |> delete("/api/v1/statuses/#{activity.id}")
+ |> assign(:user, author)
+ |> delete("/api/v1/statuses/#{String.downcase(activity.id)}")
+
+ assert %{"error" => "Record not found"} == json_response(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" => _} = json_response(conn, 403)
@@ -565,6 +561,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
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(res_conn, 200)
@@ -572,6 +569,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
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(res_conn, 200)
@@ -582,14 +580,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end
describe "reblogging" do
+ setup do: oauth_access(["write:statuses"])
+
test "reblogs and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity)
- user = insert(:user)
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{activity.id}/reblog")
+ conn = post(conn, "/api/v1/statuses/#{activity.id}/reblog")
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
@@ -599,14 +595,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
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 = post(conn, "/api/v1/statuses/#{String.downcase(activity.id)}/reblog")
+
+ assert %{"error" => "Record not found"} = json_response(conn, 404)
+ end
+
test "reblogs privately and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity)
- user = insert(:user)
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{activity.id}/reblog", %{"visibility" => "private"})
+ conn = post(conn, "/api/v1/statuses/#{activity.id}/reblog", %{"visibility" => "private"})
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
@@ -617,7 +617,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert to_string(activity.id) == id
end
- test "reblogged status for another user", %{conn: conn} do
+ test "reblogged status for another user" do
activity = insert(:note_activity)
user1 = insert(:user)
user2 = insert(:user)
@@ -628,8 +628,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
{:ok, _, _object} = CommonAPI.repeat(activity.id, user2)
conn_res =
- conn
+ build_conn()
|> assign(:user, user3)
+ |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
|> get("/api/v1/statuses/#{reblog_activity1.id}")
assert %{
@@ -640,8 +641,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
} = json_response(conn_res, 200)
conn_res =
- conn
+ build_conn()
|> assign(:user, user2)
+ |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"]))
|> get("/api/v1/statuses/#{reblog_activity1.id}")
assert %{
@@ -653,57 +655,37 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert to_string(activity.id) == id
end
-
- test "returns 400 error when activity is not exist", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/foo/reblog")
-
- assert json_response(conn, 400) == %{"error" => "Could not repeat"}
- end
end
describe "unreblogging" do
- test "unreblogs and returns the unreblogged status", %{conn: conn} do
+ setup do: oauth_access(["write:statuses"])
+
+ test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do
activity = insert(:note_activity)
- user = insert(:user)
{:ok, _, _} = CommonAPI.repeat(activity.id, user)
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{activity.id}/unreblog")
+ conn = post(conn, "/api/v1/statuses/#{activity.id}/unreblog")
assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = json_response(conn, 200)
assert to_string(activity.id) == id
end
- test "returns 400 error when activity is not exist", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/foo/unreblog")
+ test "returns 404 error when activity does not exist", %{conn: conn} do
+ conn = post(conn, "/api/v1/statuses/foo/unreblog")
- assert json_response(conn, 400) == %{"error" => "Could not unrepeat"}
+ assert json_response(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)
- user = insert(:user)
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{activity.id}/favourite")
+ conn = post(conn, "/api/v1/statuses/#{activity.id}/favourite")
assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
json_response(conn, 200)
@@ -711,31 +693,33 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert to_string(activity.id) == id
end
- test "returns 400 error for a wrong id", %{conn: conn} do
- user = insert(:user)
+ test "favoriting twice will just return 200", %{conn: conn} do
+ activity = insert(:note_activity)
- assert capture_log(fn ->
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/1/favourite")
+ post(conn, "/api/v1/statuses/#{activity.id}/favourite")
- assert json_response(conn, 400) == %{"error" => "Could not favorite"}
- end) =~ "[error]"
+ assert post(conn, "/api/v1/statuses/#{activity.id}/favourite")
+ |> json_response(200)
+ end
+
+ test "returns 404 error for a wrong id", %{conn: conn} do
+ conn =
+ conn
+ |> post("/api/v1/statuses/1/favourite")
+
+ assert json_response(conn, 404) == %{"error" => "Record not found"}
end
end
describe "unfavoriting" do
- test "unfavorites a status and returns it", %{conn: conn} do
+ setup do: oauth_access(["write:favourites"])
+
+ test "unfavorites a status and returns it", %{user: user, conn: conn} do
activity = insert(:note_activity)
- user = insert(:user)
{:ok, _} = CommonAPI.favorite(user, activity.id)
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{activity.id}/unfavourite")
+ conn = post(conn, "/api/v1/statuses/#{activity.id}/unfavourite")
assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
json_response(conn, 200)
@@ -743,24 +727,20 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert to_string(activity.id) == id
end
- test "returns 400 error for a wrong id", %{conn: conn} do
- user = insert(:user)
+ test "returns 404 error for a wrong id", %{conn: conn} do
+ conn = post(conn, "/api/v1/statuses/1/unfavourite")
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/1/unfavourite")
-
- assert json_response(conn, 400) == %{"error" => "Could not unfavorite"}
+ assert json_response(conn, 404) == %{"error" => "Record not found"}
end
end
describe "pinned statuses" do
- setup do
- user = insert(:user)
+ setup do: oauth_access(["write:accounts"])
+
+ setup %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
- [user: user, activity: activity]
+ %{activity: activity}
end
clear_config([:instance, :max_pinned_statuses]) do
@@ -772,13 +752,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert %{"id" => ^id_str, "pinned" => true} =
conn
- |> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/pin")
|> json_response(200)
assert [%{"id" => ^id_str, "pinned" => true}] =
conn
- |> assign(:user, user)
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response(200)
end
@@ -786,19 +764,16 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
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
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{dm.id}/pin")
+ conn = post(conn, "/api/v1/statuses/#{dm.id}/pin")
assert json_response(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)
- user = refresh_record(user)
assert %{"id" => ^id_str, "pinned" => false} =
conn
@@ -808,16 +783,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert [] =
conn
- |> assign(:user, user)
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response(200)
end
- test "/unpin: returns 400 error when activity is not exist", %{conn: conn, user: user} do
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/1/unpin")
+ test "/unpin: returns 400 error when activity is not exist", %{conn: conn} do
+ conn = post(conn, "/api/v1/statuses/1/unpin")
assert json_response(conn, 400) == %{"error" => "Could not unpin"}
end
@@ -829,7 +800,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert %{"id" => ^id_str_one, "pinned" => true} =
conn
- |> assign(:user, user)
|> post("/api/v1/statuses/#{id_str_one}/pin")
|> json_response(200)
@@ -847,8 +817,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
setup do
Config.put([:rich_media, :enabled], true)
- user = insert(:user)
- %{user: user}
+ oauth_access(["read:statuses"])
end
test "returns rich-media card", %{conn: conn, user: user} do
@@ -890,7 +859,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
response_two =
conn
- |> assign(:user, user)
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response(200)
@@ -928,72 +896,55 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end
test "bookmarks" do
- user = insert(:user)
- for_user = insert(:user)
+ %{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"])
+ author = insert(:user)
{:ok, activity1} =
- CommonAPI.post(user, %{
+ CommonAPI.post(author, %{
"status" => "heweoo?"
})
{:ok, activity2} =
- CommonAPI.post(user, %{
+ CommonAPI.post(author, %{
"status" => "heweoo!"
})
- response1 =
- build_conn()
- |> assign(:user, for_user)
- |> post("/api/v1/statuses/#{activity1.id}/bookmark")
+ response1 = post(conn, "/api/v1/statuses/#{activity1.id}/bookmark")
assert json_response(response1, 200)["bookmarked"] == true
- response2 =
- build_conn()
- |> assign(:user, for_user)
- |> post("/api/v1/statuses/#{activity2.id}/bookmark")
+ response2 = post(conn, "/api/v1/statuses/#{activity2.id}/bookmark")
assert json_response(response2, 200)["bookmarked"] == true
- bookmarks =
- build_conn()
- |> assign(:user, for_user)
- |> get("/api/v1/bookmarks")
+ bookmarks = get(conn, "/api/v1/bookmarks")
assert [json_response(response2, 200), json_response(response1, 200)] ==
json_response(bookmarks, 200)
- response1 =
- build_conn()
- |> assign(:user, for_user)
- |> post("/api/v1/statuses/#{activity1.id}/unbookmark")
+ response1 = post(conn, "/api/v1/statuses/#{activity1.id}/unbookmark")
assert json_response(response1, 200)["bookmarked"] == false
- bookmarks =
- build_conn()
- |> assign(:user, for_user)
- |> get("/api/v1/bookmarks")
+ bookmarks = get(conn, "/api/v1/bookmarks")
assert [json_response(response2, 200)] == json_response(bookmarks, 200)
end
describe "conversation muting" do
+ setup do: oauth_access(["write:mutes"])
+
setup do
post_user = insert(:user)
- user = insert(:user)
-
{:ok, activity} = CommonAPI.post(post_user, %{"status" => "HIE"})
-
- [user: user, activity: activity]
+ %{activity: activity}
end
- test "mute conversation", %{conn: conn, user: user, activity: activity} do
+ test "mute conversation", %{conn: conn, activity: activity} do
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "muted" => true} =
conn
- |> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/mute")
|> json_response(200)
end
@@ -1001,10 +952,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity)
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{activity.id}/mute")
+ conn = post(conn, "/api/v1/statuses/#{activity.id}/mute")
assert json_response(conn, 400) == %{"error" => "conversation is already muted"}
end
@@ -1013,11 +961,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
{:ok, _} = CommonAPI.add_mute(user, activity)
id_str = to_string(activity.id)
- user = refresh_record(user)
assert %{"id" => ^id_str, "muted" => false} =
conn
- |> assign(:user, user)
+ # |> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unmute")
|> json_response(200)
end
@@ -1034,6 +981,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
conn1 =
conn
|> assign(:user, user2)
+ |> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"]))
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
assert %{"content" => "xD", "id" => id} = json_response(conn1, 200)
@@ -1047,6 +995,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
conn2 =
conn
|> assign(:user, user3)
+ |> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"]))
|> post("/api/v1/statuses/#{activity.id}/reblog")
assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
@@ -1058,6 +1007,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
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)
@@ -1069,15 +1019,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end
describe "GET /api/v1/statuses/:id/favourited_by" do
- setup do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
+ setup do: oauth_access(["read:accounts"])
- conn =
- build_conn()
- |> assign(:user, user)
+ setup %{user: user} do
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
- [conn: conn, activity: activity, user: user]
+ %{activity: activity}
end
test "returns users who have favorited the status", %{conn: conn, activity: activity} do
@@ -1111,26 +1058,24 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
activity: activity
} do
other_user = insert(:user)
- {:ok, user} = User.block(user, other_user)
+ {:ok, _user_relationship} = User.block(user, other_user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
conn
- |> assign(:user, user)
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response(:ok)
assert Enum.empty?(response)
end
- test "does not fail on an unauthenticated request", %{conn: conn, activity: activity} do
+ test "does not fail on an unauthenticated request", %{activity: activity} do
other_user = insert(:user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
- conn
- |> assign(:user, nil)
+ build_conn()
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response(:ok)
@@ -1138,7 +1083,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert id == other_user.id
end
- test "requires authentification for private posts", %{conn: conn, user: user} do
+ test "requires authentication for private posts", %{user: user} do
other_user = insert(:user)
{:ok, activity} =
@@ -1149,15 +1094,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
- conn
- |> assign(:user, nil)
- |> get("/api/v1/statuses/#{activity.id}/favourited_by")
+ favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by"
+
+ build_conn()
+ |> get(favourited_by_url)
|> json_response(404)
- response =
+ conn =
build_conn()
|> assign(:user, other_user)
- |> get("/api/v1/statuses/#{activity.id}/favourited_by")
+ |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
+
+ conn
+ |> assign(:token, nil)
+ |> get(favourited_by_url)
+ |> json_response(404)
+
+ response =
+ conn
+ |> get(favourited_by_url)
|> json_response(200)
[%{"id" => id}] = response
@@ -1166,15 +1121,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end
describe "GET /api/v1/statuses/:id/reblogged_by" do
- setup do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
+ setup do: oauth_access(["read:accounts"])
- conn =
- build_conn()
- |> assign(:user, user)
+ setup %{user: user} do
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
- [conn: conn, activity: activity, user: user]
+ %{activity: activity}
end
test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
@@ -1208,13 +1160,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
activity: activity
} do
other_user = insert(:user)
- {:ok, user} = User.block(user, other_user)
+ {:ok, _user_relationship} = User.block(user, other_user)
{:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
response =
conn
- |> assign(:user, user)
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(:ok)
@@ -1222,7 +1173,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end
test "does not return users who have reblogged the status privately", %{
- conn: %{assigns: %{user: user}} = conn,
+ conn: conn,
activity: activity
} do
other_user = insert(:user)
@@ -1231,20 +1182,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
response =
conn
- |> assign(:user, user)
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(:ok)
assert Enum.empty?(response)
end
- test "does not fail on an unauthenticated request", %{conn: conn, activity: activity} do
+ test "does not fail on an unauthenticated request", %{activity: activity} do
other_user = insert(:user)
{:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
response =
- conn
- |> assign(:user, nil)
+ build_conn()
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(:ok)
@@ -1252,7 +1201,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert id == other_user.id
end
- test "requires authentification for private posts", %{conn: conn, user: user} do
+ test "requires authentication for private posts", %{user: user} do
other_user = insert(:user)
{:ok, activity} =
@@ -1261,14 +1210,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
"visibility" => "direct"
})
- conn
- |> assign(:user, nil)
+ build_conn()
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(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(200)
@@ -1287,7 +1236,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
response =
build_conn()
- |> assign(:user, nil)
|> get("/api/v1/statuses/#{id3}/context")
|> json_response(:ok)
@@ -1297,8 +1245,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
} = response
end
- test "returns the favorites of a user", %{conn: conn} do
- user = insert(:user)
+ 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"})
@@ -1306,10 +1254,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
{:ok, _} = CommonAPI.favorite(user, activity.id)
- first_conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/favourites")
+ first_conn = get(conn, "/api/v1/favourites")
assert [status] = json_response(first_conn, 200)
assert status["id"] == to_string(activity.id)
@@ -1328,19 +1273,32 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
last_like = status["id"]
- second_conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/favourites?since_id=#{last_like}")
+ second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like}")
assert [second_status] = json_response(second_conn, 200)
assert second_status["id"] == to_string(second_activity.id)
- third_conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/favourites?limit=0")
+ third_conn = get(conn, "/api/v1/favourites?limit=0")
assert [] = json_response(third_conn, 200)
end
+
+ test "expires_at is nil for another user" do
+ %{conn: conn, user: user} = oauth_access(["read:statuses"])
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "foobar", "expires_in" => 1_000_000})
+
+ expires_at =
+ activity.id
+ |> ActivityExpiration.get_by_activity_id()
+ |> Map.get(:scheduled_at)
+ |> NaiveDateTime.to_iso8601()
+
+ assert %{"pleroma" => %{"expires_at" => ^expires_at}} =
+ conn |> get("/api/v1/statuses/#{activity.id}") |> json_response(:ok)
+
+ %{conn: conn} = oauth_access(["read:statuses"])
+
+ assert %{"pleroma" => %{"expires_at" => nil}} =
+ conn |> get("/api/v1/statuses/#{activity.id}") |> json_response(: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
index 7dfb02f63..987158a74 100644
--- a/test/web/mastodon_api/controllers/subscription_controller_test.exs
+++ b/test/web/mastodon_api/controllers/subscription_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do
diff --git a/test/web/mastodon_api/controllers/suggestion_controller_test.exs b/test/web/mastodon_api/controllers/suggestion_controller_test.exs
index 78620a873..c697a39f8 100644
--- a/test/web/mastodon_api/controllers/suggestion_controller_test.exs
+++ b/test/web/mastodon_api/controllers/suggestion_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do
@@ -7,12 +7,12 @@ defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do
alias Pleroma.Config
- import ExUnit.CaptureLog
import Pleroma.Factory
import Tesla.Mock
- setup do
- user = insert(:user)
+ setup do: oauth_access(["read"])
+
+ setup %{user: user} do
other_user = insert(:user)
host = Config.get([Pleroma.Web.Endpoint, :url, :host])
url500 = "http://test500?#{host}&#{user.nickname}"
@@ -32,61 +32,15 @@ defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do
}
end)
- [user: user, other_user: other_user]
+ [other_user: other_user]
end
- clear_config(:suggestions)
-
- test "returns empty result when suggestions disabled", %{conn: conn, user: user} do
- Config.put([:suggestions, :enabled], false)
-
+ test "returns empty result", %{conn: conn} do
res =
conn
- |> assign(:user, user)
|> get("/api/v1/suggestions")
|> json_response(200)
assert res == []
end
-
- test "returns error", %{conn: conn, user: user} do
- Config.put([:suggestions, :enabled], true)
- Config.put([:suggestions, :third_party_engine], "http://test500?{{host}}&{{user}}")
-
- assert capture_log(fn ->
- res =
- conn
- |> assign(:user, user)
- |> get("/api/v1/suggestions")
- |> json_response(500)
-
- assert res == "Something went wrong"
- end) =~ "Could not retrieve suggestions"
- end
-
- test "returns suggestions", %{conn: conn, user: user, other_user: other_user} do
- Config.put([:suggestions, :enabled], true)
- Config.put([:suggestions, :third_party_engine], "http://test200?{{host}}&{{user}}")
-
- res =
- conn
- |> assign(:user, user)
- |> get("/api/v1/suggestions")
- |> json_response(200)
-
- assert res == [
- %{
- "acct" => "yj455",
- "avatar" => "https://social.heldscal.la/avatar/201.jpeg",
- "avatar_static" => "https://social.heldscal.la/avatar/s/201.jpeg",
- "id" => 0
- },
- %{
- "acct" => other_user.ap_id,
- "avatar" => "https://social.heldscal.la/avatar/202.jpeg",
- "avatar_static" => "https://social.heldscal.la/avatar/s/202.jpeg",
- "id" => other_user.id
- }
- ]
- end
end
diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs
index 61b6cea75..2c03b0a75 100644
--- a/test/web/mastodon_api/controllers/timeline_controller_test.exs
+++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
@@ -20,31 +20,25 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
end
describe "home" do
- test "the home timeline", %{conn: conn} do
- user = insert(:user)
+ setup do: oauth_access(["read:statuses"])
+
+ test "the home timeline", %{user: user, conn: conn} do
following = insert(:user)
{:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/timelines/home")
+ ret_conn = get(conn, "/api/v1/timelines/home")
- assert Enum.empty?(json_response(conn, :ok))
+ assert Enum.empty?(json_response(ret_conn, :ok))
- {:ok, user} = User.follow(user, following)
+ {:ok, _user} = User.follow(user, following)
- conn =
- build_conn()
- |> assign(:user, user)
- |> get("/api/v1/timelines/home")
+ conn = get(conn, "/api/v1/timelines/home")
assert [%{"content" => "test"}] = json_response(conn, :ok)
end
- test "the home timeline when the direct messages are excluded", %{conn: conn} do
- user = insert(:user)
+ 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"})
@@ -54,10 +48,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
{:ok, private_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/timelines/home", %{"exclude_visibilities" => ["direct"]})
+ conn = get(conn, "/api/v1/timelines/home", %{"exclude_visibilities" => ["direct"]})
assert status_ids = json_response(conn, :ok) |> Enum.map(& &1["id"])
assert public_activity.id in status_ids
@@ -99,11 +90,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
end
test "the public timeline includes only public statuses for an authenticated user" do
- user = insert(:user)
-
- conn =
- build_conn()
- |> assign(:user, user)
+ %{user: user, conn: conn} = oauth_access(["read:statuses"])
{:ok, _activity} = CommonAPI.post(user, %{"status" => "test"})
{:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "private"})
@@ -134,11 +121,13 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
"visibility" => "private"
})
- # Only direct should be visible here
- res_conn =
+ conn_user_two =
conn
|> assign(:user, user_two)
- |> get("api/v1/timelines/direct")
+ |> 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")
[status] = json_response(res_conn, :ok)
@@ -149,6 +138,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
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(res_conn, :ok)
@@ -156,10 +146,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
assert %{"visibility" => "direct"} = status
# Both should be visible here
- res_conn =
- conn
- |> assign(:user, user_two)
- |> get("api/v1/timelines/home")
+ res_conn = get(conn_user_two, "api/v1/timelines/home")
[_s1, _s2] = json_response(res_conn, :ok)
@@ -172,29 +159,24 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
})
end)
- res_conn =
- conn
- |> assign(:user, user_two)
- |> get("api/v1/timelines/direct")
+ res_conn = get(conn_user_two, "api/v1/timelines/direct")
statuses = json_response(res_conn, :ok)
assert length(statuses) == 20
res_conn =
- conn
- |> assign(:user, user_two)
- |> get("api/v1/timelines/direct", %{max_id: List.last(statuses)["id"]})
+ get(conn_user_two, "api/v1/timelines/direct", %{max_id: List.last(statuses)["id"]})
[status] = json_response(res_conn, :ok)
assert status["url"] != direct.data["id"]
end
- test "doesn't include DMs from blocked users", %{conn: conn} do
- blocker = insert(:user)
+ test "doesn't include DMs from blocked users" do
+ %{user: blocker, conn: conn} = oauth_access(["read:statuses"])
blocked = insert(:user)
- user = insert(:user)
- {:ok, blocker} = User.block(blocker, blocked)
+ other_user = insert(:user)
+ {:ok, _user_relationship} = User.block(blocker, blocked)
{:ok, _blocked_direct} =
CommonAPI.post(blocked, %{
@@ -203,15 +185,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
})
{:ok, direct} =
- CommonAPI.post(user, %{
+ CommonAPI.post(other_user, %{
"status" => "Hi @#{blocker.nickname}!",
"visibility" => "direct"
})
- res_conn =
- conn
- |> assign(:user, user)
- |> get("api/v1/timelines/direct")
+ res_conn = get(conn, "api/v1/timelines/direct")
[status] = json_response(res_conn, :ok)
assert status["id"] == direct.id
@@ -219,26 +198,26 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
end
describe "list" do
- test "list timeline", %{conn: conn} do
- user = insert(:user)
+ setup do: oauth_access(["read:lists"])
+
+ 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 =
- conn
- |> assign(:user, user)
- |> get("/api/v1/timelines/list/#{list.id}")
+ conn = get(conn, "/api/v1/timelines/list/#{list.id}")
assert [%{"id" => id}] = json_response(conn, :ok)
assert id == to_string(activity_two.id)
end
- test "list timeline does not leak non-public statuses for unfollowed users", %{conn: conn} do
- user = insert(:user)
+ 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."})
@@ -251,10 +230,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/timelines/list/#{list.id}")
+ conn = get(conn, "/api/v1/timelines/list/#{list.id}")
assert [%{"id" => id}] = json_response(conn, :ok)
@@ -263,6 +239,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
end
describe "hashtag" do
+ setup do: oauth_access(["n/a"])
+
@tag capture_log: true
test "hashtag timeline", %{conn: conn} do
following = insert(:user)
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 42a8779c0..75f184242 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -1,73 +1,13 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# 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
- alias Pleroma.Notification
- alias Pleroma.Repo
- alias Pleroma.Web.CommonAPI
-
- import Pleroma.Factory
- import Tesla.Mock
-
- setup do
- mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
- :ok
- end
-
- clear_config([:rich_media, :enabled])
-
- test "unimplemented follow_requests, blocks, domain blocks" do
- user = insert(:user)
-
- ["blocks", "domain_blocks", "follow_requests"]
- |> Enum.each(fn endpoint ->
- conn =
- build_conn()
- |> assign(:user, user)
- |> get("/api/v1/#{endpoint}")
-
- assert [] = json_response(conn, 200)
- end)
- end
-
- describe "link headers" do
- test "preserves parameters in link headers", %{conn: conn} do
- user = insert(:user)
- 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", %{media_only: true})
-
- assert [link_header] = get_resp_header(conn, "link")
- assert link_header =~ ~r/media_only=true/
- assert link_header =~ ~r/min_id=#{notification2.id}/
- assert link_header =~ ~r/max_id=#{notification1.id}/
- end
- end
-
- describe "empty_array, stubs for mastodon api" do
- test "GET /api/v1/accounts/:id/identity_proofs", %{conn: conn} do
- user = insert(:user)
+ describe "empty_array/2 (stubs)" do
+ test "GET /api/v1/accounts/:id/identity_proofs" do
+ %{user: user, conn: conn} = oauth_access(["n/a"])
res =
conn
@@ -78,12 +18,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
assert res == []
end
- test "GET /api/v1/endorsements", %{conn: conn} do
- user = insert(:user)
+ test "GET /api/v1/endorsements" do
+ %{conn: conn} = oauth_access(["read:accounts"])
res =
conn
- |> assign(:user, user)
|> get("/api/v1/endorsements")
|> json_response(200)
@@ -91,11 +30,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
end
test "GET /api/v1/trends", %{conn: conn} do
- user = insert(:user)
-
res =
conn
- |> assign(:user, user)
|> get("/api/v1/trends")
|> json_response(200)
diff --git a/test/web/mastodon_api/mastodon_api_test.exs b/test/web/mastodon_api/mastodon_api_test.exs
index 561ef05aa..cb971806a 100644
--- a/test/web/mastodon_api/mastodon_api_test.exs
+++ b/test/web/mastodon_api/mastodon_api_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MastodonAPITest do
diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs
index 35aefb7dc..d60ed7b64 100644
--- a/test/web/mastodon_api/views/account_view_test.exs
+++ b/test/web/mastodon_api/views/account_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
@@ -66,6 +66,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
note: "valid html",
sensitive: false,
pleroma: %{
+ actor_type: "Person",
discoverable: false
},
fields: []
@@ -92,13 +93,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
test "Represent the user account for the account owner" do
user = insert(:user)
- notification_settings = %{
- "followers" => true,
- "follows" => true,
- "non_follows" => true,
- "non_followers" => true
- }
-
+ notification_settings = %Pleroma.User.NotificationSetting{}
privacy = user.default_scope
assert %{
@@ -112,7 +107,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
insert(:user, %{
follower_count: 3,
note_count: 5,
- source_data: %{"type" => "Service"},
+ source_data: %{},
+ actor_type: "Service",
nickname: "shp@shitposter.club",
inserted_at: ~N[2017-08-15 15:47:06.597036]
})
@@ -140,6 +136,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
note: user.bio,
sensitive: false,
pleroma: %{
+ actor_type: "Service",
discoverable: false
},
fields: []
@@ -190,9 +187,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
{:ok, user} = User.follow(user, other_user)
{:ok, other_user} = User.follow(other_user, user)
- {:ok, other_user} = User.subscribe(user, other_user)
- {:ok, user} = User.mute(user, other_user, true)
- {:ok, user} = CommonAPI.hide_reblogs(user, other_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 = %{
id: to_string(other_user.id),
@@ -218,9 +215,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
- {:ok, other_user} = User.subscribe(user, other_user)
- {:ok, user} = User.block(user, other_user)
- {:ok, other_user} = User.block(other_user, 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 = %{
id: to_string(other_user.id),
@@ -284,14 +281,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
insert(:user, %{
follower_count: 0,
note_count: 5,
- source_data: %{"type" => "Service"},
+ source_data: %{},
+ actor_type: "Service",
nickname: "shp@shitposter.club",
inserted_at: ~N[2017-08-15 15:47:06.597036]
})
other_user = insert(:user)
{:ok, other_user} = User.follow(other_user, user)
- {:ok, other_user} = User.block(other_user, user)
+ {:ok, _user_relationship} = User.block(other_user, user)
{:ok, _} = User.follow(insert(:user), user)
expected = %{
@@ -317,6 +315,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
note: user.bio,
sensitive: false,
pleroma: %{
+ actor_type: "Service",
discoverable: false
},
fields: []
@@ -369,10 +368,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
assert result.pleroma[:settings_store] == nil
end
- test "sanitizes display names" do
+ test "doesn't sanitize display names" do
user = insert(:user, name: "<marquee> username </marquee>")
result = AccountView.render("show.json", %{user: user})
- refute result.display_name == "<marquee> username </marquee>"
+ assert result.display_name == "<marquee> username </marquee>"
end
test "never display nil user follow counts" do
diff --git a/test/web/mastodon_api/views/conversation_view_test.exs b/test/web/mastodon_api/views/conversation_view_test.exs
index 6ed22597d..dbf3c51e2 100644
--- a/test/web/mastodon_api/views/conversation_view_test.exs
+++ b/test/web/mastodon_api/views/conversation_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ConversationViewTest do
diff --git a/test/web/mastodon_api/views/list_view_test.exs b/test/web/mastodon_api/views/list_view_test.exs
index 59e896a7c..ca99242cb 100644
--- a/test/web/mastodon_api/views/list_view_test.exs
+++ b/test/web/mastodon_api/views/list_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ListViewTest do
diff --git a/test/web/mastodon_api/views/marker_view_test.exs b/test/web/mastodon_api/views/marker_view_test.exs
index 8a5c89d56..893cf8857 100644
--- a/test/web/mastodon_api/views/marker_view_test.exs
+++ b/test/web/mastodon_api/views/marker_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do
diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs
index a741cc014..779126556 100644
--- a/test/web/mastodon_api/views/notification_view_test.exs
+++ b/test/web/mastodon_api/views/notification_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
@@ -109,8 +109,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
end
test "Move 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])
+ old_user = insert(:user)
+ new_user = insert(:user, also_known_as: [old_user.ap_id])
follower = insert(:user)
User.follow(follower, old_user)
@@ -120,7 +120,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
old_user = refresh_record(old_user)
new_user = refresh_record(new_user)
- [notification] = Notification.for_user(follower)
+ [notification] = Notification.for_user(follower, %{with_move: true})
expected = %{
id: to_string(notification.id),
@@ -134,4 +134,31 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
assert [expected] ==
NotificationView.render("index.json", %{notifications: [notification], for: follower})
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},
+ 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)
+ }
+
+ assert expected ==
+ NotificationView.render("show.json", %{notification: notification, for: user})
+ end
end
diff --git a/test/web/mastodon_api/views/poll_view_test.exs b/test/web/mastodon_api/views/poll_view_test.exs
index 8cd7636a5..6211fa888 100644
--- a/test/web/mastodon_api/views/poll_view_test.exs
+++ b/test/web/mastodon_api/views/poll_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.PollViewTest do
diff --git a/test/web/mastodon_api/views/push_subscription_view_test.exs b/test/web/mastodon_api/views/push_subscription_view_test.exs
index 4e4f5b7e6..10c6082a5 100644
--- a/test/web/mastodon_api/views/push_subscription_view_test.exs
+++ b/test/web/mastodon_api/views/push_subscription_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.PushSubscriptionViewTest do
diff --git a/test/web/mastodon_api/views/scheduled_activity_view_test.exs b/test/web/mastodon_api/views/scheduled_activity_view_test.exs
index 6387e4555..0c0987593 100644
--- a/test/web/mastodon_api/views/scheduled_activity_view_test.exs
+++ b/test/web/mastodon_api/views/scheduled_activity_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ScheduledActivityViewTest do
diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs
index d46ecc646..191895c6f 100644
--- a/test/web/mastodon_api/views/status_view_test.exs
+++ b/test/web/mastodon_api/views/status_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
@@ -24,6 +24,31 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
: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 status[:pleroma][:emoji_reactions] == [
+ %{name: "☕", count: 2, me: false},
+ %{name: "🍵", count: 1, me: false}
+ ]
+
+ status = StatusView.render("show.json", activity: activity, for: user)
+
+ assert status[:pleroma][:emoji_reactions] == [
+ %{name: "☕", count: 2, me: true},
+ %{name: "🍵", count: 1, me: false}
+ ]
+ end
+
test "loads and returns the direct conversation id when given the `with_direct_conversation_id` option" do
user = insert(:user)
@@ -172,7 +197,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])},
expires_at: nil,
direct_conversation_id: nil,
- thread_muted: false
+ thread_muted: false,
+ emoji_reactions: []
}
}
@@ -183,7 +209,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
user = insert(:user)
other_user = insert(:user)
- {:ok, user} = User.mute(user, other_user)
+ {:ok, _user_relationships} = User.mute(user, other_user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
status = StatusView.render("show.json", %{activity: activity})
@@ -199,7 +225,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
user = insert(:user)
other_user = insert(:user)
- {:ok, user} = User.mute(user, other_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})
@@ -394,6 +420,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
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)
+ end
+
describe "build_tags/1" do
test "it returns a a dictionary tags" do
object_tags = [
@@ -450,7 +491,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
title: "Example website"
}
- %{provider_name: "Example site name"} =
+ %{provider_name: "example.com"} =
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
end
@@ -465,7 +506,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
description: "Example description"
}
- %{provider_name: "Example site name"} =
+ %{provider_name: "example.com"} =
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
end
end
diff --git a/test/web/media_proxy/media_proxy_controller_test.exs b/test/web/media_proxy/media_proxy_controller_test.exs
index fdfdb5ec6..7ac7e4af1 100644
--- a/test/web/media_proxy/media_proxy_controller_test.exs
+++ b/test/web/media_proxy/media_proxy_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
@@ -7,11 +7,8 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
import Mock
alias Pleroma.Config
- setup do
- media_proxy_config = Config.get([:media_proxy]) || []
- on_exit(fn -> Config.put([:media_proxy], media_proxy_config) end)
- :ok
- end
+ clear_config(:media_proxy)
+ clear_config([Pleroma.Web.Endpoint, :secret_key_base])
test "it returns 404 when MediaProxy disabled", %{conn: conn} do
Config.put([:media_proxy, :enabled], false)
@@ -55,9 +52,8 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
url = Pleroma.Web.MediaProxy.encode_url("https://google.fn/test.png")
invalid_url = String.replace(url, "test.png", "test-file.png")
response = get(conn, invalid_url)
- html = "<html><body>You are being <a href=\"#{url}\">redirected</a>.</body></html>"
assert response.status == 302
- assert response.resp_body == html
+ assert redirected_to(response) == url
end
test "it performs ReverseProxy.call when signature valid", %{conn: conn} do
diff --git a/test/web/media_proxy/media_proxy_test.exs b/test/web/media_proxy/media_proxy_test.exs
index 96bdde219..8f5fcf2eb 100644
--- a/test/web/media_proxy/media_proxy_test.exs
+++ b/test/web/media_proxy/media_proxy_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MediaProxyTest do
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.MediaProxyTest do
alias Pleroma.Web.MediaProxy.MediaProxyController
clear_config([:media_proxy, :enabled])
+ clear_config(Pleroma.Upload)
describe "when enabled" do
setup do
@@ -224,7 +225,6 @@ defmodule Pleroma.Web.MediaProxyTest do
end
test "ensure Pleroma.Upload base_url is always whitelisted" do
- upload_config = Pleroma.Config.get([Pleroma.Upload])
media_url = "https://media.pleroma.social"
Pleroma.Config.put([Pleroma.Upload, :base_url], media_url)
@@ -232,8 +232,6 @@ defmodule Pleroma.Web.MediaProxyTest do
encoded = url(url)
assert String.starts_with?(encoded, media_url)
-
- Pleroma.Config.put([Pleroma.Upload], upload_config)
end
end
end
diff --git a/test/web/metadata/feed_test.exs b/test/web/metadata/feed_test.exs
index 50e9ce52e..e6e5cc5ed 100644
--- a/test/web/metadata/feed_test.exs
+++ b/test/web/metadata/feed_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.FeedTest do
diff --git a/test/web/metadata/opengraph_test.exs b/test/web/metadata/opengraph_test.exs
index 4283f72cd..9d7c009eb 100644
--- a/test/web/metadata/opengraph_test.exs
+++ b/test/web/metadata/opengraph_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.OpenGraphTest do
@@ -7,6 +7,8 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraphTest do
import Pleroma.Factory
alias Pleroma.Web.Metadata.Providers.OpenGraph
+ clear_config([Pleroma.Web.Metadata, :unfurl_nsfw])
+
test "it renders all supported types of attachments and skips unknown types" do
user = insert(:user)
diff --git a/test/web/metadata/player_view_test.exs b/test/web/metadata/player_view_test.exs
index 742b0ed8b..e6c990242 100644
--- a/test/web/metadata/player_view_test.exs
+++ b/test/web/metadata/player_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.PlayerViewTest do
diff --git a/test/web/metadata/rel_me_test.exs b/test/web/metadata/rel_me_test.exs
index 3874e077b..4107a8459 100644
--- a/test/web/metadata/rel_me_test.exs
+++ b/test/web/metadata/rel_me_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.RelMeTest do
diff --git a/test/web/metadata/twitter_card_test.exs b/test/web/metadata/twitter_card_test.exs
index 0814006d2..3d75d1ed5 100644
--- a/test/web/metadata/twitter_card_test.exs
+++ b/test/web/metadata/twitter_card_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do
@@ -13,6 +13,8 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do
alias Pleroma.Web.Metadata.Utils
alias Pleroma.Web.Router
+ 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))
@@ -26,7 +28,32 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do
]
end
- test "it does not render attachments if post is nsfw" do
+ 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"})
@@ -67,7 +94,7 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do
{: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_large_image"], []}
+ {:meta, [property: "twitter:card", content: "summary"], []}
] == result
end
diff --git a/test/web/metadata/utils_test.exs b/test/web/metadata/utils_test.exs
new file mode 100644
index 000000000..8183256d8
--- /dev/null
+++ b/test/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/web/mongooseim/mongoose_im_controller_test.exs b/test/web/mongooseim/mongoose_im_controller_test.exs
index eb83999bb..291ae54fc 100644
--- a/test/web/mongooseim/mongoose_im_controller_test.exs
+++ b/test/web/mongooseim/mongoose_im_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MongooseIMController do
diff --git a/test/web/node_info_test.exs b/test/web/node_info_test.exs
index 9a574a38d..ee10ad5db 100644
--- a/test/web/node_info_test.exs
+++ b/test/web/node_info_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.NodeInfoTest do
@@ -7,6 +7,9 @@ defmodule Pleroma.Web.NodeInfoTest do
import Pleroma.Factory
+ clear_config([:mrf_simple])
+ clear_config(:instance)
+
test "GET /.well-known/nodeinfo", %{conn: conn} do
links =
conn
@@ -62,11 +65,6 @@ defmodule Pleroma.Web.NodeInfoTest do
end
test "returns fieldsLimits field", %{conn: conn} do
- max_account_fields = Pleroma.Config.get([:instance, :max_account_fields])
- max_remote_account_fields = Pleroma.Config.get([:instance, :max_remote_account_fields])
- account_field_name_length = Pleroma.Config.get([:instance, :account_field_name_length])
- account_field_value_length = Pleroma.Config.get([:instance, :account_field_value_length])
-
Pleroma.Config.put([:instance, :max_account_fields], 10)
Pleroma.Config.put([:instance, :max_remote_account_fields], 15)
Pleroma.Config.put([:instance, :account_field_name_length], 255)
@@ -81,11 +79,6 @@ defmodule Pleroma.Web.NodeInfoTest do
assert response["metadata"]["fieldsLimits"]["maxRemoteFields"] == 15
assert response["metadata"]["fieldsLimits"]["nameLength"] == 255
assert response["metadata"]["fieldsLimits"]["valueLength"] == 2048
-
- Pleroma.Config.put([:instance, :max_account_fields], max_account_fields)
- Pleroma.Config.put([:instance, :max_remote_account_fields], max_remote_account_fields)
- Pleroma.Config.put([:instance, :account_field_name_length], account_field_name_length)
- Pleroma.Config.put([:instance, :account_field_value_length], account_field_value_length)
end
test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do
@@ -111,28 +104,28 @@ defmodule Pleroma.Web.NodeInfoTest do
Pleroma.Config.put([:instance, :safe_dm_mentions], option)
end
- test "it shows if federation is enabled/disabled", %{conn: conn} do
- original = Pleroma.Config.get([:instance, :federating])
+ describe "`metadata/federation/enabled`" do
+ clear_config([:instance, :federating])
- Pleroma.Config.put([:instance, :federating], true)
-
- response =
- conn
- |> get("/nodeinfo/2.1.json")
- |> json_response(:ok)
+ test "it shows if federation is enabled/disabled", %{conn: conn} do
+ Pleroma.Config.put([:instance, :federating], true)
- assert response["metadata"]["federation"]["enabled"] == true
+ response =
+ conn
+ |> get("/nodeinfo/2.1.json")
+ |> json_response(:ok)
- Pleroma.Config.put([:instance, :federating], false)
+ assert response["metadata"]["federation"]["enabled"] == true
- response =
- conn
- |> get("/nodeinfo/2.1.json")
- |> json_response(:ok)
+ Pleroma.Config.put([:instance, :federating], false)
- assert response["metadata"]["federation"]["enabled"] == false
+ response =
+ conn
+ |> get("/nodeinfo/2.1.json")
+ |> json_response(:ok)
- Pleroma.Config.put([:instance, :federating], original)
+ assert response["metadata"]["federation"]["enabled"] == false
+ end
end
test "it shows MRF transparency data if enabled", %{conn: conn} do
diff --git a/test/web/oauth/app_test.exs b/test/web/oauth/app_test.exs
index 195b8c17f..899af648e 100644
--- a/test/web/oauth/app_test.exs
+++ b/test/web/oauth/app_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.AppTest do
diff --git a/test/web/oauth/authorization_test.exs b/test/web/oauth/authorization_test.exs
index 2e82a7b79..d74b26cf8 100644
--- a/test/web/oauth/authorization_test.exs
+++ b/test/web/oauth/authorization_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.AuthorizationTest do
diff --git a/test/web/oauth/ldap_authorization_test.exs b/test/web/oauth/ldap_authorization_test.exs
index 1cbe133b7..c55b0ffc5 100644
--- a/test/web/oauth/ldap_authorization_test.exs
+++ b/test/web/oauth/ldap_authorization_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do
diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs
index beb995cd8..cff469c28 100644
--- a/test/web/oauth/oauth_controller_test.exs
+++ b/test/web/oauth/oauth_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.OAuthControllerTest do
@@ -17,7 +17,8 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
key: "_test",
signing_salt: "cooldude"
]
- clear_config_all([:instance, :account_activation_required])
+
+ clear_config([:instance, :account_activation_required])
describe "in OAuth consumer mode, " do
setup do
@@ -450,7 +451,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
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_id: app.id)
+ token = insert(:oauth_token, app: app)
conn =
conn
@@ -474,7 +475,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
app: app,
conn: conn
} do
- token = insert(:oauth_token, app_id: app.id)
+ token = insert(:oauth_token, app: app)
conn =
conn
@@ -497,7 +498,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
app: app,
conn: conn
} do
- token = insert(:oauth_token, app_id: app.id)
+ token = insert(:oauth_token, app: app)
conn =
conn
@@ -523,7 +524,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
conn: conn
} do
unlisted_redirect_uri = "http://cross-site-request.com"
- token = insert(:oauth_token, app_id: app.id)
+ token = insert(:oauth_token, app: app)
conn =
conn
@@ -547,7 +548,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
app: app,
conn: conn
} do
- token = insert(:oauth_token, app_id: app.id)
+ token = insert(:oauth_token, app: app)
conn =
conn
@@ -567,33 +568,46 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
end
describe "POST /oauth/authorize" do
- test "redirects with oauth authorization" do
- user = insert(:user)
- app = insert(:oauth_app, scopes: ["read", "write", "follow"])
+ 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)
- conn =
- build_conn()
- |> post("/oauth/authorize", %{
- "authorization" => %{
- "name" => user.nickname,
- "password" => "test",
- "client_id" => app.client_id,
- "redirect_uri" => redirect_uri,
- "scope" => "read:subscope write",
- "state" => "statepassed"
- }
- })
+ non_admin = insert(:user, is_admin: false)
+ admin = insert(:user, is_admin: true)
+ scopes_subset = ["read:subscope", "write", "admin"]
- target = redirected_to(conn)
- assert target =~ redirect_uri
+ # 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()
+ 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 == ["read:subscope", "write"]
+ assert %{"state" => "statepassed", "code" => code} = query
+ auth = Repo.get_by(Authorization, token: code)
+ assert auth
+ assert auth.scopes == expected_scopes
+ end
end
test "returns 401 for wrong credentials", %{conn: conn} do
@@ -623,13 +637,13 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
assert result =~ "Invalid Username/Password"
end
- test "returns 401 for missing scopes", %{conn: conn} do
- user = insert(:user)
- app = insert(:oauth_app)
+ 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 =
- conn
+ build_conn()
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
@@ -806,7 +820,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
|> User.confirmation_changeset(need_confirmation: true)
|> User.update_and_set_cache()
- refute Pleroma.User.auth_active?(user)
+ refute Pleroma.User.account_status(user) == :active
app = insert(:oauth_app)
@@ -836,7 +850,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
app = insert(:oauth_app)
- conn =
+ resp =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
@@ -845,10 +859,12 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
+ |> json_response(403)
- assert resp = json_response(conn, 403)
- assert %{"error" => _} = resp
- refute Map.has_key?(resp, "access_token")
+ 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
@@ -862,7 +878,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
app = insert(:oauth_app, scopes: ["read", "write"])
- conn =
+ resp =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
@@ -871,12 +887,41 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
+ |> json_response(403)
- assert resp = json_response(conn, 403)
+ assert resp == %{
+ "error" => "Password reset is required",
+ "identifier" => "password_reset_required"
+ }
+ end
- assert resp["error"] == "Password reset is required"
- assert resp["identifier"] == "password_reset_required"
- refute Map.has_key?(resp, "access_token")
+ 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: Comeonin.Pbkdf2.hashpwsalt(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 an invalid authorization code" do
diff --git a/test/web/oauth/token/utils_test.exs b/test/web/oauth/token/utils_test.exs
index dc1f9a986..a610d92f8 100644
--- a/test/web/oauth/token/utils_test.exs
+++ b/test/web/oauth/token/utils_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token.UtilsTest do
diff --git a/test/web/oauth/token_test.exs b/test/web/oauth/token_test.exs
index 5359940f8..40d71eb59 100644
--- a/test/web/oauth/token_test.exs
+++ b/test/web/oauth/token_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.TokenTest do
diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs
index 567aabbf1..ae99e37fe 100644
--- a/test/web/ostatus/ostatus_controller_test.exs
+++ b/test/web/ostatus/ostatus_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.OStatusControllerTest do
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
import Pleroma.Factory
+ alias Pleroma.Config
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.CommonAPI
@@ -16,22 +17,24 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
:ok
end
- clear_config_all([:instance, :federating]) do
- Pleroma.Config.put([:instance, :federating], true)
+ clear_config([:instance, :federating]) do
+ Config.put([:instance, :federating], true)
end
- describe "GET object/2" do
+ # 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 =
- conn
- |> put_req_header("accept", "text/html")
- |> get(url)
-
+ conn = get(conn, url)
assert redirected_to(conn) == "/notice/#{note_activity.id}"
end
@@ -45,23 +48,25 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
|> response(404)
end
- test "404s on nonexisting objects", %{conn: conn} do
+ test "404s on non-existing objects", %{conn: conn} do
conn
|> get("/objects/123")
|> response(404)
end
end
- describe "GET activity/2" do
+ # 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 =
- conn
- |> put_req_header("accept", "text/html")
- |> get("/activities/#{uuid}")
-
+ conn = get(conn, "/activities/#{uuid}")
assert redirected_to(conn) == "/notice/#{note_activity.id}"
end
@@ -79,19 +84,6 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
|> get("/activities/123")
|> response(404)
end
-
- test "gets an activity in AS2 format", %{conn: conn} do
- note_activity = insert(:note_activity)
- [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
- url = "/activities/#{uuid}"
-
- conn =
- conn
- |> put_req_header("accept", "application/activity+json")
- |> get(url)
-
- assert json_response(conn, 200)
- end
end
describe "GET notice/2" do
@@ -170,7 +162,7 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
assert response(conn, 404)
end
- test "404s a nonexisting notice", %{conn: conn} do
+ test "404s a non-existing notice", %{conn: conn} do
url = "/notice/123"
conn =
@@ -179,10 +171,21 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
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
- test "render embed player", %{conn: conn} do
+ setup do
note_activity = insert(:note_activity)
object = Pleroma.Object.normalize(note_activity)
@@ -204,9 +207,11 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
|> Ecto.Changeset.change(data: object_data)
|> Pleroma.Repo.update()
- conn =
- conn
- |> get("/notice/#{note_activity.id}/embed_player")
+ %{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"]
@@ -272,9 +277,19 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
|> Ecto.Changeset.change(data: object_data)
|> Pleroma.Repo.update()
- assert conn
- |> get("/notice/#{note_activity.id}/embed_player")
- |> response(404)
+ 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
index c9f67c280..3853a9bbb 100644
--- a/test/web/pleroma_api/controllers/account_controller_test.exs
+++ b/test/web/pleroma_api/controllers/account_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
@@ -33,7 +33,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
test "resend account confirmation email", %{conn: conn, user: user} do
conn
- |> assign(:user, user)
|> post("/api/v1/pleroma/accounts/confirmation_resend?email=#{user.email}")
|> json_response(:no_content)
@@ -52,14 +51,12 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
end
describe "PATCH /api/v1/pleroma/accounts/update_avatar" do
- test "user avatar can be set", %{conn: conn} do
- user = insert(:user)
+ setup do: oauth_access(["write:accounts"])
+
+ test "user avatar can be set", %{user: user, conn: conn} do
avatar_image = File.read!("test/fixtures/avatar_data_uri")
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/pleroma/accounts/update_avatar", %{img: avatar_image})
+ conn = patch(conn, "/api/v1/pleroma/accounts/update_avatar", %{img: avatar_image})
user = refresh_record(user)
@@ -78,13 +75,8 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
assert %{"url" => _} = json_response(conn, 200)
end
- test "user avatar can be reset", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/pleroma/accounts/update_avatar", %{img: ""})
+ test "user avatar can be reset", %{user: user, conn: conn} do
+ conn = patch(conn, "/api/v1/pleroma/accounts/update_avatar", %{img: ""})
user = User.get_cached_by_id(user.id)
@@ -95,13 +87,10 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
end
describe "PATCH /api/v1/pleroma/accounts/update_banner" do
- test "can set profile banner", %{conn: conn} do
- user = insert(:user)
+ setup do: oauth_access(["write:accounts"])
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => @image})
+ test "can set profile banner", %{user: user, conn: conn} do
+ conn = patch(conn, "/api/v1/pleroma/accounts/update_banner", %{"banner" => @image})
user = refresh_record(user)
assert user.banner["type"] == "Image"
@@ -109,13 +98,8 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
assert %{"url" => _} = json_response(conn, 200)
end
- test "can reset profile banner", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => ""})
+ test "can reset profile banner", %{user: user, conn: conn} do
+ conn = patch(conn, "/api/v1/pleroma/accounts/update_banner", %{"banner" => ""})
user = refresh_record(user)
assert user.banner == %{}
@@ -125,26 +109,18 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
end
describe "PATCH /api/v1/pleroma/accounts/update_background" do
- test "background image can be set", %{conn: conn} do
- user = insert(:user)
+ setup do: oauth_access(["write:accounts"])
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/pleroma/accounts/update_background", %{"img" => @image})
+ test "background image can be set", %{user: user, conn: conn} do
+ conn = patch(conn, "/api/v1/pleroma/accounts/update_background", %{"img" => @image})
user = refresh_record(user)
assert user.background["type"] == "Image"
assert %{"url" => _} = json_response(conn, 200)
end
- test "background image can be reset", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/pleroma/accounts/update_background", %{"img" => ""})
+ test "background image can be reset", %{user: user, conn: conn} do
+ conn = patch(conn, "/api/v1/pleroma/accounts/update_background", %{"img" => ""})
user = refresh_record(user)
assert user.background == %{}
@@ -155,12 +131,12 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
describe "getting favorites timeline of specified user" do
setup do
[current_user, user] = insert_pair(:user, hide_favorites: false)
- [current_user: current_user, user: user]
+ %{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,
- current_user: current_user,
user: user
} do
[activity | _] = insert_pair(:note_activity)
@@ -168,7 +144,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
response =
conn
- |> assign(:user, current_user)
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response(:ok)
@@ -178,23 +153,18 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
assert like["id"] == activity.id
end
- test "returns favorites for specified user_id when user is not logged in", %{
- conn: conn,
+ test "does not return favorites for specified user_id when user is not logged in", %{
user: user
} do
activity = insert(:note_activity)
CommonAPI.favorite(user, activity.id)
- response =
- conn
- |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
- |> json_response(:ok)
-
- assert length(response) == 1
+ build_conn()
+ |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+ |> json_response(403)
end
test "returns favorited DM only when user is logged in and he is one of recipients", %{
- conn: conn,
current_user: current_user,
user: user
} do
@@ -206,25 +176,24 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
CommonAPI.favorite(user, direct.id)
- response =
- conn
- |> assign(:user, current_user)
- |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
- |> json_response(:ok)
+ 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(:ok)
- assert length(response) == 1
+ assert length(response) == 1
+ end
- anonymous_response =
- conn
- |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
- |> json_response(:ok)
-
- assert Enum.empty?(anonymous_response)
+ build_conn()
+ |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+ |> json_response(403)
end
test "does not return others' favorited DM when user is not one of recipients", %{
conn: conn,
- current_user: current_user,
user: user
} do
user_two = insert(:user)
@@ -239,7 +208,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
response =
conn
- |> assign(:user, current_user)
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response(:ok)
@@ -248,7 +216,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
test "paginates favorites using since_id and max_id", %{
conn: conn,
- current_user: current_user,
user: user
} do
activities = insert_list(10, :note_activity)
@@ -262,7 +229,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
response =
conn
- |> assign(:user, current_user)
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites", %{
since_id: third_activity.id,
max_id: seventh_activity.id
@@ -276,7 +242,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
test "limits favorites using limit parameter", %{
conn: conn,
- current_user: current_user,
user: user
} do
7
@@ -287,7 +252,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
response =
conn
- |> assign(:user, current_user)
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites", %{limit: "3"})
|> json_response(:ok)
@@ -296,12 +260,10 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
test "returns empty response when user does not have any favorited statuses", %{
conn: conn,
- current_user: current_user,
user: user
} do
response =
conn
- |> assign(:user, current_user)
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response(:ok)
@@ -314,79 +276,61 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
assert json_response(conn, 404) == %{"error" => "Record not found"}
end
- test "returns 403 error when user has hidden own favorites", %{
- conn: conn,
- current_user: current_user
- } do
+ 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 =
- conn
- |> assign(:user, current_user)
- |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+ conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites")
assert json_response(conn, 403) == %{"error" => "Can't get favorites"}
end
- test "hides favorites for new users by default", %{conn: conn, current_user: current_user} do
+ test "hides favorites for new users by default", %{conn: conn} do
user = insert(:user)
activity = insert(:note_activity)
CommonAPI.favorite(user, activity.id)
- conn =
- conn
- |> assign(:user, current_user)
- |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
-
assert user.hide_favorites
+ conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites")
+
assert json_response(conn, 403) == %{"error" => "Can't get favorites"}
end
end
describe "subscribing / unsubscribing" do
- test "subscribing / unsubscribing to a user", %{conn: conn} do
- user = insert(:user)
+ test "subscribing / unsubscribing to a user" do
+ %{user: user, conn: conn} = oauth_access(["follow"])
subscription_target = insert(:user)
- conn =
+ ret_conn =
conn
|> assign(:user, user)
|> post("/api/v1/pleroma/accounts/#{subscription_target.id}/subscribe")
- assert %{"id" => _id, "subscribing" => true} = json_response(conn, 200)
+ assert %{"id" => _id, "subscribing" => true} = json_response(ret_conn, 200)
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/v1/pleroma/accounts/#{subscription_target.id}/unsubscribe")
+ conn = post(conn, "/api/v1/pleroma/accounts/#{subscription_target.id}/unsubscribe")
assert %{"id" => _id, "subscribing" => false} = json_response(conn, 200)
end
end
describe "subscribing" do
- test "returns 404 when subscription_target not found", %{conn: conn} do
- user = insert(:user)
+ test "returns 404 when subscription_target not found" do
+ %{conn: conn} = oauth_access(["write:follows"])
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/pleroma/accounts/target_id/subscribe")
+ conn = post(conn, "/api/v1/pleroma/accounts/target_id/subscribe")
assert %{"error" => "Record not found"} = json_response(conn, 404)
end
end
describe "unsubscribing" do
- test "returns 404 when subscription_target not found", %{conn: conn} do
- user = insert(:user)
+ test "returns 404 when subscription_target not found" do
+ %{conn: conn} = oauth_access(["follow"])
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/pleroma/accounts/target_id/unsubscribe")
+ conn = post(conn, "/api/v1/pleroma/accounts/target_id/unsubscribe")
assert %{"error" => "Record not found"} = json_response(conn, 404)
end
diff --git a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
index 3d3becefd..4b9f5cf9a 100644
--- a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
+++ b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
@@ -1,12 +1,11 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
use Pleroma.Web.ConnCase
import Tesla.Mock
-
import Pleroma.Factory
@emoji_dir_path Path.join(
@@ -14,6 +13,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
"emoji"
)
+ clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
+ Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
+ end
+
test "shared & non-shared pack information in list_packs is ok" do
conn = build_conn()
resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
@@ -39,9 +42,12 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
test "listing remote packs" do
admin = insert(:user, is_admin: true)
- conn = build_conn() |> assign(:user, admin)
+ %{conn: conn} = oauth_access(["admin:write"], user: admin)
- resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
+ resp =
+ build_conn()
+ |> get(emoji_api_path(conn, :list_packs))
+ |> json_response(200)
mock(fn
%{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
@@ -123,7 +129,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
admin = insert(:user, is_admin: true)
- conn = build_conn() |> assign(:user, admin)
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> assign(:token, insert(:oauth_admin_token, user: admin, scopes: ["admin:write"]))
assert (conn
|> put_req_header("content-type", "application/json")
@@ -168,8 +177,6 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
# non-shared, downloaded from the fallback URL
- conn = build_conn() |> assign(:user, admin)
-
assert conn
|> put_req_header("content-type", "application/json")
|> post(
@@ -205,8 +212,12 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
File.write!(pack_file, original_content)
end)
+ admin = insert(:user, is_admin: true)
+ %{conn: conn} = oauth_access(["admin:write"], user: admin)
+
{:ok,
- admin: insert(:user, is_admin: true),
+ admin: admin,
+ conn: conn,
pack_file: pack_file,
new_data: %{
"license" => "Test license changed",
@@ -217,10 +228,9 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
end
test "for a pack without a fallback source", ctx do
- conn = build_conn()
+ conn = ctx[:conn]
assert conn
- |> assign(:user, ctx[:admin])
|> post(
emoji_api_path(conn, :update_metadata, "test_pack"),
%{
@@ -250,10 +260,9 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
"74409E2674DAA06C072729C6C8426C4CB3B7E0B85ED77792DB7A436E11D76DAF"
)
- conn = build_conn()
+ conn = ctx[:conn]
assert conn
- |> assign(:user, ctx[:admin])
|> post(
emoji_api_path(conn, :update_metadata, "test_pack"),
%{
@@ -277,10 +286,9 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack")
- conn = build_conn()
+ conn = ctx[:conn]
assert (conn
- |> assign(:user, ctx[:admin])
|> post(
emoji_api_path(conn, :update_metadata, "test_pack"),
%{
@@ -304,8 +312,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
end)
admin = insert(:user, is_admin: true)
-
- conn = build_conn()
+ %{conn: conn} = oauth_access(["admin:write"], user: admin)
same_name = %{
"action" => "add",
@@ -319,8 +326,6 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
different_name = %{same_name | "shortcode" => "blank_2"}
- conn = conn |> assign(:user, admin)
-
assert (conn
|> post(emoji_api_path(conn, :update_file, "test_pack"), same_name)
|> json_response(:conflict))["error"] =~ "already exists"
@@ -392,8 +397,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
end)
admin = insert(:user, is_admin: true)
-
- conn = build_conn() |> assign(:user, admin)
+ %{conn: conn} = oauth_access(["admin:write"], user: admin)
assert conn
|> put_req_header("content-type", "application/json")
@@ -432,9 +436,9 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
refute Map.has_key?(resp, "test_pack_for_import")
admin = insert(:user, is_admin: true)
+ %{conn: conn} = oauth_access(["admin:write"], user: admin)
assert conn
- |> assign(:user, admin)
|> post(emoji_api_path(conn, :import_from_fs))
|> json_response(200) == ["test_pack_for_import"]
@@ -449,11 +453,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
File.write!("#{@emoji_dir_path}/test_pack_for_import/emoji.txt", emoji_txt_content)
assert conn
- |> assign(:user, admin)
|> post(emoji_api_path(conn, :import_from_fs))
|> json_response(200) == ["test_pack_for_import"]
- resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
+ resp = build_conn() |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
assert resp["test_pack_for_import"]["files"] == %{
"blank" => "blank.png",
diff --git a/test/web/pleroma_api/controllers/mascot_controller_test.exs b/test/web/pleroma_api/controllers/mascot_controller_test.exs
index ae9539b04..617831b02 100644
--- a/test/web/pleroma_api/controllers/mascot_controller_test.exs
+++ b/test/web/pleroma_api/controllers/mascot_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.MascotControllerTest do
@@ -7,10 +7,8 @@ defmodule Pleroma.Web.PleromaAPI.MascotControllerTest do
alias Pleroma.User
- import Pleroma.Factory
-
- test "mascot upload", %{conn: conn} do
- user = insert(:user)
+ test "mascot upload" do
+ %{conn: conn} = oauth_access(["write:accounts"])
non_image_file = %Plug.Upload{
content_type: "audio/mpeg",
@@ -18,12 +16,9 @@ defmodule Pleroma.Web.PleromaAPI.MascotControllerTest do
filename: "sound.mp3"
}
- conn =
- conn
- |> assign(:user, user)
- |> put("/api/v1/pleroma/mascot", %{"file" => non_image_file})
+ ret_conn = put(conn, "/api/v1/pleroma/mascot", %{"file" => non_image_file})
- assert json_response(conn, 415)
+ assert json_response(ret_conn, 415)
file = %Plug.Upload{
content_type: "image/jpg",
@@ -31,23 +26,18 @@ defmodule Pleroma.Web.PleromaAPI.MascotControllerTest do
filename: "an_image.jpg"
}
- conn =
- build_conn()
- |> assign(:user, user)
- |> put("/api/v1/pleroma/mascot", %{"file" => file})
+ conn = put(conn, "/api/v1/pleroma/mascot", %{"file" => file})
assert %{"id" => _, "type" => image} = json_response(conn, 200)
end
- test "mascot retrieving", %{conn: conn} do
- user = insert(:user)
+ 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
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/pleroma/mascot")
+ ret_conn = get(conn, "/api/v1/pleroma/mascot")
- assert %{"url" => url} = json_response(conn, 200)
+ assert %{"url" => url} = json_response(ret_conn, 200)
assert url =~ "pleroma-fox-tan-smol"
# When a user sets their mascot, we should get that back
@@ -57,17 +47,14 @@ defmodule Pleroma.Web.PleromaAPI.MascotControllerTest do
filename: "an_image.jpg"
}
- conn =
- build_conn()
- |> assign(:user, user)
- |> put("/api/v1/pleroma/mascot", %{"file" => file})
+ ret_conn = put(conn, "/api/v1/pleroma/mascot", %{"file" => file})
- assert json_response(conn, 200)
+ assert json_response(ret_conn, 200)
user = User.get_cached_by_id(user.id)
conn =
- build_conn()
+ conn
|> assign(:user, user)
|> get("/api/v1/pleroma/mascot")
diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
index b1b59beed..32250f06f 100644
--- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
+++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
@@ -14,7 +14,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
import Pleroma.Factory
- test "POST /api/v1/pleroma/statuses/:id/react_with_emoji", %{conn: conn} do
+ test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
@@ -23,13 +23,20 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
result =
conn
|> assign(:user, other_user)
- |> post("/api/v1/pleroma/statuses/#{activity.id}/react_with_emoji", %{"emoji" => "☕"})
+ |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
+ |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕")
+ |> json_response(200)
- assert %{"id" => id} = json_response(result, 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}
+ ]
end
- test "POST /api/v1/pleroma/statuses/:id/unreact_with_emoji", %{conn: conn} do
+ test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
@@ -39,7 +46,8 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
result =
conn
|> assign(:user, other_user)
- |> post("/api/v1/pleroma/statuses/#{activity.id}/unreact_with_emoji", %{"emoji" => "☕"})
+ |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
+ |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕")
assert %{"id" => id} = json_response(result, 200)
assert to_string(activity.id) == id
@@ -49,34 +57,75 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
assert object.data["reaction_count"] == 0
end
- test "GET /api/v1/pleroma/statuses/:id/emoji_reactions_by", %{conn: conn} do
+ 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}/emoji_reactions_by")
+ |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
|> json_response(200)
- assert result == %{}
+ 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}/emoji_reactions_by")
+ |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
|> json_response(200)
- [represented_user] = result["🎅"]
+ [%{"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(200)
+
+ assert [%{"name" => "🎅", "count" => 1, "accounts" => [_represented_user], "me" => true}] =
+ result
end
- test "/api/v1/pleroma/conversations/:id", %{conn: conn} do
+ 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(200)
+
+ assert result == []
+
+ {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
+ {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
+
+ result =
+ conn
+ |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅")
+ |> json_response(200)
+
+ [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result
+
+ assert represented_user["id"] == other_user.id
+ end
+
+ 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"})
@@ -84,16 +133,15 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
result =
conn
- |> assign(:user, other_user)
|> get("/api/v1/pleroma/conversations/#{participation.id}")
|> json_response(200)
assert result["id"] == participation.id |> to_string()
end
- test "/api/v1/pleroma/conversations/:id/statuses", %{conn: conn} do
+ test "/api/v1/pleroma/conversations/:id/statuses" do
user = insert(:user)
- other_user = insert(:user)
+ %{user: other_user, conn: conn} = oauth_access(["read:statuses"])
third_user = insert(:user)
{:ok, _activity} =
@@ -113,7 +161,6 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
result =
conn
- |> assign(:user, other_user)
|> get("/api/v1/pleroma/conversations/#{participation.id}/statuses")
|> json_response(200)
@@ -124,8 +171,8 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result
end
- test "PATCH /api/v1/pleroma/conversations/:id", %{conn: conn} do
- user = insert(:user)
+ 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"})
@@ -140,7 +187,6 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
result =
conn
- |> assign(:user, user)
|> patch("/api/v1/pleroma/conversations/#{participation.id}", %{
"recipients" => [user.id, other_user.id]
})
@@ -155,9 +201,9 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
assert other_user in participation.recipients
end
- test "POST /api/v1/pleroma/conversations/read", %{conn: conn} do
+ test "POST /api/v1/pleroma/conversations/read" do
user = insert(:user)
- other_user = insert(:user)
+ %{user: other_user, conn: conn} = oauth_access(["write:notifications"])
{:ok, _activity} =
CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}", "visibility" => "direct"})
@@ -172,7 +218,6 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
[%{"unread" => false}, %{"unread" => false}] =
conn
- |> assign(:user, other_user)
|> post("/api/v1/pleroma/conversations/read", %{})
|> json_response(200)
@@ -183,8 +228,9 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
end
describe "POST /api/v1/pleroma/notifications/read" do
- test "it marks a single notification as read", %{conn: conn} do
- user1 = insert(:user)
+ 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}"})
@@ -193,7 +239,6 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
response =
conn
- |> assign(:user, user1)
|> post("/api/v1/pleroma/notifications/read", %{"id" => "#{notification1.id}"})
|> json_response(:ok)
@@ -202,8 +247,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
refute Repo.get(Notification, notification2.id).seen
end
- test "it marks multiple notifications as read", %{conn: conn} do
- user1 = insert(:user)
+ 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}"})
@@ -213,7 +257,6 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
[response1, response2] =
conn
- |> assign(:user, user1)
|> post("/api/v1/pleroma/notifications/read", %{"max_id" => "#{notification2.id}"})
|> json_response(:ok)
@@ -225,11 +268,8 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
end
test "it returns error when notification not found", %{conn: conn} do
- user1 = insert(:user)
-
response =
conn
- |> assign(:user, user1)
|> post("/api/v1/pleroma/notifications/read", %{"id" => "22222222222222"})
|> json_response(:bad_request)
diff --git a/test/web/pleroma_api/controllers/scrobble_controller_test.exs b/test/web/pleroma_api/controllers/scrobble_controller_test.exs
index 881f8012c..1b945040c 100644
--- a/test/web/pleroma_api/controllers/scrobble_controller_test.exs
+++ b/test/web/pleroma_api/controllers/scrobble_controller_test.exs
@@ -1,21 +1,18 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# 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
- import Pleroma.Factory
describe "POST /api/v1/pleroma/scrobble" do
- test "works correctly", %{conn: conn} do
- user = insert(:user)
+ test "works correctly" do
+ %{conn: conn} = oauth_access(["write"])
conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/pleroma/scrobble", %{
+ post(conn, "/api/v1/pleroma/scrobble", %{
"title" => "lain radio episode 1",
"artist" => "lain",
"album" => "lain radio",
@@ -27,8 +24,8 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do
end
describe "GET /api/v1/pleroma/accounts/:id/scrobbles" do
- test "works correctly", %{conn: conn} do
- user = insert(:user)
+ test "works correctly" do
+ %{user: user, conn: conn} = oauth_access(["read"])
{:ok, _activity} =
CommonAPI.listen(user, %{
@@ -51,9 +48,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do
"album" => "lain radio"
})
- conn =
- conn
- |> get("/api/v1/pleroma/accounts/#{user.id}/scrobbles")
+ conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/scrobbles")
result = json_response(conn, 200)
diff --git a/test/web/plugs/federating_plug_test.exs b/test/web/plugs/federating_plug_test.exs
index 9dcab93da..13edc4359 100644
--- a/test/web/plugs/federating_plug_test.exs
+++ b/test/web/plugs/federating_plug_test.exs
@@ -1,10 +1,11 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.FederatingPlugTest do
use Pleroma.Web.ConnCase
- clear_config_all([:instance, :federating])
+
+ clear_config([:instance, :federating])
test "returns and halt the conn when federating is disabled" do
Pleroma.Config.put([:instance, :federating], false)
diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs
index 37c45eda6..b90e31f94 100644
--- a/test/web/push/impl_test.exs
+++ b/test/web/push/impl_test.exs
@@ -1,11 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Push.ImplTest do
use Pleroma.DataCase
alias Pleroma.Object
+ alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Push.Impl
alias Pleroma.Web.Push.Subscription
@@ -97,6 +98,14 @@ defmodule Pleroma.Web.Push.ImplTest do
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")
@@ -182,4 +191,50 @@ defmodule Pleroma.Web.Push.ImplTest do
assert Impl.format_title(%{activity: activity}) ==
"New Direct Message"
end
+
+ describe "build_content/3" do
+ test "returns info content for direct message with enabled privacy option" do
+ user = insert(:user, nickname: "Bob")
+ user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: 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: "@Bob",
+ title: "New Direct Message"
+ }
+ end
+
+ test "returns regular content for direct message with disabled privacy option" do
+ user = insert(:user, nickname: "Bob")
+ user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: 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"
+ }
+ end
+ end
end
diff --git a/test/web/rel_me_test.exs b/test/web/rel_me_test.exs
index 77b5d5dc6..e05a8863d 100644
--- a/test/web/rel_me_test.exs
+++ b/test/web/rel_me_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RelMeTest do
diff --git a/test/web/rich_media/aws_signed_url_test.exs b/test/web/rich_media/aws_signed_url_test.exs
index a3a50cbb1..b30f4400e 100644
--- a/test/web/rich_media/aws_signed_url_test.exs
+++ b/test/web/rich_media/aws_signed_url_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.TTL.AwsSignedUrlTest do
diff --git a/test/web/rich_media/helpers_test.exs b/test/web/rich_media/helpers_test.exs
index 48884319d..8237802a7 100644
--- a/test/web/rich_media/helpers_test.exs
+++ b/test/web/rich_media/helpers_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.HelpersTest do
diff --git a/test/web/rich_media/parser_test.exs b/test/web/rich_media/parser_test.exs
index b75bdf96f..e54a13bc8 100644
--- a/test/web/rich_media/parser_test.exs
+++ b/test/web/rich_media/parser_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.ParserTest do
diff --git a/test/web/rich_media/parsers/twitter_card_test.exs b/test/web/rich_media/parsers/twitter_card_test.exs
index f8e1c9b40..87c767c15 100644
--- a/test/web/rich_media/parsers/twitter_card_test.exs
+++ b/test/web/rich_media/parsers/twitter_card_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
@@ -7,11 +7,14 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
alias Pleroma.Web.RichMedia.Parsers.TwitterCard
test "returns error when html not contains twitter card" do
- assert TwitterCard.parse("", %{}) == {:error, "No twitter card metadata found"}
+ assert TwitterCard.parse([{"html", [], [{"head", [], []}, {"body", [], []}]}], %{}) ==
+ {:error, "No twitter card metadata found"}
end
test "parses twitter card with only name attributes" do
- html = File.read!("test/fixtures/nypd-facial-recognition-children-teenagers3.html")
+ html =
+ File.read!("test/fixtures/nypd-facial-recognition-children-teenagers3.html")
+ |> Floki.parse_document!()
assert TwitterCard.parse(html, %{}) ==
{:ok,
@@ -26,7 +29,9 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
end
test "parses twitter card with only property attributes" do
- html = File.read!("test/fixtures/nypd-facial-recognition-children-teenagers2.html")
+ html =
+ File.read!("test/fixtures/nypd-facial-recognition-children-teenagers2.html")
+ |> Floki.parse_document!()
assert TwitterCard.parse(html, %{}) ==
{:ok,
@@ -45,7 +50,9 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
end
test "parses twitter card with name & property attributes" do
- html = File.read!("test/fixtures/nypd-facial-recognition-children-teenagers.html")
+ html =
+ File.read!("test/fixtures/nypd-facial-recognition-children-teenagers.html")
+ |> Floki.parse_document!()
assert TwitterCard.parse(html, %{}) ==
{:ok,
@@ -66,4 +73,41 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
"https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"
}}
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, %{}) ==
+ {:ok,
+ %{
+ site: "@atlasobscura",
+ title:
+ "The Missing Grave of Margaret Corbin, Revolutionary War Veteran - Atlas Obscura",
+ card: "summary_large_image",
+ image: image_path
+ }}
+ 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, %{}) ==
+ {:ok,
+ %{
+ site: nil,
+ title:
+ "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times",
+ "app:id:googleplay": "com.nytimes.android",
+ "app:name:googleplay": "NYTimes",
+ "app:url:googleplay": "nytimes://reader/id/100000006583622"
+ }}
+ end
end
diff --git a/test/web/static_fe/static_fe_controller_test.exs b/test/web/static_fe/static_fe_controller_test.exs
index 2ce8f9fa3..c3d2ae3b4 100644
--- a/test/web/static_fe/static_fe_controller_test.exs
+++ b/test/web/static_fe/static_fe_controller_test.exs
@@ -1,56 +1,46 @@
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
clear_config_all([:static_fe, :enabled]) do
- Pleroma.Config.put([:static_fe, :enabled], true)
+ Config.put([:static_fe, :enabled], true)
end
- describe "user profile page" do
- test "just the profile as HTML", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> put_req_header("accept", "text/html")
- |> get("/users/#{user.nickname}")
+ clear_config([:instance, :federating]) do
+ Config.put([:instance, :federating], true)
+ end
- assert html_response(conn, 200) =~ user.nickname
- end
+ setup %{conn: conn} do
+ conn = put_req_header(conn, "accept", "text/html")
+ user = insert(:user)
- test "renders json unless there's an html accept header", %{conn: conn} do
- user = insert(:user)
+ %{conn: conn, user: user}
+ end
- conn =
- conn
- |> put_req_header("accept", "application/json")
- |> get("/users/#{user.nickname}")
+ describe "user profile html" do
+ test "just the profile as HTML", %{conn: conn, user: user} do
+ conn = get(conn, "/users/#{user.nickname}")
- assert json_response(conn, 200)
+ assert html_response(conn, 200) =~ user.nickname
end
test "404 when user not found", %{conn: conn} do
- conn =
- conn
- |> put_req_header("accept", "text/html")
- |> get("/users/limpopo")
+ conn = get(conn, "/users/limpopo")
assert html_response(conn, 404) =~ "not found"
end
- test "profile does not include private messages", %{conn: conn} do
- user = insert(:user)
+ 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 =
- conn
- |> put_req_header("accept", "text/html")
- |> get("/users/#{user.nickname}")
+ conn = get(conn, "/users/#{user.nickname}")
html = html_response(conn, 200)
@@ -58,14 +48,10 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
refute html =~ ">private<"
end
- test "pagination", %{conn: conn} do
- user = insert(:user)
+ test "pagination", %{conn: conn, user: user} do
Enum.map(1..30, fn i -> CommonAPI.post(user, %{"status" => "test#{i}"}) end)
- conn =
- conn
- |> put_req_header("accept", "text/html")
- |> get("/users/#{user.nickname}")
+ conn = get(conn, "/users/#{user.nickname}")
html = html_response(conn, 200)
@@ -75,15 +61,11 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
refute html =~ ">test1<"
end
- test "pagination, page 2", %{conn: conn} do
- user = insert(:user)
+ 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 =
- conn
- |> put_req_header("accept", "text/html")
- |> get("/users/#{user.nickname}?max_id=#{a11.id}")
+ conn = get(conn, "/users/#{user.nickname}?max_id=#{a11.id}")
html = html_response(conn, 200)
@@ -92,17 +74,17 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
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 rendering" do
- test "single notice page", %{conn: conn} do
- user = insert(:user)
+ describe "notice html" do
+ test "single notice page", %{conn: conn, user: user} do
{:ok, activity} = CommonAPI.post(user, %{"status" => "testing a thing!"})
- conn =
- conn
- |> put_req_header("accept", "text/html")
- |> get("/notice/#{activity.id}")
+ conn = get(conn, "/notice/#{activity.id}")
html = html_response(conn, 200)
assert html =~ "<header>"
@@ -110,8 +92,20 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
assert html =~ "testing a thing!"
end
- test "shows the whole thread", %{conn: conn} do
+ 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, %{
@@ -119,70 +113,47 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
"in_reply_to_status_id" => activity.id
})
- conn =
- conn
- |> put_req_header("accept", "text/html")
- |> get("/notice/#{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} do
- user = insert(:user)
-
+ 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 =
- conn
- |> put_req_header("accept", "text/html")
- |> get(URI.parse(object_url).path)
+ conn = get(conn, URI.parse(object_url).path)
assert html_response(conn, 302) =~ "redirected"
end
- test "redirect by activity ID", %{conn: conn} do
- user = insert(:user)
-
+ 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 =
- conn
- |> put_req_header("accept", "text/html")
- |> get(URI.parse(id).path)
+ conn = get(conn, URI.parse(id).path)
assert html_response(conn, 302) =~ "redirected"
end
test "404 when notice not found", %{conn: conn} do
- conn =
- conn
- |> put_req_header("accept", "text/html")
- |> get("/notice/88c9c317")
+ conn = get(conn, "/notice/88c9c317")
assert html_response(conn, 404) =~ "not found"
end
- test "404 for private status", %{conn: conn} do
- user = insert(:user)
-
+ test "404 for private status", %{conn: conn, user: user} do
{:ok, activity} =
CommonAPI.post(user, %{"status" => "don't show me!", "visibility" => "private"})
- conn =
- conn
- |> put_req_header("accept", "text/html")
- |> get("/notice/#{activity.id}")
+ conn = get(conn, "/notice/#{activity.id}")
assert html_response(conn, 404) =~ "not found"
end
- test "302 for remote cached status", %{conn: conn} do
- user = insert(:user)
-
+ test "302 for remote cached status", %{conn: conn, user: user} do
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => user.follower_address,
@@ -199,12 +170,15 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
- conn =
- conn
- |> put_req_header("accept", "text/html")
- |> get("/notice/#{activity.id}")
+ 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/ping_test.exs b/test/web/streamer/ping_test.exs
index 3d52c00e4..5df6c1cc3 100644
--- a/test/web/streamer/ping_test.exs
+++ b/test/web/streamer/ping_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PingTest do
diff --git a/test/web/streamer/state_test.exs b/test/web/streamer/state_test.exs
index d1aeac541..a755e75c0 100644
--- a/test/web/streamer/state_test.exs
+++ b/test/web/streamer/state_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.StateTest do
diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs
index 5a5b35147..f0bafc093 100644
--- a/test/web/streamer/streamer_test.exs
+++ b/test/web/streamer/streamer_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.StreamerTest do
@@ -16,7 +16,11 @@ defmodule Pleroma.Web.StreamerTest do
alias Pleroma.Web.Streamer.Worker
@moduletag needs_streamer: true, capture_log: true
- clear_config_all([:instance, :skip_thread_containment])
+
+ @streamer_timeout 150
+ @streamer_start_wait 10
+
+ clear_config([:instance, :skip_thread_containment])
describe "user streams" do
setup do
@@ -28,7 +32,7 @@ defmodule Pleroma.Web.StreamerTest do
test "it sends notify to in the 'user' stream", %{user: user, notify: notify} do
task =
Task.async(fn ->
- assert_receive {:text, _}, 4_000
+ assert_receive {:text, _}, @streamer_timeout
end)
Streamer.add_socket(
@@ -43,7 +47,7 @@ defmodule Pleroma.Web.StreamerTest do
test "it sends notify to in the 'user:notification' stream", %{user: user, notify: notify} do
task =
Task.async(fn ->
- assert_receive {:text, _}, 4_000
+ assert_receive {:text, _}, @streamer_timeout
end)
Streamer.add_socket(
@@ -59,9 +63,9 @@ defmodule Pleroma.Web.StreamerTest do
user: user
} do
blocked = insert(:user)
- {:ok, user} = User.block(user, blocked)
+ {:ok, _user_relationship} = User.block(user, blocked)
- task = Task.async(fn -> refute_receive {:text, _}, 4_000 end)
+ task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end)
Streamer.add_socket(
"user:notification",
@@ -79,7 +83,8 @@ defmodule Pleroma.Web.StreamerTest do
user: user
} do
user2 = insert(:user)
- task = Task.async(fn -> refute_receive {:text, _}, 4_000 end)
+
+ task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end)
Streamer.add_socket(
"user:notification",
@@ -89,6 +94,7 @@ defmodule Pleroma.Web.StreamerTest do
{:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
{:ok, activity} = CommonAPI.add_mute(user, activity)
{:ok, notif} = CommonAPI.favorite(user2, activity.id)
+
Streamer.stream("user:notification", notif)
Task.await(task)
end
@@ -97,7 +103,8 @@ defmodule Pleroma.Web.StreamerTest do
user: user
} do
user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"})
- task = Task.async(fn -> refute_receive {:text, _}, 4_000 end)
+
+ task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end)
Streamer.add_socket(
"user:notification",
@@ -116,7 +123,9 @@ defmodule Pleroma.Web.StreamerTest do
user: user
} do
user2 = insert(:user)
- task = Task.async(fn -> assert_receive {:text, _}, 4_000 end)
+ task = Task.async(fn -> assert_receive {:text, _}, @streamer_timeout end)
+
+ Process.sleep(@streamer_start_wait)
Streamer.add_socket(
"user:notification",
@@ -137,7 +146,7 @@ defmodule Pleroma.Web.StreamerTest do
task =
Task.async(fn ->
- assert_receive {:text, _}, 4_000
+ assert_receive {:text, _}, @streamer_timeout
end)
fake_socket = %StreamerSocket{
@@ -164,7 +173,7 @@ defmodule Pleroma.Web.StreamerTest do
}
|> Jason.encode!()
- assert_receive {:text, received_event}, 4_000
+ assert_receive {:text, received_event}, @streamer_timeout
assert received_event == expected_event
end)
@@ -259,7 +268,9 @@ defmodule Pleroma.Web.StreamerTest do
test "it doesn't send messages involving blocked users" do
user = insert(:user)
blocked_user = insert(:user)
- {:ok, user} = User.block(user, blocked_user)
+ {:ok, _user_relationship} = User.block(user, blocked_user)
+
+ {:ok, activity} = CommonAPI.post(blocked_user, %{"status" => "Test"})
task =
Task.async(fn ->
@@ -271,8 +282,6 @@ defmodule Pleroma.Web.StreamerTest do
user: user
}
- {:ok, activity} = CommonAPI.post(blocked_user, %{"status" => "Test"})
-
topics = %{
"public" => [fake_socket]
}
@@ -301,7 +310,7 @@ defmodule Pleroma.Web.StreamerTest do
"public" => [fake_socket]
}
- {:ok, blocker} = User.block(blocker, blockee)
+ {:ok, _user_relationship} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey! @#{blockee.nickname}"})
@@ -329,6 +338,12 @@ defmodule Pleroma.Web.StreamerTest do
{:ok, list} = List.create("Test", user_a)
{:ok, list} = List.follow(list, user_b)
+ {:ok, activity} =
+ CommonAPI.post(user_b, %{
+ "status" => "@#{user_c.nickname} Test",
+ "visibility" => "direct"
+ })
+
task =
Task.async(fn ->
refute_receive {:text, _}, 1_000
@@ -339,12 +354,6 @@ defmodule Pleroma.Web.StreamerTest do
user: user_a
}
- {:ok, activity} =
- CommonAPI.post(user_b, %{
- "status" => "@#{user_c.nickname} Test",
- "visibility" => "direct"
- })
-
topics = %{
"list:#{list.id}" => [fake_socket]
}
@@ -361,6 +370,12 @@ defmodule Pleroma.Web.StreamerTest do
{:ok, list} = List.create("Test", user_a)
{:ok, list} = List.follow(list, user_b)
+ {:ok, activity} =
+ CommonAPI.post(user_b, %{
+ "status" => "Test",
+ "visibility" => "private"
+ })
+
task =
Task.async(fn ->
refute_receive {:text, _}, 1_000
@@ -371,12 +386,6 @@ defmodule Pleroma.Web.StreamerTest do
user: user_a
}
- {:ok, activity} =
- CommonAPI.post(user_b, %{
- "status" => "Test",
- "visibility" => "private"
- })
-
topics = %{
"list:#{list.id}" => [fake_socket]
}
@@ -395,6 +404,12 @@ defmodule Pleroma.Web.StreamerTest do
{:ok, list} = List.create("Test", user_a)
{:ok, list} = List.follow(list, user_b)
+ {:ok, activity} =
+ CommonAPI.post(user_b, %{
+ "status" => "Test",
+ "visibility" => "private"
+ })
+
task =
Task.async(fn ->
assert_receive {:text, _}, 1_000
@@ -405,12 +420,6 @@ defmodule Pleroma.Web.StreamerTest do
user: user_a
}
- {:ok, activity} =
- CommonAPI.post(user_b, %{
- "status" => "Test",
- "visibility" => "private"
- })
-
Streamer.add_socket(
"list:#{list.id}",
fake_socket
@@ -427,6 +436,9 @@ defmodule Pleroma.Web.StreamerTest do
user3 = insert(:user)
CommonAPI.hide_reblogs(user1, user2)
+ {:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"})
+ {:ok, announce_activity, _} = CommonAPI.repeat(create_activity.id, user2)
+
task =
Task.async(fn ->
refute_receive {:text, _}, 1_000
@@ -437,14 +449,39 @@ defmodule Pleroma.Web.StreamerTest do
user: user1
}
+ topics = %{
+ "public" => [fake_socket]
+ }
+
+ Worker.push_to_socket(topics, "public", announce_activity)
+
+ Task.await(task)
+ end
+
+ test "it does send non-reblog notification for reblog-muted actors" do
+ user1 = insert(:user)
+ user2 = insert(:user)
+ user3 = insert(:user)
+ CommonAPI.hide_reblogs(user1, user2)
+
{:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"})
- {:ok, announce_activity, _} = CommonAPI.repeat(create_activity.id, user2)
+ {:ok, favorite_activity} = CommonAPI.favorite(user2, create_activity.id)
+
+ task =
+ Task.async(fn ->
+ assert_receive {:text, _}, 1_000
+ end)
+
+ fake_socket = %StreamerSocket{
+ transport_pid: task.pid,
+ user: user1
+ }
topics = %{
"public" => [fake_socket]
}
- Worker.push_to_socket(topics, "public", announce_activity)
+ Worker.push_to_socket(topics, "public", favorite_activity)
Task.await(task)
end
@@ -458,9 +495,7 @@ defmodule Pleroma.Web.StreamerTest do
{:ok, activity} = CommonAPI.add_mute(user2, activity)
- task = Task.async(fn -> refute_receive {:text, _}, 4_000 end)
-
- Process.sleep(4000)
+ task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end)
Streamer.add_socket(
"user",
@@ -482,7 +517,7 @@ defmodule Pleroma.Web.StreamerTest do
task =
Task.async(fn ->
- assert_receive {:text, received_event}, 4_000
+ assert_receive {:text, received_event}, @streamer_timeout
assert %{"event" => "conversation", "payload" => received_payload} =
Jason.decode!(received_event)
@@ -518,13 +553,13 @@ defmodule Pleroma.Web.StreamerTest do
task =
Task.async(fn ->
- assert_receive {:text, received_event}, 4_000
+ assert_receive {:text, received_event}, @streamer_timeout
assert %{"event" => "delete", "payload" => _} = Jason.decode!(received_event)
- refute_receive {:text, _}, 4_000
+ refute_receive {:text, _}, @streamer_timeout
end)
- Process.sleep(1000)
+ Process.sleep(@streamer_start_wait)
Streamer.add_socket(
"direct",
@@ -555,10 +590,10 @@ defmodule Pleroma.Web.StreamerTest do
task =
Task.async(fn ->
- assert_receive {:text, received_event}, 4_000
+ assert_receive {:text, received_event}, @streamer_timeout
assert %{"event" => "delete", "payload" => _} = Jason.decode!(received_event)
- assert_receive {:text, received_event}, 4_000
+ assert_receive {:text, received_event}, @streamer_timeout
assert %{"event" => "conversation", "payload" => received_payload} =
Jason.decode!(received_event)
@@ -567,7 +602,7 @@ defmodule Pleroma.Web.StreamerTest do
assert last_status["id"] == to_string(create_activity.id)
end)
- Process.sleep(1000)
+ Process.sleep(@streamer_start_wait)
Streamer.add_socket(
"direct",
diff --git a/test/web/twitter_api/password_controller_test.exs b/test/web/twitter_api/password_controller_test.exs
index 840c84a05..0a24860d3 100644
--- a/test/web/twitter_api/password_controller_test.exs
+++ b/test/web/twitter_api/password_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do
@@ -55,7 +55,7 @@ defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do
user = refresh_record(user)
assert Comeonin.Pbkdf2.checkpw("test", user.password_hash)
- assert length(Token.get_user_tokens(user)) == 0
+ assert Enum.empty?(Token.get_user_tokens(user))
end
test "it sets password_reset_pending to false", %{conn: conn} do
diff --git a/test/web/twitter_api/remote_follow_controller_test.exs b/test/web/twitter_api/remote_follow_controller_test.exs
new file mode 100644
index 000000000..73062f18f
--- /dev/null
+++ b/test/web/twitter_api/remote_follow_controller_test.exs
@@ -0,0 +1,237 @@
+# 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.User
+ alias Pleroma.Web.CommonAPI
+
+ import ExUnit.CaptureLog
+ import Pleroma.Factory
+
+ setup do
+ Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+ :ok
+ end
+
+ clear_config_all([:instance, :federating]) do
+ Config.put([:instance, :federating], true)
+ end
+
+ clear_config([:instance])
+ clear_config([:frontend_configurations, :pleroma_fe])
+ 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 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
new file mode 100644
index 000000000..ab0a2c3df
--- /dev/null
+++ b/test/web/twitter_api/twitter_api_controller_test.exs
@@ -0,0 +1,142 @@
+# 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
+ %{user: current_user, conn: conn} =
+ oauth_access(["read:notifications", "write:notifications"])
+
+ conn =
+ conn
+ |> assign(:user, current_user)
+ |> post("/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
index 85a9be3e0..14eed5f27 100644
--- a/test/web/twitter_api/twitter_api_test.exs
+++ b/test/web/twitter_api/twitter_api_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
@@ -117,15 +117,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
end
describe "register with one time token" do
- setup do
- setting = Pleroma.Config.get([:instance, :registrations_open])
-
- if setting do
- Pleroma.Config.put([:instance, :registrations_open], false)
- on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end)
- end
-
- :ok
+ clear_config([:instance, :registrations_open]) do
+ Pleroma.Config.put([:instance, :registrations_open], false)
end
test "returns user on success" do
@@ -191,14 +184,11 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
end
describe "registers with date limited token" do
- setup do
- setting = Pleroma.Config.get([:instance, :registrations_open])
-
- if setting do
- Pleroma.Config.put([:instance, :registrations_open], false)
- on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end)
- end
+ clear_config([:instance, :registrations_open]) do
+ Pleroma.Config.put([:instance, :registrations_open], false)
+ end
+ setup do
data = %{
"nickname" => "vinny",
"email" => "pasta@pizza.vs",
@@ -256,15 +246,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
end
describe "registers with reusable token" do
- setup do
- setting = Pleroma.Config.get([:instance, :registrations_open])
-
- if setting do
- Pleroma.Config.put([:instance, :registrations_open], false)
- on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end)
- end
-
- :ok
+ clear_config([:instance, :registrations_open]) do
+ Pleroma.Config.put([:instance, :registrations_open], false)
end
test "returns user on success, after him registration fails" do
@@ -309,15 +292,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
end
describe "registers with reusable date limited token" do
- setup do
- setting = Pleroma.Config.get([:instance, :registrations_open])
-
- if setting do
- Pleroma.Config.put([:instance, :registrations_open], false)
- on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end)
- end
-
- :ok
+ clear_config([:instance, :registrations_open]) do
+ Pleroma.Config.put([:instance, :registrations_open], false)
end
test "returns user on success" do
diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs
index f0211f59c..9d757b5ef 100644
--- a/test/web/twitter_api/util_controller_test.exs
+++ b/test/web/twitter_api/util_controller_test.exs
@@ -1,16 +1,15 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# 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.Repo
+ alias Pleroma.Config
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
- alias Pleroma.Web.CommonAPI
- import ExUnit.CaptureLog
+
import Pleroma.Factory
import Mock
@@ -21,24 +20,22 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
clear_config([:instance])
clear_config([:frontend_configurations, :pleroma_fe])
- clear_config([:user, :deny_follow_blocked])
describe "POST /api/pleroma/follow_import" do
+ setup do: oauth_access(["follow"])
+
test "it returns HTTP 200", %{conn: conn} do
- user1 = insert(:user)
user2 = insert(:user)
response =
conn
- |> assign(:user, user1)
|> post("/api/pleroma/follow_import", %{"list" => "#{user2.ap_id}"})
|> json_response(:ok)
assert response == "job started"
end
- test "it imports follow lists from file", %{conn: conn} do
- user1 = insert(:user)
+ test "it imports follow lists from file", %{user: user1, conn: conn} do
user2 = insert(:user)
with_mocks([
@@ -49,7 +46,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
]) do
response =
conn
- |> assign(:user, user1)
|> post("/api/pleroma/follow_import", %{"list" => %Plug.Upload{path: "follow_list.txt"}})
|> json_response(:ok)
@@ -67,12 +63,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end
test "it imports new-style mastodon follow lists", %{conn: conn} do
- user1 = insert(:user)
user2 = insert(:user)
response =
conn
- |> assign(:user, user1)
|> post("/api/pleroma/follow_import", %{
"list" => "Account address,Show boosts\n#{user2.ap_id},true"
})
@@ -81,7 +75,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
assert response == "job started"
end
- test "requires 'follow' or 'write:follows' permissions", %{conn: conn} do
+ 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"])
@@ -89,7 +83,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
for token <- [token1, token2, token3] do
conn =
- conn
+ build_conn()
|> put_req_header("authorization", "Bearer #{token.token}")
|> post("/api/pleroma/follow_import", %{"list" => "#{another_user.ap_id}"})
@@ -104,21 +98,21 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
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
- user1 = insert(:user)
user2 = insert(:user)
response =
conn
- |> assign(:user, user1)
|> post("/api/pleroma/blocks_import", %{"list" => "#{user2.ap_id}"})
|> json_response(:ok)
assert response == "job started"
end
- test "it imports blocks users from file", %{conn: conn} do
- user1 = insert(:user)
+ test "it imports blocks users from file", %{user: user1, conn: conn} do
user2 = insert(:user)
user3 = insert(:user)
@@ -127,7 +121,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
]) do
response =
conn
- |> assign(:user, user1)
|> post("/api/pleroma/blocks_import", %{"list" => %Plug.Upload{path: "blocks_list.txt"}})
|> json_response(:ok)
@@ -146,31 +139,47 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end
describe "PUT /api/pleroma/notification_settings" do
- test "it updates notification settings", %{conn: conn} do
- user = insert(:user)
+ setup do: oauth_access(["write:accounts"])
+ test "it updates notification settings", %{user: user, conn: conn} do
conn
- |> assign(:user, user)
|> put("/api/pleroma/notification_settings", %{
"followers" => false,
"bar" => 1
})
|> json_response(:ok)
- user = Repo.get(User, user.id)
+ user = refresh_record(user)
- assert %{
- "followers" => false,
- "follows" => true,
- "non_follows" => true,
- "non_followers" => true
+ assert %Pleroma.User.NotificationSetting{
+ followers: false,
+ follows: true,
+ non_follows: true,
+ non_followers: true,
+ privacy_option: false
+ } == user.notification_settings
+ end
+
+ test "it updates notification privacy option", %{user: user, conn: conn} do
+ conn
+ |> put("/api/pleroma/notification_settings", %{"privacy_option" => "1"})
+ |> json_response(:ok)
+
+ user = refresh_record(user)
+
+ assert %Pleroma.User.NotificationSetting{
+ followers: true,
+ follows: true,
+ non_follows: true,
+ non_followers: true,
+ privacy_option: true
} == user.notification_settings
end
end
describe "GET /api/statusnet/config" do
test "it returns config in xml format", %{conn: conn} do
- instance = Pleroma.Config.get(:instance)
+ instance = Config.get(:instance)
response =
conn
@@ -187,12 +196,12 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end
test "it returns config in json format", %{conn: conn} do
- instance = Pleroma.Config.get(:instance)
- Pleroma.Config.put([:instance, :managed_config], true)
- Pleroma.Config.put([:instance, :registrations_open], false)
- Pleroma.Config.put([:instance, :invites_enabled], true)
- Pleroma.Config.put([:instance, :public], false)
- Pleroma.Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"})
+ instance = Config.get(:instance)
+ Config.put([:instance, :managed_config], true)
+ Config.put([:instance, :registrations_open], false)
+ Config.put([:instance, :invites_enabled], true)
+ Config.put([:instance, :public], false)
+ Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"})
response =
conn
@@ -226,7 +235,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end
test "returns the state of safe_dm_mentions flag", %{conn: conn} do
- Pleroma.Config.put([:instance, :safe_dm_mentions], true)
+ Config.put([:instance, :safe_dm_mentions], true)
response =
conn
@@ -235,7 +244,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
assert response["site"]["safeDMMentionsEnabled"] == "1"
- Pleroma.Config.put([:instance, :safe_dm_mentions], false)
+ Config.put([:instance, :safe_dm_mentions], false)
response =
conn
@@ -246,8 +255,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end
test "it returns the managed config", %{conn: conn} do
- Pleroma.Config.put([:instance, :managed_config], false)
- Pleroma.Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"})
+ Config.put([:instance, :managed_config], false)
+ Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"})
response =
conn
@@ -256,7 +265,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
refute response["site"]["pleromafe"]
- Pleroma.Config.put([:instance, :managed_config], true)
+ Config.put([:instance, :managed_config], true)
response =
conn
@@ -279,7 +288,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
}
]
- Pleroma.Config.put(:frontend_configurations, config)
+ Config.put(:frontend_configurations, config)
response =
conn
@@ -308,201 +317,11 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end
end
- describe "GET /ostatus_subscribe - remote_follow/2" do
- test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} do
- conn =
- get(
- conn,
- "/ostatus_subscribe?acct=https://mastodon.social/users/emelie/statuses/101849165031453009"
- )
-
- assert redirected_to(conn) =~ "/notice/"
- end
-
- test "show follow account page if the `acct` is a account link", %{conn: conn} do
- response =
- get(
- conn,
- "/ostatus_subscribe?acct=https://mastodon.social/users/emelie"
- )
-
- assert html_response(response, 200) =~ "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("/ostatus_subscribe?acct=https://mastodon.social/users/emelie")
-
- assert html_response(response, 200) =~ "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("/ostatus_subscribe?acct=https://mastodon.social/users/not_found")
-
- assert html_response(response, 200) =~ "Error fetching user"
- end) =~ "Object has been deleted"
- end
- end
-
- describe "POST /ostatus_subscribe - do_remote_follow/2 with assigned user " do
- test "follows user", %{conn: conn} do
- user = insert(:user)
- user2 = insert(:user)
-
- response =
- conn
- |> assign(:user, user)
- |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}})
- |> response(200)
-
- assert response =~ "Account followed!"
- assert user2.follower_address in User.following(user)
- 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("/ostatus_subscribe", %{"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} = Pleroma.User.block(user2, user)
-
- response =
- conn
- |> assign(:user, user)
- |> post("/ostatus_subscribe", %{"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("/ostatus_subscribe", %{"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)
-
- response =
- conn
- |> assign(:user, refresh_record(user))
- |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}})
- |> response(200)
-
- assert response =~ "Account followed!"
- end
- end
-
- describe "POST /ostatus_subscribe - do_remote_follow/2 without assigned user " do
- test "follows", %{conn: conn} do
- user = insert(:user)
- user2 = insert(:user)
-
- response =
- conn
- |> post("/ostatus_subscribe", %{
- "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id}
- })
- |> response(200)
-
- assert response =~ "Account followed!"
- 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("/ostatus_subscribe", %{
- "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("/ostatus_subscribe", %{
- "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("/ostatus_subscribe", %{
- "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} = Pleroma.User.block(user2, user)
-
- response =
- conn
- |> post("/ostatus_subscribe", %{
- "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id}
- })
- |> response(200)
-
- assert response =~ "Error following account"
- end
- end
-
describe "GET /api/pleroma/healthcheck" do
clear_config([:instance, :healthcheck])
test "returns 503 when healthcheck disabled", %{conn: conn} do
- Pleroma.Config.put([:instance, :healthcheck], false)
+ Config.put([:instance, :healthcheck], false)
response =
conn
@@ -513,7 +332,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end
test "returns 200 when healthcheck enabled and all ok", %{conn: conn} do
- Pleroma.Config.put([:instance, :healthcheck], true)
+ Config.put([:instance, :healthcheck], true)
with_mock Pleroma.Healthcheck,
system_info: fn -> %Pleroma.Healthcheck{healthy: true} end do
@@ -532,8 +351,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end
end
- test "returns 503 when healthcheck enabled and health is false", %{conn: conn} do
- Pleroma.Config.put([:instance, :healthcheck], true)
+ 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
@@ -554,12 +373,11 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end
describe "POST /api/pleroma/disable_account" do
- test "it returns HTTP 200", %{conn: conn} do
- user = insert(:user)
+ setup do: oauth_access(["write:accounts"])
+ test "with valid permissions and password, it disables the account", %{conn: conn, user: user} do
response =
conn
- |> assign(:user, user)
|> post("/api/pleroma/disable_account", %{"password" => "test"})
|> json_response(:ok)
@@ -571,12 +389,11 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
assert user.deactivated == true
end
- test "it returns returns when password invalid", %{conn: conn} do
+ test "with valid permissions and invalid password, it returns an error", %{conn: conn} do
user = insert(:user)
response =
conn
- |> assign(:user, user)
|> post("/api/pleroma/disable_account", %{"password" => "test1"})
|> json_response(:ok)
@@ -610,6 +427,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end
describe "POST /main/ostatus - remote_subscribe/2" do
+ clear_config([:instance, :federating]) do
+ Config.put([:instance, :federating], true)
+ end
+
test "renders subscribe form", %{conn: conn} do
user = insert(:user)
@@ -646,7 +467,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
"https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}"
end
- test "it renders form with error when use not found", %{conn: conn} do
+ test "it renders form with error when user not found", %{conn: conn} do
user2 = insert(:user, ap_id: "shp@social.heldscal.la")
response =
@@ -671,29 +492,21 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end
end
- defp with_credentials(conn, username, password) do
- header_content = "Basic " <> Base.encode64("#{username}:#{password}")
- put_req_header(conn, "authorization", header_content)
- end
-
- defp valid_user(_context) do
- user = insert(:user)
- [user: user]
- end
-
describe "POST /api/pleroma/change_email" do
- setup [:valid_user]
+ setup do: oauth_access(["write:accounts"])
- test "without credentials", %{conn: conn} do
- conn = post(conn, "/api/pleroma/change_email")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
+ 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 credentials and invalid password", %{conn: conn, user: current_user} do
+ test "with proper permissions and invalid password", %{conn: conn} do
conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/change_email", %{
+ post(conn, "/api/pleroma/change_email", %{
"password" => "hi",
"email" => "test@test.com"
})
@@ -701,14 +514,11 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
assert json_response(conn, 200) == %{"error" => "Invalid password."}
end
- test "with credentials, valid password and invalid email", %{
- conn: conn,
- user: current_user
+ test "with proper permissions, valid password and invalid email", %{
+ conn: conn
} do
conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/change_email", %{
+ post(conn, "/api/pleroma/change_email", %{
"password" => "test",
"email" => "foobar"
})
@@ -716,28 +526,22 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
assert json_response(conn, 200) == %{"error" => "Email has invalid format."}
end
- test "with credentials, valid password and no email", %{
- conn: conn,
- user: current_user
+ test "with proper permissions, valid password and no email", %{
+ conn: conn
} do
conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/change_email", %{
+ post(conn, "/api/pleroma/change_email", %{
"password" => "test"
})
assert json_response(conn, 200) == %{"error" => "Email can't be blank."}
end
- test "with credentials, valid password and blank email", %{
- conn: conn,
- user: current_user
+ test "with proper permissions, valid password and blank email", %{
+ conn: conn
} do
conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/change_email", %{
+ post(conn, "/api/pleroma/change_email", %{
"password" => "test",
"email" => ""
})
@@ -745,16 +549,13 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
assert json_response(conn, 200) == %{"error" => "Email can't be blank."}
end
- test "with credentials, valid password and non unique email", %{
- conn: conn,
- user: current_user
+ test "with proper permissions, valid password and non unique email", %{
+ conn: conn
} do
user = insert(:user)
conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/change_email", %{
+ post(conn, "/api/pleroma/change_email", %{
"password" => "test",
"email" => user.email
})
@@ -762,14 +563,11 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
assert json_response(conn, 200) == %{"error" => "Email has already been taken."}
end
- test "with credentials, valid password and valid email", %{
- conn: conn,
- user: current_user
+ test "with proper permissions, valid password and valid email", %{
+ conn: conn
} do
conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/change_email", %{
+ post(conn, "/api/pleroma/change_email", %{
"password" => "test",
"email" => "cofe@foobar.com"
})
@@ -779,18 +577,20 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end
describe "POST /api/pleroma/change_password" do
- setup [:valid_user]
+ setup do: oauth_access(["write:accounts"])
+
+ test "without permissions", %{conn: conn} do
+ conn =
+ conn
+ |> assign(:token, nil)
+ |> post("/api/pleroma/change_password")
- test "without credentials", %{conn: conn} do
- conn = post(conn, "/api/pleroma/change_password")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
+ assert json_response(conn, 403) == %{"error" => "Insufficient permissions: write:accounts."}
end
- test "with credentials and invalid password", %{conn: conn, user: current_user} do
+ test "with proper permissions and invalid password", %{conn: conn} do
conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/change_password", %{
+ post(conn, "/api/pleroma/change_password", %{
"password" => "hi",
"new_password" => "newpass",
"new_password_confirmation" => "newpass"
@@ -799,14 +599,12 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
assert json_response(conn, 200) == %{"error" => "Invalid password."}
end
- test "with credentials, valid password and new password and confirmation not matching", %{
- conn: conn,
- user: current_user
- } do
+ test "with proper permissions, valid password and new password and confirmation not matching",
+ %{
+ conn: conn
+ } do
conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/change_password", %{
+ post(conn, "/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "newpass",
"new_password_confirmation" => "notnewpass"
@@ -817,14 +615,11 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
}
end
- test "with credentials, valid password and invalid new password", %{
- conn: conn,
- user: current_user
+ test "with proper permissions, valid password and invalid new password", %{
+ conn: conn
} do
conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/change_password", %{
+ post(conn, "/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "",
"new_password_confirmation" => ""
@@ -835,51 +630,48 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
}
end
- test "with credentials, valid password and matching new password and confirmation", %{
+ test "with proper permissions, valid password and matching new password and confirmation", %{
conn: conn,
- user: current_user
+ user: user
} do
conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/change_password", %{
+ 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(current_user.id)
+ fetched_user = User.get_cached_by_id(user.id)
assert Comeonin.Pbkdf2.checkpw("newpass", fetched_user.password_hash) == true
end
end
describe "POST /api/pleroma/delete_account" do
- setup [:valid_user]
-
- test "without credentials", %{conn: conn} do
- conn = post(conn, "/api/pleroma/delete_account")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
+ setup do: oauth_access(["write:accounts"])
- test "with credentials and invalid password", %{conn: conn, user: current_user} do
+ test "without permissions", %{conn: conn} do
conn =
conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/delete_account", %{"password" => "hi"})
+ |> assign(:token, nil)
+ |> post("/api/pleroma/delete_account")
- assert json_response(conn, 200) == %{"error" => "Invalid password."}
+ assert json_response(conn, 403) ==
+ %{"error" => "Insufficient permissions: write:accounts."}
end
- test "with credentials and valid password", %{conn: conn, user: current_user} do
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/delete_account", %{"password" => "test"})
+ 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} do
+ conn = post(conn, "/api/pleroma/delete_account", %{"password" => "test"})
assert json_response(conn, 200) == %{"status" => "success"}
- # Wait a second for the started task to end
- :timer.sleep(1000)
end
end
end
diff --git a/test/web/uploader_controller_test.exs b/test/web/uploader_controller_test.exs
index 7c7f9a6ea..21e518236 100644
--- a/test/web/uploader_controller_test.exs
+++ b/test/web/uploader_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.UploaderControllerTest do
diff --git a/test/web/views/error_view_test.exs b/test/web/views/error_view_test.exs
index 4e5398c83..8dbbd18b4 100644
--- a/test/web/views/error_view_test.exs
+++ b/test/web/views/error_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ErrorViewTest do
diff --git a/test/web/web_finger/web_finger_controller_test.exs b/test/web/web_finger/web_finger_controller_test.exs
index 49cd1460b..b65bf5904 100644
--- a/test/web/web_finger/web_finger_controller_test.exs
+++ b/test/web/web_finger/web_finger_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
diff --git a/test/web/web_finger/web_finger_test.exs b/test/web/web_finger/web_finger_test.exs
index 5aa8c73cf..4b4282727 100644
--- a/test/web/web_finger/web_finger_test.exs
+++ b/test/web/web_finger/web_finger_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFingerTest do
diff --git a/test/workers/cron/clear_oauth_token_worker_test.exs b/test/workers/cron/clear_oauth_token_worker_test.exs
new file mode 100644
index 000000000..f056b1a3e
--- /dev/null
+++ b/test/workers/cron/clear_oauth_token_worker_test.exs
@@ -0,0 +1,22 @@
+# 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.ClearOauthTokenWorkerTest do
+ use Pleroma.DataCase
+
+ import Pleroma.Factory
+ alias Pleroma.Workers.Cron.ClearOauthTokenWorker
+
+ clear_config([:oauth2, :clean_expired_tokens])
+
+ test "deletes expired tokens" do
+ insert(:oauth_token,
+ valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -60 * 10)
+ )
+
+ Pleroma.Config.put([:oauth2, :clean_expired_tokens], true)
+ ClearOauthTokenWorker.perform(:opts, :job)
+ assert Pleroma.Repo.all(Pleroma.Web.OAuth.Token) == []
+ end
+end
diff --git a/test/daemons/digest_email_daemon_test.exs b/test/workers/cron/digest_emails_worker_test.exs
index faf592d5f..5d65b9fef 100644
--- a/test/daemons/digest_email_daemon_test.exs
+++ b/test/workers/cron/digest_emails_worker_test.exs
@@ -1,17 +1,25 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.DigestEmailDaemonTest do
+defmodule Pleroma.Workers.Cron.DigestEmailsWorkerTest do
use Pleroma.DataCase
+
import Pleroma.Factory
- alias Pleroma.Daemons.DigestEmailDaemon
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.CommonAPI
- test "it sends digest emails" 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 =
@@ -23,8 +31,11 @@ defmodule Pleroma.DigestEmailDaemonTest do
{:ok, _} = User.switch_email_notifications(user2, "digest", true)
CommonAPI.post(user, %{"status" => "hey @#{user2.nickname}!"})
- DigestEmailDaemon.perform()
- ObanHelpers.perform_all()
+ {:ok, user2: user2}
+ end
+
+ test "it sends digest emails", %{user2: user2} do
+ Pleroma.Workers.Cron.DigestEmailsWorker.perform(:opts, :pid)
# Performing job(s) enqueued at previous step
ObanHelpers.perform_all()
@@ -32,4 +43,12 @@ defmodule Pleroma.DigestEmailDaemonTest do
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(:opts, :pid)
+ # 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
new file mode 100644
index 000000000..e6d050ecc
--- /dev/null
+++ b/test/workers/cron/new_users_digest_worker_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.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(nil, nil)
+ 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"
+ 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(nil, nil)
+ ObanHelpers.perform_all()
+ end
+end
diff --git a/test/workers/cron/purge_expired_activities_worker_test.exs b/test/workers/cron/purge_expired_activities_worker_test.exs
new file mode 100644
index 000000000..56c5aa409
--- /dev/null
+++ b/test/workers/cron/purge_expired_activities_worker_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.Workers.Cron.PurgeExpiredActivitiesWorkerTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.ActivityExpiration
+ alias Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker
+
+ import Pleroma.Factory
+ import ExUnit.CaptureLog
+
+ clear_config([ActivityExpiration, :enabled])
+
+ test "deletes an expiration activity" do
+ Pleroma.Config.put([ActivityExpiration, :enabled], true)
+ activity = insert(:note_activity)
+
+ naive_datetime =
+ NaiveDateTime.add(
+ NaiveDateTime.utc_now(),
+ -:timer.minutes(2),
+ :millisecond
+ )
+
+ expiration =
+ insert(
+ :expiration_in_the_past,
+ %{activity_id: activity.id, scheduled_at: naive_datetime}
+ )
+
+ Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker.perform(:ops, :pid)
+
+ refute Pleroma.Repo.get(Pleroma.Activity, activity.id)
+ refute Pleroma.Repo.get(Pleroma.ActivityExpiration, expiration.id)
+ end
+
+ describe "delete_activity/1" do
+ test "adds log message if activity isn't find" do
+ assert capture_log([level: :error], fn ->
+ PurgeExpiredActivitiesWorker.delete_activity(%ActivityExpiration{
+ activity_id: "test-activity"
+ })
+ end) =~ "Couldn't delete expired activity: not found activity"
+ end
+
+ test "adds log message if actor isn't find" do
+ assert capture_log([level: :error], fn ->
+ PurgeExpiredActivitiesWorker.delete_activity(%ActivityExpiration{
+ activity_id: "test-activity"
+ })
+ end) =~ "Couldn't delete expired activity: not found activity"
+ end
+ end
+end
diff --git a/test/workers/scheduled_activity_worker_test.exs b/test/workers/scheduled_activity_worker_test.exs
new file mode 100644
index 000000000..ab9f9c125
--- /dev/null
+++ b/test/workers/scheduled_activity_worker_test.exs
@@ -0,0 +1,52 @@
+# 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
+
+ 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(
+ %{"activity_id" => scheduled_activity.id},
+ :pid
+ )
+
+ 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(%{"activity_id" => 42}, :pid)
+ end) =~ "Couldn't find scheduled activity"
+ end
+end
diff --git a/test/xml_builder_test.exs b/test/xml_builder_test.exs
index a7742f339..059384c34 100644
--- a/test/xml_builder_test.exs
+++ b/test/xml_builder_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.XmlBuilderTest do