From f0ef87b84d7d56a9d59ce721062277eb58e6b3e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 11:08:13 +0200 Subject: [PATCH 01/35] Update dependency i18n-tasks to v1.0.13 (#27403) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0068df461b..ce21bc0a20 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -151,7 +151,7 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - better_html (2.0.1) + better_html (2.0.2) actionview (>= 6.0) activesupport (>= 6.0) ast (~> 2.0) @@ -345,14 +345,14 @@ GEM rainbow (>= 2.0.0) i18n (1.14.1) concurrent-ruby (~> 1.0) - i18n-tasks (1.0.12) + i18n-tasks (1.0.13) activesupport (>= 4.0.2) ast (>= 2.1.0) better_html (>= 1.0, < 3.0) erubi highline (>= 2.0.0) i18n - parser (>= 2.2.3.0) + parser (>= 3.2.2.1) rails-i18n rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) @@ -561,7 +561,7 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - rails-i18n (7.0.7) + rails-i18n (7.0.8) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) railties (7.0.8) From a00fc837014da27465695da1c3fd3a86e476937f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 11:09:20 +0200 Subject: [PATCH 02/35] Update dependency bufferutil to v4.0.8 (#27412) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index b69e7b65f0..5684a034d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3820,9 +3820,9 @@ buffer@^6.0.3: ieee754 "^1.2.1" bufferutil@^4.0.7: - version "4.0.7" - resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.7.tgz#60c0d19ba2c992dd8273d3f73772ffc894c153ad" - integrity sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw== + version "4.0.8" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.8.tgz#1de6a71092d65d7766c4d8a522b261a6e787e8ea" + integrity sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw== dependencies: node-gyp-build "^4.3.0" @@ -8766,9 +8766,9 @@ node-forge@^0.10.0: integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== node-gyp-build@^4.3.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" - integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== + version "4.6.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.1.tgz#24b6d075e5e391b8d5539d98c7fc5c210cac8a3e" + integrity sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ== node-int64@^0.4.0: version "0.4.0" @@ -11473,6 +11473,7 @@ stringz@^2.1.0: char-regex "^1.0.2" "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== From 13456efd7b3f95e5312eac350644679ae90d63db Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 11:31:52 +0200 Subject: [PATCH 03/35] New Crowdin Translations (automated) (#27410) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/eu.json | 2 +- app/javascript/mastodon/locales/fi.json | 16 ++-- app/javascript/mastodon/locales/ja.json | 2 +- app/javascript/mastodon/locales/sk.json | 2 +- app/javascript/mastodon/locales/zh-TW.json | 6 +- config/locales/devise.zh-TW.yml | 4 +- config/locales/fi.yml | 100 ++++++++++----------- config/locales/si.yml | 14 +-- config/locales/simple_form.fi.yml | 32 +++---- config/locales/simple_form.zh-TW.yml | 10 +-- config/locales/sk.yml | 1 + config/locales/zh-TW.yml | 28 +++--- 12 files changed, 107 insertions(+), 110 deletions(-) diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json index b674f90c84..5b4fef59c0 100644 --- a/app/javascript/mastodon/locales/eu.json +++ b/app/javascript/mastodon/locales/eu.json @@ -613,7 +613,7 @@ "sign_in_banner.create_account": "Sortu kontua", "sign_in_banner.sign_in": "Hasi saioa", "sign_in_banner.sso_redirect": "Hasi saioa edo izena eman", - "sign_in_banner.text": "Hasi saioa profilak edo traolak jarraitzeko, bidalketak gogokoetara gehitzeko, partekatzeko edo erantzuteko. Zure kontutik ere komunika zaitezke beste zerbitzari ezberdin vatean.", + "sign_in_banner.text": "Hasi saioa profilak edo traolak jarraitzeko, bidalketak gogokoetara gehitzeko, partekatzeko edo erantzuteko. Zure kontutik ere komunika zaitezke beste zerbitzari ezberdin batean.", "status.admin_account": "Ireki @{name} erabiltzailearen moderazio interfazea", "status.admin_domain": "{domain}-(r)en moderazio-interfazea ireki", "status.admin_status": "Ireki bidalketa hau moderazio interfazean", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 91be7bfc0e..a7a68533d6 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -71,8 +71,8 @@ "account.unmute_notifications_short": "Poista ilmoitusten mykistys", "account.unmute_short": "Poista mykistys", "account_note.placeholder": "Lisää muistiinpano napsauttamalla", - "admin.dashboard.daily_retention": "Käyttäjän säilyminen rekisteröitymisen jälkeiseen päivään mennessä", - "admin.dashboard.monthly_retention": "Käyttäjän säilyminen rekisteröitymisen jälkeiseen kuukauteen mennessä", + "admin.dashboard.daily_retention": "Käyttäjän pysyminen rekisteröitymisen jälkeiseen päivään mennessä", + "admin.dashboard.monthly_retention": "Käyttäjän pysyminen rekisteröitymisen jälkeiseen kuukauteen mennessä", "admin.dashboard.retention.average": "Keskimäärin", "admin.dashboard.retention.cohort": "Kirjautumiset", "admin.dashboard.retention.cohort_size": "Uudet käyttäjät", @@ -101,7 +101,7 @@ "bundle_modal_error.close": "Sulje", "bundle_modal_error.message": "Jotain meni pieleen komponenttia ladattaessa.", "bundle_modal_error.retry": "Yritä uudelleen", - "closed_registrations.other_server_instructions": "Koska Mastodon on hajautettu, voit luoda tilin toiselle palvelimelle ja silti olla vuorovaikutuksessa tämän kanssa.", + "closed_registrations.other_server_instructions": "Koska Mastodon on hajautettu, voit luoda tilin toiselle palvelimelle ja olla silti vuorovaikutuksessa tämän kanssa.", "closed_registrations_modal.description": "Tilin luonti palveluun {domain} ei tällä hetkellä ole mahdollista, mutta huomioi, ettei Mastodonin käyttö edellytä juuri kyseisen palvelun tiliä.", "closed_registrations_modal.find_another_server": "Etsi toinen palvelin", "closed_registrations_modal.preamble": "Mastodon on hajautettu, joten riippumatta siitä, missä luot tilisi, voit seurata ja olla vuorovaikutuksessa kenen tahansa kanssa tällä palvelimella. Voit jopa isännöidä palvelinta!", @@ -154,9 +154,9 @@ "compose_form.publish_form": "Uusi julkaisu", "compose_form.publish_loud": "{publish}!", "compose_form.save_changes": "Tallenna muutokset", - "compose_form.sensitive.hide": "{count, plural, one {Merkitse media arkaluontoiseksi} other {Merkitse mediat arkaluontoiseksi}}", - "compose_form.sensitive.marked": "{count, plural, one {Media on merkitty arkaluontoiseksi} other {Mediat on merkitty arkaluontoiseksi}}", - "compose_form.sensitive.unmarked": "{count, plural, one {Mediaa ei ole merkitty arkaluontoiseksi} other {Medioja ei ole merkitty arkaluontoiseksi}}", + "compose_form.sensitive.hide": "{count, plural, one {Merkitse media arkaluonteiseksi} other {Merkitse mediat arkaluonteisiksi}}", + "compose_form.sensitive.marked": "{count, plural, one {Media on merkitty arkaluonteiseksi} other {Mediat on merkitty arkaluonteisiksi}}", + "compose_form.sensitive.unmarked": "{count, plural, one {Mediaa ei ole merkitty arkaluonteiseksi} other {Medioita ei ole merkitty arkaluonteisiksi}}", "compose_form.spoiler.marked": "Poista sisältövaroitus", "compose_form.spoiler.unmarked": "Lisää sisältövaroitus", "compose_form.spoiler_placeholder": "Kirjoita varoituksesi tähän", @@ -592,7 +592,7 @@ "search.search_or_paste": "Hae tai kirjoita URL-osoite", "search_popout.full_text_search_disabled_message": "Ei saatavilla palvelimella {domain}.", "search_popout.language_code": "ISO-kielikoodi", - "search_popout.options": "Haun asetukset", + "search_popout.options": "Hakuvalinnat", "search_popout.quick_actions": "Pikatoiminnot", "search_popout.recent": "Viimeaikaiset haut", "search_popout.specific_date": "tietty päivämäärä", @@ -686,7 +686,7 @@ "timeline_hint.resources.followers": "Seuraajat", "timeline_hint.resources.follows": "seurattua", "timeline_hint.resources.statuses": "Vanhemmat julkaisut", - "trends.counter_by_accounts": "{count, plural, one {{counter} henkilö} other {{counter} henkilöä}} {days, plural, one {viimeisen päivän} other {viimeisten {days} päivän}} aikana", + "trends.counter_by_accounts": "{count, plural, one {{counter} henkilö} other {{counter} henkilöä}} {days, plural, one {viime päivänä} other {viimeisenä {days} päivänä}}", "trends.trending_now": "Suosittua nyt", "ui.beforeunload": "Luonnos häviää, jos poistut Mastodonista.", "units.short.billion": "{count} mrd.", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 0c8e417b91..c5817c199e 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -47,7 +47,7 @@ "account.locked_info": "このアカウントは承認制アカウントです。相手が承認するまでフォローは完了しません。", "account.media": "メディア", "account.mention": "@{name}さんにメンション", - "account.moved_to": "{name}さんがアカウントを引っ越しました:", + "account.moved_to": "{name}さんはこちらのアカウントに引っ越しました:", "account.mute": "@{name}さんをミュート", "account.mute_notifications_short": "通知をオフにする", "account.mute_short": "ミュート", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index da12eaa1b5..60760607f2 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -61,7 +61,7 @@ "account.requested_follow": "{name} ti poslal žiadosť na sledovanie", "account.share": "Zdieľaj @{name} profil", "account.show_reblogs": "Ukáž vyzdvihnutia od @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}", + "account.statuses_counter": "{count, plural, one {{counter} príspevok} other {{counter} príspevkov}}", "account.unblock": "Odblokuj @{name}", "account.unblock_domain": "Prestaň skrývať {domain}", "account.unblock_short": "Odblokuj", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 27fd0ad35e..b1cd0ca753 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -204,7 +204,7 @@ "dismissable_banner.explore_links": "這些新聞故事正在被此伺服器以及去中心化網路上的人們熱烈討論著。越多不同人所嘟出的新聞排名更高。", "dismissable_banner.explore_statuses": "這些於此伺服器以及去中心化網路中其他伺服器發出的嘟文正在被此伺服器上的人們熱烈討論著。越多不同人轉嘟及最愛排名更高。", "dismissable_banner.explore_tags": "這些主題標籤正在被此伺服器以及去中心化網路上的人們熱烈討論著。越多不同人所嘟出的主題標籤排名更高。", - "dismissable_banner.public_timeline": "這些是來自 {domain} 上人們於社群網站中跟隨者所發表之最近公開嘟文。", + "dismissable_banner.public_timeline": "這些是來自 {domain} 使用者們跟隨中帳號所發表之最新公開嘟文。", "embed.instructions": "要在您的網站嵌入此嘟文,請複製以下程式碼。", "embed.preview": "它將顯示成這樣:", "emoji_button.activity": "活動", @@ -271,8 +271,8 @@ "filter_modal.select_filter.title": "過濾此嘟文", "filter_modal.title.status": "過濾一則嘟文", "firehose.all": "全部", - "firehose.local": "此伺服器", - "firehose.remote": "其他伺服器", + "firehose.local": "本站", + "firehose.remote": "聯邦宇宙", "follow_request.authorize": "授權", "follow_request.reject": "拒絕", "follow_requests.unlocked_explanation": "即便您的帳號未被鎖定,{domain} 的管理員認為您可能想要自己審核這些帳號的跟隨請求。", diff --git a/config/locales/devise.zh-TW.yml b/config/locales/devise.zh-TW.yml index ea1813e413..c01beb796b 100644 --- a/config/locales/devise.zh-TW.yml +++ b/config/locales/devise.zh-TW.yml @@ -4,7 +4,7 @@ zh-TW: confirmations: confirmed: 您的電子郵件地址已確認成功。 send_instructions: 幾分鐘後您將收到確認信件。若未收到此信件,請檢查垃圾郵件資料夾。 - send_paranoid_instructions: 如果您的電子郵件存在於我們的資料庫,您將會在幾分鐘內收到確認信。若未收到請檢查垃圾郵件資料夾。 + send_paranoid_instructions: 如果您的電子郵件存在於我們的資料庫,您將於幾分鐘內收到確認信。若未收到請檢查垃圾郵件資料夾。 failure: already_authenticated: 您已登入。 inactive: 您的帳號尚未啟用。 @@ -43,7 +43,7 @@ zh-TW: reset_password_instructions: action: 變更密碼 explanation: 您已請求帳號的新密碼。 - extra: 若您並未請求,請忽略此信件。您的密碼在存取上方連結並建立新密碼前不會變更。 + extra: 若您並未請求,請忽略此信件。您的密碼於存取上方連結並建立新密碼前不會變更。 subject: Mastodon:重設密碼指引 title: 重設密碼 two_factor_disabled: diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 0c36b795c9..cdc5d12678 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -12,7 +12,7 @@ fi: one: seuraaja other: seuraajaa following: seurattu(a) - instance_actor_flash: Tämä tili on virtuaalinen toimija, jota käytetään edustamaan itse palvelinta eikä yksittäistä käyttäjää. Sitä käytetään federointitarkoituksiin, eikä sitä tule jäädyttää. + instance_actor_flash: Tämä tili on virtuaalinen toimija, jota käytetään edustamaan itse palvelinta eikä yksittäistä käyttäjää. Sitä käytetään liittoutumistarkoituksiin, eikä sitä tule jäädyttää. last_active: viimeksi aktiivinen link_verified_on: Tämän linkin omistus on tarkastettu %{date} nothing_here: Täällä ei ole mitään! @@ -138,7 +138,7 @@ fi: security_measures: only_password: Vain salasana password_and_2fa: Salasana ja kaksivaiheinen tunnistautuminen - sensitive: Pakotus arkaluonteiseksi + sensitive: Pakota arkaluonteiseksi sensitized: Merkitty arkaluonteiseksi shared_inbox_url: Jaetun saapuvan postilaatikon osoite show: @@ -157,7 +157,7 @@ fi: unblock_email: Poista sähköpostiosoitteen esto unblocked_email_msg: Käyttäjän %{username} sähköpostiosoitteen esto kumottiin unconfirmed_email: Sähköpostia ei vahvistettu - undo_sensitized: Kumoa pakotus arkaluonteiseksi tiliksi + undo_sensitized: Kumoa pakotus arkaluonteiseksi undo_silenced: Kumoa rajoitus undo_suspension: Peru jäähy unsilenced_msg: Tilin %{username} rajoituksen kumoaminen onnistui @@ -167,7 +167,7 @@ fi: view_domain: Näytä verkkotunnuksen yhteenveto warn: Varoita web: Verkko - whitelisted: Sallittu federoimaan + whitelisted: Sallittu liittoutua action_logs: action_types: approve_appeal: Hyväksy valitus @@ -214,12 +214,12 @@ fi: resend_user: Lähetä vahvistusviesti uudelleen reset_password_user: Nollaa salasana resolve_report: Selvitä raportti - sensitive_account: Pakotus arkaluontoiseksi tiliksi + sensitive_account: Pakotus arkaluonteiseksi tiliksi silence_account: Rajoita tiliä suspend_account: Jäädytä tili unassigned_report: Peruuta raportin määritys unblock_email_account: Poista sähköpostiosoitteen esto - unsensitive_account: Kumoa pakotus arkaluontoiseksi tiliksi + unsensitive_account: Kumoa pakotus arkaluonteiseksi tiliksi unsilence_account: Kumoa tilin rajoitus unsuspend_account: Kumoa tilin jäädytys update_announcement: Päivitä tiedote @@ -239,7 +239,7 @@ fi: create_announcement_html: "%{name} loi uuden tiedotteen %{target}" create_canonical_email_block_html: "%{name} esti sähköpostin tiivisteellä %{target}" create_custom_emoji_html: "%{name} lähetti uuden emojin %{target}" - create_domain_allow_html: "%{name} salli federoinnin verkkotunnuksen %{target} kanssa" + create_domain_allow_html: "%{name} salli liittoutumisen verkkotunnuksen %{target} kanssa" create_domain_block_html: "%{name} esti verkkotunnuksen %{target}" create_email_domain_block_html: "%{name} esti sähköpostiverkkotunnuksen %{target}" create_ip_block_html: "%{name} loi IP-säännön %{target}" @@ -249,7 +249,7 @@ fi: destroy_announcement_html: "%{name} poisti tiedotteen %{target}" destroy_canonical_email_block_html: "%{name} poisti sähköpostin eston tiivisteellä %{target}" destroy_custom_emoji_html: "%{name} poisti emojin %{target}" - destroy_domain_allow_html: "%{name} kielsi federoinnin verkkotunnuksen %{target} kanssa" + destroy_domain_allow_html: "%{name} kielsi liittoutumisen verkkotunnuksen %{target} kanssa" destroy_domain_block_html: "%{name} poisti verkkotunnuksen %{target} eston" destroy_email_domain_block_html: "%{name} poisti sähköpostiverkkotunnuksen %{target} eston" destroy_instance_html: "%{name} tyhjensi verkkotunnuksen %{target}" @@ -278,7 +278,7 @@ fi: suspend_account_html: "%{name} jäädytti käyttäjän %{target} tilin" unassigned_report_html: "%{name} peruutti raportin määrityksen %{target}" unblock_email_account_html: "%{name} poisti käyttäjän %{target} sähköpostiosoitteen eston" - unsensitive_account_html: "%{name} poisti käyttäjän %{target} median arkaluonteisen merkinnän" + unsensitive_account_html: "%{name} kumosi käyttäjän %{target} median arkaluonteisuusmerkinnän" unsilence_account_html: "%{name} kumosi käyttäjän %{target} rajoituksen" unsuspend_account_html: "%{name} kumosi käyttäjän %{target} tilin jäädytyksen" update_announcement_html: "%{name} päivitti tiedotteen %{target}" @@ -375,12 +375,12 @@ fi: empty: Valituksia ei löytynyt. title: Valitukset domain_allows: - add_new: Salli liitto verkkotunnuksella - created_msg: Verkkotunnus on onnistuneesti sallittu federoinnille - destroyed_msg: Verkkotunnusta on kielletty federoimasta + add_new: Salli liittoutuminen tämän verkkotunnuksen kanssa + created_msg: Verkkotunnuksen on onnistuneesti sallittu liittoutua + destroyed_msg: Verkkotunnusta on kielletty liittoutumasta export: Vie import: Tuo - undo: Estä liitto verkkotunnukselle + undo: Kiellä liittoutuminen tämän verkkotunnuksen kanssa domain_blocks: add_new: Lisää uusi verkkotunnuksen esto confirm_suspension: @@ -527,7 +527,7 @@ fi: public_comment: Julkinen kommentti purge: Tyhjennä purge_description_html: Jos uskot, että tämä verkkotunnus on offline-tilassa tarkoituksella, voit poistaa kaikki verkkotunnuksen tilitietueet ja niihin liittyvät tiedot tallennustilastasi. Tämä voi kestää jonkin aikaa. - title: Federointi + title: Liittoutuminen total_blocked_by_us: Estämämme total_followed_by_them: Heidän seuraama total_followed_by_us: Meidän seuraama @@ -562,7 +562,7 @@ fi: relays: add_new: Lisää uusi välittäjä delete: Poista - description_html: "Federointivälittäjä on välityspalvelin, joka siirtää suuria määriä julkisia julkaisuja siihen liittyneiden palvelinten välillä. Se voi auttaa pieniä ja keskisuuria palvelimia löytämään fediversumin sisältöä, mikä muutoin vaatisi paikallisia käyttäjiä seuraamaan etäpalvalinten käyttäjiä manuaalisesti." + description_html: "Liittoutumisvälittäjä on välityspalvelin, joka siirtää suuria määriä julkisia julkaisuja siihen liittyneiden palvelinten välillä. Se voi auttaa pieniä ja keskisuuria palvelimia löytämään fediversumin sisältöä, mikä muutoin vaatisi paikallisia käyttäjiä seuraamaan etäpalvalinten käyttäjiä manuaalisesti." disable: Poista käytöstä disabled: Poissa käytöstä enable: Ota käyttöön @@ -671,56 +671,56 @@ fi: devops: DevOps invites: Kutsut moderation: Valvonta - special: Erikois + special: Erityistä delete: Poista description_html: "Käyttäjärooleilla voit muokata, mihin toimintoihin ja alueisiin käyttäjäsi pääsevät käsiksi." - edit: Muokkaa "%{name}" roolia - everyone: Oletus käyttöoikeudet - everyone_full_description_html: Tämä on perusrooli joka vaikuttaa kaikkiin käyttäjiin, jopa ilman määrättyä roolia. Kaikki muut roolit perivät sen käyttöoikeudet. + edit: Muokkaa roolia ”%{name}” + everyone: Oletuskäyttöoikeudet + everyone_full_description_html: Tämä on perusrooli, joka vaikuttaa kaikkiin käyttäjiin, jopa ilman määrättyä roolia. Kaikki muut roolit perivät sen käyttöoikeudet. permissions_count: one: "%{count} käyttöoikeus" other: "%{count} käyttöoikeutta" privileges: administrator: Ylläpitäjä administrator_description: Käyttäjät, joilla on tämä käyttöoikeus, ohittavat jokaisen käyttöoikeuden - delete_user_data: Poista käyttäjän tiedot + delete_user_data: Poistaa käyttäjän tiedot delete_user_data_description: Salli käyttäjien poistaa muiden käyttäjien tiedot viipymättä - invite_users: Kutsu käyttäjiä + invite_users: Kutsua käyttäjiä invite_users_description: Sallii käyttäjien kutsua uusia ihmisiä palvelimelle - manage_announcements: Hallitse tiedotteita + manage_announcements: Hallita tiedotteita manage_announcements_description: Sallii käyttäjien hallita tiedotteita palvelimella - manage_appeals: Hallitse valituksia + manage_appeals: Hallita valituksia manage_appeals_description: Sallii käyttäjien tarkistaa valvontatoimia koskevia valituksia - manage_blocks: Hallitse estoja + manage_blocks: Hallita estoja manage_blocks_description: Sallii käyttäjien estää sähköpostipalveluntarjoajia ja IP-osoitteita - manage_custom_emojis: Hallitse mukautettuja emojeita + manage_custom_emojis: Hallita mukautettuja emojeita manage_custom_emojis_description: Sallii käyttäjien hallita mukautettuja emojeita palvelimella - manage_federation: Hallitse federointia - manage_federation_description: Sallii käyttäjien estää tai sallia federointi muiden verkkotunnusten kanssa ja hallita toimitusta - manage_invites: Hallitse kutsuja + manage_federation: Hallita liittoutumista + manage_federation_description: Sallii käyttäjien estää tai sallia liittoutuminen muiden verkkotunnusten kanssa ja hallita toimitusta + manage_invites: Hallita kutsuja manage_invites_description: Sallii käyttäjien selata ja poistaa kutsulinkkejä käytöstä - manage_reports: Hallitse raportteja + manage_reports: Hallita raportteja manage_reports_description: Sallii käyttäjien tarkistaa raportteja ja suorittaa valvontatoimia niitä vastaan - manage_roles: Hallitse rooleja + manage_roles: Hallita rooleja manage_roles_description: Sallii käyttäjien hallita ja määrittää rooleja heidän alapuolellaan - manage_rules: Hallitse sääntöjä + manage_rules: Hallita sääntöjä manage_rules_description: Sallii käyttäjien muuttaa palvelimen sääntöjä - manage_settings: Hallitse asetuksia + manage_settings: Hallita asetuksia manage_settings_description: Sallii käyttäjien muuttaa sivuston asetuksia - manage_taxonomies: Hallitse luokittelua + manage_taxonomies: Hallita luokittelua manage_taxonomies_description: Sallii käyttäjien tarkistaa suositun sisällön ja päivittää aihetunnisteiden asetuksia - manage_user_access: Hallitse käyttäjäoikeuksia + manage_user_access: Hallita käyttäjäoikeuksia manage_user_access_description: Sallii käyttäjien poistaa muiden käyttäjien kaksivaiheinen todennus käytöstä, vaihtaa heidän sähköpostiosoitteensa ja nollata heidän salasanansa - manage_users: Hallitse käyttäjiä + manage_users: Hallita käyttäjiä manage_users_description: Sallii käyttäjien tarkastella muiden käyttäjien tietoja ja suorittaa valvontatoimia heitä kohtaan - manage_webhooks: Hallitse webhookeja + manage_webhooks: Hallita webhookeja manage_webhooks_description: Sallii käyttäjien luoda webhookeja hallinnollisiin tapahtumiin view_audit_log: Katsoa valvontalokia view_audit_log_description: Sallii käyttäjien nähdä palvelimen hallinnollisten toimien historian - view_dashboard: Näytä koontinäyttö + view_dashboard: Katsoa koontinäyttöä view_dashboard_description: Sallii käyttäjien käyttää kojelautaa ja erilaisia mittareita view_devops: DevOps - view_devops_description: Sallii käyttäjille oikeuden käyttää Sidekiq ja pgHero dashboardeja + view_devops_description: Sallii käyttäjille pääsyn Sidekiq- ja pgHero-hallintapaneeleihin title: Roolit rules: add_new: Lisää sääntö @@ -732,7 +732,7 @@ fi: settings: about: manage_rules: Hallitse palvelimen sääntöjä - preamble: Anna perusteellista tietoa siitä, miten palvelinta käytetään, valvotaan, rahoitetaan. + preamble: Anna perusteellista tietoa siitä, miten palvelinta käytetään, valvotaan ja rahoitetaan. rules_hint: On olemassa erityinen alue sääntöjä, joita käyttäjien odotetaan noudattavan. title: Tietoja appearance: @@ -752,7 +752,7 @@ fi: title: Jätä käyttäjät oletusarvoisesti hakukoneindeksoinnin ulkopuolelle discovery: follow_recommendations: Seuraamissuositukset - preamble: Mielenkiintoisen sisällön esille tuominen auttaa saamaan uusia käyttäjiä, jotka eivät ehkä tunne ketään Mastodonista. Määrittele, kuinka erilaiset etsintäominaisuudet toimivat palvelimellasi. + preamble: Mielenkiintoisen sisällön esille tuominen auttaa saamaan uusia käyttäjiä, jotka eivät ehkä tunne ketään Mastodonista. Määrittele, kuinka erilaiset löytämisominaisuudet toimivat palvelimellasi. profile_directory: Profiilihakemisto public_timelines: Julkiset aikajanat publish_discovered_servers: Julkaise löydetyt palvelimet @@ -765,17 +765,17 @@ fi: users: Kirjautuneille paikallisille käyttäjille registrations: preamble: Määritä, kuka voi luoda tilin palvelimellesi. - title: Rekisteröinnit + title: Rekisteröityminen registrations_mode: modes: approved: Rekisteröinti vaatii hyväksynnän none: Kukaan ei voi rekisteröityä open: Kaikki voivat rekisteröityä security: - authorized_fetch: Vaadi todennus yhdistetyiltä palvelimilta - authorized_fetch_hint: Todennuksen vaatiminen federoiduilta palvelimilta mahdollistaa sekä käyttäjä- että palvelintason estojen tiukemman valvonnan. Tämä tapahtuu kuitenkin suorituskyvyn kustannuksella, vähentää vastauksiesi tavoittavuutta ja voi aiheuttaa yhteensopivuusongelmia joidenkin federoitujen palvelujen kanssa. Tämä ei myöskään estä omistautuneita toimijoita hakemasta julkisia julkaisujasi ja tilejäsi. + authorized_fetch: Vaadi todennus liittoutuvilta palvelimilta + authorized_fetch_hint: Todennuksen vaatiminen liittoutuvilta palvelimilta mahdollistaa sekä käyttäjä- että palvelintason estojen tiukemman valvonnan. Tämä tapahtuu kuitenkin suorituskyvyn kustannuksella, vähentää vastauksiesi tavoittavuutta ja voi aiheuttaa yhteensopivuusongelmia joidenkin liittoutuvien palvelujen kanssa. Tämä ei myöskään estä omistautuneita toimijoita hakemasta julkisia julkaisujasi ja tilejäsi. authorized_fetch_overridden_hint: Et voi tällä hetkellä muuttaa tätä asetusta, koska se on ohitettu ympäristömuuttujalla. - federation_authentication: Yhdistettyjen palvelinten todentamisen täytäntöönpano + federation_authentication: Liittoutumisen todentamisen täytäntöönpano title: Palvelimen asetukset site_uploads: delete: Poista ladattu tiedosto @@ -1239,7 +1239,7 @@ fi: deprecated_api_multiple_keywords: Näitä parametreja ei voi muuttaa tästä sovelluksesta, koska ne koskevat useampaa kuin yhtä suodattimen avainsanaa. Käytä uudempaa sovellusta tai selainkäyttöliittymää. invalid_context: Ei sisältöä tai se on virheellinen index: - contexts: Suodattimet %{contexts} + contexts: Suodattaa kontektissa %{contexts} delete: Poista empty: Sinulla ei ole suodattimia. expires_in: Vanhenee %{distance} @@ -1414,8 +1414,8 @@ fi: not_found: ei voitu löytää on_cooldown: Sinä olet jäähyllä followers_count: Seuraajat muuton aikana - incoming_migrations: Siirtyminen toiselta tililtä - incoming_migrations_html: Siirtyäksesi toisesta tilistä tähän, sinun täytyy ensin luoda tilin alias. + incoming_migrations: Muutto toiselta tililtä + incoming_migrations_html: Muuttaaksesi toisesta tilistä tähän, sinun täytyy ensin luoda tilin alias. moved_msg: Tilisi ohjaa nyt kohteeseen %{acct} ja seuraajiasi siirretään. not_redirecting: Tilisi ei ohjaa tällä hetkellä mihinkään muuhun tiliin. on_cooldown: Olet siirtänyt tilisi äskettäin. Tämä toiminto tulee saataville uudelleen %{count} päivän kuluttua. @@ -1707,7 +1707,7 @@ fi: keep_self_bookmark: Säilytä kirjanmerkkeihin lisäämäsi julkaisut keep_self_bookmark_hint: Ei poista julkaisujasi, jos olet lisännyt ne kirjanmerkkeihin keep_self_fav: Säilytä suosikkeihin lisäämäsi julkaisut - keep_self_fav_hint: Ei poista julkaisujasi, jos olet lisännyt ne suosikkeihin + keep_self_fav_hint: Ei poista julkaisujasi, jos olet lisännyt ne suosikkeihisi min_age: '1209600': 2 viikkoa '15778476': 6 kuukautta @@ -1786,7 +1786,7 @@ fi: spam: Roskaposti violation: Sisältö rikkoo seuraavia yhteisön sääntöjä explanation: - delete_statuses: Joidenkin julkaisuistasi on havaittu rikkovan ainakin yhtä yhteisön sääntöä, ja instanssin %{instance} valvojat ovat poistaneet ne. + delete_statuses: Joidenkin julkaisuistasi on havaittu rikkovan ainakin yhtä yhteisön sääntöä, joten instanssin %{instance} valvojat ovat poistaneet ne. disable: Et voi enää käyttää tiliäsi, mutta profiilisi ja muut tiedot pysyvät muuttumattomina. Voit pyytää varmuuskopiota tiedoistasi, vaihtaa tilin asetuksia tai poistaa tilisi. mark_statuses_as_sensitive: Palvelimen %{instance} valvojat ovat merkinneet osan julkaisuistasi arkaluonteisiksi. Tämä tarkoittaa sitä, että ihmisten täytyy napauttaa mediaa ennen kuin sen esikatselu näytetään. Voit merkitä median itse arkaluonteiseksi, kun julkaiset tulevaisuudessa. sensitive: Tästä lähtien kaikki ladatut mediatiedostot merkitään arkaluonteisiksi ja piilotetaan napsautusvaroituksen taakse. @@ -1807,7 +1807,7 @@ fi: disable: Tili jäädytetty mark_statuses_as_sensitive: Julkaisut merkitty arkaluonteisiksi none: Varoitus - sensitive: Tili on merkitty arkaluonteiseksi + sensitive: Tili merkitty arkaluonteiseksi silence: Tiliä rajoitettu suspend: Tili jäädytetty welcome: diff --git a/config/locales/si.yml b/config/locales/si.yml index 39368b3f85..2c2a8ae824 100644 --- a/config/locales/si.yml +++ b/config/locales/si.yml @@ -791,9 +791,9 @@ si: guide_link_text: පරිවර්තකයින්ට දායක වීමට හැකිය. sensitive_content: සංවේදී අන්තර්ගත application_mailer: - notification_preferences: ඊමේල් මනාප වෙනස් කරන්න + notification_preferences: වි-තැපැල් අභිප්‍රේත වෙනස් කරන්න salutation: "%{name}," - settings: 'ඊමේල් මනාප වෙනස් කරන්න: %{link}' + settings: 'වි-තැපැල් අභිප්‍රේත වෙනස් කරන්න: %{link}' view: 'දැක්ම:' view_profile: පැතිකඩ බලන්න view_status: ලිපිය බලන්න @@ -1405,11 +1405,11 @@ si: title: සංරක්ෂිත රැගෙන යාම suspicious_sign_in: change_password: මුරපදය වෙනස් කරන්න - details: 'පුරනය වීමේ විස්තර මෙන්න:' - explanation: අපි නව IP ලිපිනයකින් ඔබගේ ගිණුමට පුරනය වීමක් අනාවරණය කරගෙන ඇත. - further_actions_html: මෙය ඔබ නොවේ නම්, අපි ඔබට වහාම %{action} ලෙස නිර්දේශ කර ඔබගේ ගිණුම සුරක්ෂිතව තබා ගැනීමට සාධක දෙකක සත්‍යාපනය සබල කරන්න. - subject: ඔබගේ ගිණුම නව IP ලිපිනයකින් ප්‍රවේශ වී ඇත - title: නව පුරනය වීමක් + details: 'ප්‍රවේශයට අදාළ විස්තර:' + explanation: ඔබගේ ගිණුමට නව අ.ජා.කෙ. (IP) ලිපිනයකින් ප්‍රවේශයක් අනාවරණය වී ඇත. + further_actions_html: මේ ඔබ නොවේ නම්, වහාම %{action}. ඔබගේ ගිණුම සුරක්‍ෂිතව තබා ගැනීමට ද්වි-සාධකය සබල කරන්න. + subject: ඔබගේ ගිණුමට නව අ.ජා.කෙ. (IP) ලිපිනයකින් ප්‍රවේශ වී ඇත + title: නව ප්‍රවේශයක් warning: appeal: අභියාචනයක් ඉදිරිපත් කරන්න appeal_description: මෙය දෝෂයක් බව ඔබ විශ්වාස කරන්නේ නම්, ඔබට %{instance}හි කාර්ය මණ්ඩලයට අභියාචනයක් ඉදිරිපත් කළ හැක. diff --git a/config/locales/simple_form.fi.yml b/config/locales/simple_form.fi.yml index f73667f5cc..6c8875327b 100644 --- a/config/locales/simple_form.fi.yml +++ b/config/locales/simple_form.fi.yml @@ -25,7 +25,7 @@ fi: types: disable: Estä käyttäjää käyttämästä tiliään, mutta älä poista tai piilota sen sisältöä. none: Käytä tätä lähettääksesi varoituksen käyttäjälle käynnistämättä mitään muita toimintoja. - sensitive: Pakota kaikki tämän käyttäjän mediatiedostot arkaluontoisiksi. + sensitive: Pakota kaikki tämän käyttäjän mediatiedostot arkaluonteisiksi. silence: Estä käyttäjää lähettämästä viestejä julkisesti, piilota hänen viestinsä ja ilmoituksensa ihmisiltä, jotka eivät seuraa häntä. Sulkee kaikki tämän tilin raportit. suspend: Estä kaikki vuorovaikutus tältä -tai tälle tilille ja poista sen kaikki sisältö. Päätös voidaan peruuttaa 30 päivän aikana. Sulkee kaikki raportit tätä tiliä vasten. warning_preset_id: Valinnainen. Voit silti lisätä mukautetun tekstin esiasetuksen loppuun @@ -79,14 +79,14 @@ fi: activity_api_enabled: Paikallisesti julkaistujen julkaisujen, aktiivisten käyttäjien ja rekisteröitymisten viikoittainen määrä backups_retention_period: Säilytä luodut arkistot määritetyn määrän päiviä. bootstrap_timeline_accounts: Nämä tilit kiinnitetään uusien käyttäjien seuraamissuositusten yläpuolelle. - closed_registrations_message: Näkyy, kun ilmoittautuminen on suljettu - content_cache_retention_period: Viestit muilta palvelimilta poistetaan määritetyn määrän päiviä jälkeen, kun arvo on asetettu positiiviseksi. Tämä voi olla peruuttamatonta. + closed_registrations_message: Näkyy, kun rekisteröityminen on suljettu + content_cache_retention_period: Kaikki julkaisut ja tehostukset muilta palvelimilta poistetaan, kun määritelty määrä päiviä on kulunut. Osaa julkaisuista voi olla mahdoton palauttaa. Kaikki julkaisuihin liittyvät kirjanmerkit, suosikit ja tehostukset menetetään, eikä niitä voi palauttaa. custom_css: Voit käyttää mukautettuja tyylejä Mastodonin verkkoversiossa. mascot: Ohittaa kuvituksen edistyneessä selainkäyttöliittymässä. media_cache_retention_period: Ladatut mediatiedostot poistetaan määritetyn määrän päiviä jälkeen, kun arvo on positiivinen ja ladataan uudelleen pyynnöstä. - peers_api_enabled: Luettelo verkkotunnuksista, jotka tämä palvelin on kohdannut fediversumissa. Se ei kerro, oletko liitossa tietyn palvelimen kanssa, vaan että palvelimesi on ylipäätään tietoinen siitä. Tätä tietoa käytetään palveluissa, jotka keräävät tilastoja liittoutumisesta yleisellä tasolla. + peers_api_enabled: Luettelo verkkotunnuksista, jotka tämä palvelin on kohdannut fediversumissa. Se ei kerro, oletko liitossa tietyn palvelimen kanssa, vaan että palvelimesi on ylipäätään tietoinen siitä. Tätä tietoa käytetään palveluissa, jotka keräävät tilastoja federoinnista yleisellä tasolla. profile_directory: Profiilihakemisto lueteloi kaikki käyttäjät, jotka ovat ilmoittaneet olevansa löydettävissä. - require_invite_text: Kun kirjautuminen vaatii manuaalisen hyväksynnän, tee ”Miksi haluat liittyä?” teksti syötetään pakolliseksi eikä vapaaehtoiseksi + require_invite_text: Kun rekisteröityminen vaatii manuaalisen hyväksynnän, tee ”Miksi haluat liittyä?” -tekstikentästä pakollinen vapaaehtoisen sijaan site_contact_email: Kuinka ihmiset voivat tavoittaa sinut oikeudellisissa tai tukikysymyksissä. site_contact_username: Miten ihmiset voivat tavoittaa sinut Mastodonissa. site_extended_description: Kaikki lisätiedot, jotka voivat olla hyödyllisiä kävijöille ja käyttäjille. Voidaan jäsentää Markdown-syntaksilla. @@ -96,10 +96,10 @@ fi: status_page_url: URL-osoite sivulle, jonka kautta tämän palvelimen tila voidaan ongelmatilanteissa tarkastaa theme: Teema, jonka uloskirjautuneet vierailijat ja uudet käyttäjät näkevät. thumbnail: Noin 2:1 kuva näytetään palvelimen tietojen rinnalla. - timeline_preview: Uloskirjautuneet vierailijat voivat selata uusimpia julkisia viestejä, jotka ovat saatavilla palvelimella. + timeline_preview: Uloskirjautuneet vierailijat voivat selata uusimpia julkisia julkaisuja, jotka ovat saatavilla palvelimella. trendable_by_default: Ohita suositun sisällön manuaalinen tarkistus. Yksittäisiä kohteita voidaan edelleen poistaa jälkikäteen. trends: Trendit osoittavat, mitkä julkaisut, aihetunnisteet ja uutiset ovat saamassa vetoa palvelimellasi. - trends_as_landing_page: Näytä vierailijoille ja uloskirjautuneille käyttäjille suosittu sisältö palvelininstanssin kuvaustekstin sijaan. Edellytyksenä on, että suosittu sisältö -ominaisuus on käytössä. + trends_as_landing_page: Näytä vierailijoille ja uloskirjautuneille käyttäjille suosittua sisältöä palvelimen kuvauksen sijaan. Edellyttää, että trendit on otettu käyttöön. form_challenge: current_password: Olet menossa suojatulle alueelle imports: @@ -129,9 +129,9 @@ fi: chosen_languages: Kun valittu, vain valituilla kielillä kirjoitetut julkaisut näkyvät julkisilla aikajanoilla role: Rooli määrää, mitkä käyttöoikeudet käyttäjällä on user_role: - color: Väri, jota käytetään roolin koko käyttöliittymässä, RGB heksamuodossa + color: Väri, jota käytetään roolille kaikkialla käyttöliittymässä, RGB-heksadesimaalimuodossa highlighted: Tämä tekee roolista julkisesti näkyvän - name: Roolin julkinen nimi, jos rooli on asetettu näytettäväksi mekkinä + name: Roolin julkinen nimi, jos rooli on asetettu näytettäväksi merkkinä permissions_as_keys: Käyttäjillä, joilla on tämä rooli, on käyttöoikeus... position: Korkeampi rooli ratkaisee konfliktit tietyissä tilanteissa. Tiettyjä toimintoja voidaan suorittaa vain rooleille, joiden prioriteetti on pienempi webhook: @@ -238,10 +238,10 @@ fi: hide: Piilota kokonaan warn: Piilota ja näytä varoitus form_admin_settings: - activity_api_enabled: Julkaise yhteenlasketut tilastot käyttäjätoiminnasta rajapinnassa + activity_api_enabled: Julkaise yhteenlasketut tilastot käyttäjätoiminnasta ohjelmointirajapinnassa backups_retention_period: Käyttäjän arkiston säilytysaika bootstrap_timeline_accounts: Suosittele aina näitä tilejä uusille käyttäjille - closed_registrations_message: Mukautettu viesti, kun kirjautumisia ei ole saatavilla + closed_registrations_message: Mukautettu viesti, kun rekisteröityminen ei ole saatavilla content_cache_retention_period: Sisällön välimuistin säilytysaika custom_css: Mukautettu CSS mascot: Mukautettu maskotti (legacy) @@ -251,7 +251,7 @@ fi: registrations_mode: Kuka voi rekisteröityä require_invite_text: Vaadi syy liittyä show_domain_blocks: Näytä verkkotunnusten estot - show_domain_blocks_rationale: Näytä miksi verkkotunnukset on estetty + show_domain_blocks_rationale: Näytä, miksi verkkotunnukset on estetty site_contact_email: Ota yhteyttä sähköpostilla site_contact_username: Yhteyshenkilön käyttäjänimi site_extended_description: Laajennettu kuvaus @@ -261,10 +261,10 @@ fi: status_page_url: Tilasivun URL-osoite theme: Oletusteema thumbnail: Palvelimen pikkukuva - timeline_preview: Salli todentamaton pääsy julkiselle aikajanalle + timeline_preview: Salli todentamaton pääsy julkisille aikajanoille trendable_by_default: Salli trendit ilman ennakkotarkastusta - trends: Trendit käyttöön - trends_as_landing_page: Käytä suosittua sisältöä aloitussivuna + trends: Ota trendit käyttöön + trends_as_landing_page: Käytä trendejä aloitussivuna interactions: must_be_follower: Estä ilmoitukset käyttäjiltä, jotka eivät seuraa sinua must_be_following: Estä ilmoitukset käyttäjiltä, joita et seuraa @@ -313,7 +313,7 @@ fi: time_zone: Aikavyöhyke user_role: color: Merkin väri - highlighted: Näyttä rooli merkkinä käyttäjäprofiileissa + highlighted: Näytä rooli merkkinä käyttäjäprofiileissa name: Nimi permissions_as_keys: Oikeudet position: Prioriteetti diff --git a/config/locales/simple_form.zh-TW.yml b/config/locales/simple_form.zh-TW.yml index 5108a97452..c48c659fa6 100644 --- a/config/locales/simple_form.zh-TW.yml +++ b/config/locales/simple_form.zh-TW.yml @@ -5,7 +5,7 @@ zh-TW: account: discoverable: 公開嘟文及個人檔案可能於各 Mastodon 功能中被推薦,並且您的個人檔案可能被推薦至其他使用者。 display_name: 完整名稱或暱稱。 - fields: 烘培雞,自我認同代稱,年齡,及任何您想分享的。 + fields: 烘培雞、自我認同代稱、年齡,及任何您想分享的。 indexable: 您的公開嘟文可能會顯示於 Mastodon 之搜尋結果中。曾與您嘟文互動過的人可能無論如何都能搜尋它們。 note: '您可以 @mention 其他人或者使用 #主題標籤。' show_collections: 人們將能瀏覽您跟隨中及跟隨者帳號。您所跟隨之人能得知您正在跟隨其帳號。 @@ -31,7 +31,7 @@ zh-TW: warning_preset_id: 選用。您仍可在預設的結尾新增自訂文字 announcement: all_day: 核取後,只會顯示出時間範圍中的日期部分 - ends_at: 可選的,公告會在該時間點自動取消發布 + ends_at: 可選的,公告會於該時間點自動取消發布 scheduled_at: 空白則立即發布公告 starts_at: 可選的,讓公告在特定時間範圍內顯示 text: 您可以使用嘟文語法,但請小心別讓公告太鴨霸而佔據使用者的整個版面。 @@ -59,8 +59,8 @@ zh-TW: setting_display_media_default: 隱藏標為敏感內容的媒體 setting_display_media_hide_all: 總是隱藏所有媒體 setting_display_media_show_all: 總是顯示標為敏感內容的媒體 - setting_use_blurhash: 彩色漸層圖樣是基於隱藏媒體內容顏色產生,所有細節會變得模糊 - setting_use_pending_items: 關閉自動捲動更新,時間軸只會在點擊後更新 + setting_use_blurhash: 彩色漸層圖樣是基於隱藏媒體內容顏色產生,所有細節將變得模糊 + setting_use_pending_items: 關閉自動捲動更新,時間軸只會於點擊後更新 username: 您可以使用字幕、數字與底線 whole_word: 如果關鍵字或詞組僅有字母與數字,則其將只在符合整個單字的時候才會套用 domain_allow: @@ -126,7 +126,7 @@ zh-TW: tag: name: 您只能變更大小寫,例如,以使其更易讀。 user: - chosen_languages: 當選取時,只有選取語言之嘟文會在公開時間軸中顯示 + chosen_languages: 當選取時,只有選取語言之嘟文會於公開時間軸中顯示 role: 角色控制使用者有哪些權限 user_role: color: 在整個使用者介面中用於角色的顏色,十六進位格式的 RGB diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 78b9b87db3..c25fab6aeb 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -353,6 +353,7 @@ sk: silence: Obmedz suspend: Vylúč title: Nové blokovanie domény + not_permitted: Nemáš povolenie na vykonanie tohto kroku obfuscate: Zatemniť názov domény private_comment: Súkromný komentár private_comment_hint: Odôvodni toto doménové obmedzenie, pre vnútorné vyrozumenie moderátorov. diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 33870c0bb2..e139742ff1 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -390,7 +390,7 @@ zh-TW: domain: 站點 edit: 更改封鎖的站台 existing_domain_block: 您已對 %{name} 施加更嚴格的限制。 - existing_domain_block_html: 您已對 %{name} 施加更嚴格的限制,您需要先 解除封鎖。 + existing_domain_block_html: 您已對 %{name} 施加更嚴格的限制,您需要先解除封鎖。 export: 匯出 import: 匯入 new: @@ -451,9 +451,7 @@ zh-TW: title: 匯入網域黑名單 no_file: 尚未選擇檔案 follow_recommendations: - description_html: |- - 跟隨建議幫助新使用者們快速找到有趣的內容。當使用者沒有與其他帳號有足夠多的互動以建立個人化跟隨建議時,這些帳號將會被推薦。這些帳號將基於某選定語言之高互動和高本地跟隨者數量帳號而 - 每日重新更新。 + description_html: "跟隨建議幫助新使用者們快速找到有趣的內容。當使用者沒有與其他帳號有足夠多的互動以建立個人化跟隨建議時,這些帳號將會被推薦。這些帳號將基於某選定語言之高互動和高本地跟隨者數量帳號而每日重新更新。" language: 對於語言 status: 狀態 suppress: 取消跟隨建議 @@ -553,7 +551,7 @@ zh-TW: relays: add_new: 新增中繼站 delete: 刪除 - description_html: "聯邦中繼站 是種中繼伺服器,會在訂閱並推送至此中繼站的伺服器之間交換大量的公開嘟文。中繼站也能協助小型或中型伺服器從聯邦宇宙中探索內容,而無須本地使用者手動跟隨遠端伺服器的其他使用者。" + description_html: "聯邦中繼站 是種中繼伺服器,會於訂閱並推送至此中繼站的伺服器之間交換大量的公開嘟文。中繼站也能協助小型或中型伺服器從聯邦宇宙中探索內容,而無須本地使用者手動跟隨遠端伺服器的其他使用者。" disable: 停用 disabled: 停用 enable: 啟用 @@ -988,7 +986,7 @@ zh-TW: aliases: add_new: 建立別名 created_msg: 成功建立別名。您可以自舊帳號開始轉移。 - deleted_msg: 成功移除別名。您將無法再由舊帳號轉移到目前的帳號。 + deleted_msg: 成功移除別名。您將無法再由舊帳號轉移至目前的帳號。 empty: 您目前沒有任何別名。 hint_html: 如果想由其他帳號轉移至此帳號,您可以在此處新增別名,稍後系統將容許您將跟隨者由舊帳號轉移至此。此項作業是無害且可復原的帳號的遷移程序需要在舊帳號啟動。 remove: 取消連結別名 @@ -999,7 +997,7 @@ zh-TW: confirmation_dialogs: 確認對話框 discovery: 探索 localization: - body: Mastodon 是由志願者翻譯的。 + body: Mastodon 是由志願者所翻譯。 guide_link: https://crowdin.com/project/mastodon guide_link_text: 每個人都能貢獻。 sensitive_content: 敏感內容 @@ -1042,8 +1040,8 @@ zh-TW: log_in_with: 登入,使用 login: 登入 logout: 登出 - migrate_account: 轉移到另一個帳號 - migrate_account_html: 如果您希望引導他人跟隨另一個帳號,請 到這裡設定。 + migrate_account: 轉移至另一個帳號 + migrate_account_html: 如果您希望引導他人跟隨另一個帳號,請至這裡設定。 or_log_in_with: 或透過其他方式登入 privacy_policy_agreement_html: 我已閱讀且同意 隱私權政策 progress: @@ -1072,7 +1070,7 @@ zh-TW: email_below_hint_html: 請檢查您的垃圾郵件資料夾,或是請求另一個。如果是錯的,您可以更正您的電子郵件地址。 email_settings_hint_html: 請點擊我們寄給您連結以驗證 %{email}。我們將於此稍候。 link_not_received: 無法取得連結嗎? - new_confirmation_instructions_sent: 您將會在幾分鐘之內收到新的包含確認連結的電子郵件! + new_confirmation_instructions_sent: 您將於幾分鐘之內收到新的包含確認連結的電子郵件! title: 請檢查您的收件匣 sign_in: preamble_html: 請使用您於 %{domain} 的帳號密碼登入。若您的帳號託管於其他伺服器,您將無法在此登入。 @@ -1084,7 +1082,7 @@ zh-TW: status: account_status: 帳號狀態 confirming: 等待電子郵件確認完成。 - functional: 您的帳號可以正常使用了。 + functional: "您的帳號可以正常使用了。🎉" pending: 管管們正在處理您的申請,這可能需要一點時間處理。我們將於申請通過後以電子郵件方式通知您。 redirecting_to: 您的帳號因目前重定向至 %{acct} 而被停用。 view_strikes: 檢視針對您帳號過去的警示 @@ -1410,7 +1408,7 @@ zh-TW: followers: 此動作將會將目前帳號的所有跟隨者轉移至新帳號 only_redirect_html: 或者,您也可以僅在您的個人檔案中設定重新導向。 other_data: 其他資料並不會自動轉移 - redirect: 您目前的帳號將會在個人檔案頁面新增重新導向公告,並會被排除在搜尋結果之外 + redirect: 您目前的帳號將於個人檔案頁面新增重新導向公告,並會被排除在搜尋結果之外 moderation: title: 站務 move_handler: @@ -1499,9 +1497,7 @@ zh-TW: posting_defaults: 嘟文預設值 public_timelines: 公開時間軸 privacy: - hint_html: |- - 自訂您希望如何讓您的個人檔案及嘟文被找到。 - 藉由啟用一系列 Mastodon 功能以幫助您觸及更廣的受眾。煩請花些時間確認您是否欲啟用這些設定。 + hint_html: "自訂您希望如何讓您的個人檔案及嘟文被發現。藉由啟用一系列 Mastodon 功能以幫助您觸及更廣的受眾。煩請花些時間確認您是否欲啟用這些設定。" privacy: 隱私權 privacy_hint_html: 控制您希望向其他人揭露之內容。人們透過瀏覽其他人的跟隨者與其發嘟之應用程式發現有趣的個人檔案和酷炫的 Mastodon 應用程式,但您能選擇將其隱藏。 reach: 觸及 @@ -1665,7 +1661,7 @@ zh-TW: enabled: 自動刪除舊嘟文 enabled_hint: 一旦達到指定的保存期限,就會自動刪除您的嘟文,除非該嘟文符合下列例外 exceptions: 例外 - explanation: 因為刪除嘟文是耗費資源的操作,當伺服器不那麼忙碌時才會慢慢完成。因此,您的嘟文會在到達保存期限後一段時間才會被刪除。 + explanation: 因為刪除嘟文是耗費資源的操作,當伺服器不那麼忙碌時才會慢慢完成。因此,您的嘟文將於到達保存期限後一段時間才會被刪除。 ignore_favs: 忽略最愛數 ignore_reblogs: 忽略轉嘟數 interaction_exceptions: 基於互動的例外規則 From 23f8e93c6405277515d018d4ff2d550fe64ac159 Mon Sep 17 00:00:00 2001 From: Wladimir Palant Date: Mon, 16 Oct 2023 13:39:25 +0200 Subject: [PATCH 04/35] Fixes #23135 - Allow cross origin request for /nodeinfo/2.0 API (#27413) --- config/initializers/cors.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb index 3d94e38e8e..6424477846 100644 --- a/config/initializers/cors.rb +++ b/config/initializers/cors.rb @@ -14,6 +14,7 @@ Rails.application.config.middleware.insert_before 0, Rack::Cors do with_options headers: :any, credentials: false do with_options methods: [:get] do resource '/.well-known/*' + resource '/nodeinfo/*' resource '/@:username' resource '/users/:username' end From 299aa71c8f85542ace4d9cb61cbdf896ff342213 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 16 Oct 2023 15:24:14 +0200 Subject: [PATCH 05/35] Fix handling of `inLanguage` attribute in preview card processing (#27423) --- app/lib/link_details_extractor.rb | 3 ++- spec/lib/link_details_extractor_spec.rb | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/lib/link_details_extractor.rb b/app/lib/link_details_extractor.rb index b95ec80519..a96612cab0 100644 --- a/app/lib/link_details_extractor.rb +++ b/app/lib/link_details_extractor.rb @@ -36,7 +36,8 @@ class LinkDetailsExtractor end def language - json['inLanguage'] + lang = json['inLanguage'] + lang.is_a?(Hash) ? (lang['alternateName'] || lang['name']) : lang end def type diff --git a/spec/lib/link_details_extractor_spec.rb b/spec/lib/link_details_extractor_spec.rb index 599bc4e6de..8c485cef2a 100644 --- a/spec/lib/link_details_extractor_spec.rb +++ b/spec/lib/link_details_extractor_spec.rb @@ -82,6 +82,10 @@ RSpec.describe LinkDetailsExtractor do 'name' => 'Pet News', 'url' => 'https://example.com', }, + 'inLanguage' => { + name: 'English', + alternateName: 'en', + }, }.to_json end @@ -115,6 +119,12 @@ RSpec.describe LinkDetailsExtractor do expect(subject.provider_name).to eq 'Pet News' end end + + describe '#language' do + it 'returns the language from structured data' do + expect(subject.language).to eq 'en' + end + end end context 'when is wrapped in CDATA tags' do From 00e92b40386e9ced25e5a6cad58f80dc78aca636 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Oct 2023 09:37:22 -0400 Subject: [PATCH 06/35] Add coverage for `CLI::Statuses` command (#25321) --- spec/lib/mastodon/cli/statuses_spec.rb | 22 ++++++++++++++++++++++ spec/rails_helper.rb | 6 ++++++ 2 files changed, 28 insertions(+) diff --git a/spec/lib/mastodon/cli/statuses_spec.rb b/spec/lib/mastodon/cli/statuses_spec.rb index 2430a88416..38ebcd9934 100644 --- a/spec/lib/mastodon/cli/statuses_spec.rb +++ b/spec/lib/mastodon/cli/statuses_spec.rb @@ -4,9 +4,31 @@ require 'rails_helper' require 'mastodon/cli/statuses' describe Mastodon::CLI::Statuses do + let(:cli) { described_class.new } + describe '.exit_on_failure?' do it 'returns true' do expect(described_class.exit_on_failure?).to be true end end + + describe '#remove', use_transactional_tests: false do + context 'with small batch size' do + let(:options) { { batch_size: 0 } } + + it 'exits with error message' do + expect { cli.invoke :remove, [], options }.to output( + a_string_including('Cannot run') + ).to_stdout.and raise_error(SystemExit) + end + end + + context 'with default batch size' do + it 'removes unreferenced statuses' do + expect { cli.invoke :remove }.to output( + a_string_including('Done after') + ).to_stdout + end + end + end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 06e8418a0e..0bbf8fc52d 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -85,6 +85,12 @@ RSpec.configure do |config| config.include Redisable config.include SignedRequestHelpers, type: :request + config.around(:each, use_transactional_tests: false) do |example| + self.use_transactional_tests = false + example.run + self.use_transactional_tests = true + end + config.before :each, type: :cli do stub_stdout stub_reset_connection_pools From f8afa0f614a1ec4637d98baf6125a7fc1c47c23c Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Oct 2023 09:38:27 -0400 Subject: [PATCH 07/35] Remove unused stub json ld context (#25454) --- .../requests/json-ld.activitystreams.txt | 391 ------------------ spec/fixtures/requests/json-ld.identity.txt | 100 ----- spec/fixtures/requests/json-ld.security.txt | 61 --- .../activitypub/linked_data_signature_spec.rb | 4 - spec/rails_helper.rb | 14 - ..._block_domain_from_account_service_spec.rb | 1 - 6 files changed, 571 deletions(-) delete mode 100644 spec/fixtures/requests/json-ld.activitystreams.txt delete mode 100644 spec/fixtures/requests/json-ld.identity.txt delete mode 100644 spec/fixtures/requests/json-ld.security.txt diff --git a/spec/fixtures/requests/json-ld.activitystreams.txt b/spec/fixtures/requests/json-ld.activitystreams.txt deleted file mode 100644 index 395797b272..0000000000 --- a/spec/fixtures/requests/json-ld.activitystreams.txt +++ /dev/null @@ -1,391 +0,0 @@ -HTTP/1.1 200 OK -Date: Tue, 01 May 2018 23:25:57 GMT -Content-Location: activitystreams.jsonld -Vary: negotiate,accept -TCN: choice -Last-Modified: Mon, 16 Apr 2018 00:28:23 GMT -ETag: "1eb0-569ec4caa97c0;d3-540ee27e0eec0" -Accept-Ranges: bytes -Content-Length: 7856 -Cache-Control: max-age=21600 -Expires: Wed, 02 May 2018 05:25:57 GMT -P3P: policyref="http://www.w3.org/2014/08/p3p.xml" -Access-Control-Allow-Origin: * -Content-Type: application/ld+json -Strict-Transport-Security: max-age=15552000; includeSubdomains; preload -Content-Security-Policy: upgrade-insecure-requests - -{ - "@context": { - "@vocab": "_:", - "xsd": "http://www.w3.org/2001/XMLSchema#", - "as": "https://www.w3.org/ns/activitystreams#", - "ldp": "http://www.w3.org/ns/ldp#", - "id": "@id", - "type": "@type", - "Accept": "as:Accept", - "Activity": "as:Activity", - "IntransitiveActivity": "as:IntransitiveActivity", - "Add": "as:Add", - "Announce": "as:Announce", - "Application": "as:Application", - "Arrive": "as:Arrive", - "Article": "as:Article", - "Audio": "as:Audio", - "Block": "as:Block", - "Collection": "as:Collection", - "CollectionPage": "as:CollectionPage", - "Relationship": "as:Relationship", - "Create": "as:Create", - "Delete": "as:Delete", - "Dislike": "as:Dislike", - "Document": "as:Document", - "Event": "as:Event", - "Follow": "as:Follow", - "Flag": "as:Flag", - "Group": "as:Group", - "Ignore": "as:Ignore", - "Image": "as:Image", - "Invite": "as:Invite", - "Join": "as:Join", - "Leave": "as:Leave", - "Like": "as:Like", - "Link": "as:Link", - "Mention": "as:Mention", - "Note": "as:Note", - "Object": "as:Object", - "Offer": "as:Offer", - "OrderedCollection": "as:OrderedCollection", - "OrderedCollectionPage": "as:OrderedCollectionPage", - "Organization": "as:Organization", - "Page": "as:Page", - "Person": "as:Person", - "Place": "as:Place", - "Profile": "as:Profile", - "Question": "as:Question", - "Reject": "as:Reject", - "Remove": "as:Remove", - "Service": "as:Service", - "TentativeAccept": "as:TentativeAccept", - "TentativeReject": "as:TentativeReject", - "Tombstone": "as:Tombstone", - "Undo": "as:Undo", - "Update": "as:Update", - "Video": "as:Video", - "View": "as:View", - "Listen": "as:Listen", - "Read": "as:Read", - "Move": "as:Move", - "Travel": "as:Travel", - "IsFollowing": "as:IsFollowing", - "IsFollowedBy": "as:IsFollowedBy", - "IsContact": "as:IsContact", - "IsMember": "as:IsMember", - "subject": { - "@id": "as:subject", - "@type": "@id" - }, - "relationship": { - "@id": "as:relationship", - "@type": "@id" - }, - "actor": { - "@id": "as:actor", - "@type": "@id" - }, - "attributedTo": { - "@id": "as:attributedTo", - "@type": "@id" - }, - "attachment": { - "@id": "as:attachment", - "@type": "@id" - }, - "bcc": { - "@id": "as:bcc", - "@type": "@id" - }, - "bto": { - "@id": "as:bto", - "@type": "@id" - }, - "cc": { - "@id": "as:cc", - "@type": "@id" - }, - "context": { - "@id": "as:context", - "@type": "@id" - }, - "current": { - "@id": "as:current", - "@type": "@id" - }, - "first": { - "@id": "as:first", - "@type": "@id" - }, - "generator": { - "@id": "as:generator", - "@type": "@id" - }, - "icon": { - "@id": "as:icon", - "@type": "@id" - }, - "image": { - "@id": "as:image", - "@type": "@id" - }, - "inReplyTo": { - "@id": "as:inReplyTo", - "@type": "@id" - }, - "items": { - "@id": "as:items", - "@type": "@id" - }, - "instrument": { - "@id": "as:instrument", - "@type": "@id" - }, - "orderedItems": { - "@id": "as:items", - "@type": "@id", - "@container": "@list" - }, - "last": { - "@id": "as:last", - "@type": "@id" - }, - "location": { - "@id": "as:location", - "@type": "@id" - }, - "next": { - "@id": "as:next", - "@type": "@id" - }, - "object": { - "@id": "as:object", - "@type": "@id" - }, - "oneOf": { - "@id": "as:oneOf", - "@type": "@id" - }, - "anyOf": { - "@id": "as:anyOf", - "@type": "@id" - }, - "closed": { - "@id": "as:closed", - "@type": "xsd:dateTime" - }, - "origin": { - "@id": "as:origin", - "@type": "@id" - }, - "accuracy": { - "@id": "as:accuracy", - "@type": "xsd:float" - }, - "prev": { - "@id": "as:prev", - "@type": "@id" - }, - "preview": { - "@id": "as:preview", - "@type": "@id" - }, - "replies": { - "@id": "as:replies", - "@type": "@id" - }, - "result": { - "@id": "as:result", - "@type": "@id" - }, - "audience": { - "@id": "as:audience", - "@type": "@id" - }, - "partOf": { - "@id": "as:partOf", - "@type": "@id" - }, - "tag": { - "@id": "as:tag", - "@type": "@id" - }, - "target": { - "@id": "as:target", - "@type": "@id" - }, - "to": { - "@id": "as:to", - "@type": "@id" - }, - "url": { - "@id": "as:url", - "@type": "@id" - }, - "altitude": { - "@id": "as:altitude", - "@type": "xsd:float" - }, - "content": "as:content", - "contentMap": { - "@id": "as:content", - "@container": "@language" - }, - "name": "as:name", - "nameMap": { - "@id": "as:name", - "@container": "@language" - }, - "duration": { - "@id": "as:duration", - "@type": "xsd:duration" - }, - "endTime": { - "@id": "as:endTime", - "@type": "xsd:dateTime" - }, - "height": { - "@id": "as:height", - "@type": "xsd:nonNegativeInteger" - }, - "href": { - "@id": "as:href", - "@type": "@id" - }, - "hreflang": "as:hreflang", - "latitude": { - "@id": "as:latitude", - "@type": "xsd:float" - }, - "longitude": { - "@id": "as:longitude", - "@type": "xsd:float" - }, - "mediaType": "as:mediaType", - "published": { - "@id": "as:published", - "@type": "xsd:dateTime" - }, - "radius": { - "@id": "as:radius", - "@type": "xsd:float" - }, - "rel": "as:rel", - "startIndex": { - "@id": "as:startIndex", - "@type": "xsd:nonNegativeInteger" - }, - "startTime": { - "@id": "as:startTime", - "@type": "xsd:dateTime" - }, - "summary": "as:summary", - "summaryMap": { - "@id": "as:summary", - "@container": "@language" - }, - "totalItems": { - "@id": "as:totalItems", - "@type": "xsd:nonNegativeInteger" - }, - "units": "as:units", - "updated": { - "@id": "as:updated", - "@type": "xsd:dateTime" - }, - "width": { - "@id": "as:width", - "@type": "xsd:nonNegativeInteger" - }, - "describes": { - "@id": "as:describes", - "@type": "@id" - }, - "formerType": { - "@id": "as:formerType", - "@type": "@id" - }, - "deleted": { - "@id": "as:deleted", - "@type": "xsd:dateTime" - }, - "inbox": { - "@id": "ldp:inbox", - "@type": "@id" - }, - "outbox": { - "@id": "as:outbox", - "@type": "@id" - }, - "following": { - "@id": "as:following", - "@type": "@id" - }, - "followers": { - "@id": "as:followers", - "@type": "@id" - }, - "streams": { - "@id": "as:streams", - "@type": "@id" - }, - "preferredUsername": "as:preferredUsername", - "endpoints": { - "@id": "as:endpoints", - "@type": "@id" - }, - "uploadMedia": { - "@id": "as:uploadMedia", - "@type": "@id" - }, - "proxyUrl": { - "@id": "as:proxyUrl", - "@type": "@id" - }, - "liked": { - "@id": "as:liked", - "@type": "@id" - }, - "oauthAuthorizationEndpoint": { - "@id": "as:oauthAuthorizationEndpoint", - "@type": "@id" - }, - "oauthTokenEndpoint": { - "@id": "as:oauthTokenEndpoint", - "@type": "@id" - }, - "provideClientKey": { - "@id": "as:provideClientKey", - "@type": "@id" - }, - "signClientKey": { - "@id": "as:signClientKey", - "@type": "@id" - }, - "sharedInbox": { - "@id": "as:sharedInbox", - "@type": "@id" - }, - "Public": { - "@id": "as:Public", - "@type": "@id" - }, - "source": "as:source", - "likes": { - "@id": "as:likes", - "@type": "@id" - }, - "shares": { - "@id": "as:shares", - "@type": "@id" - } - } -} diff --git a/spec/fixtures/requests/json-ld.identity.txt b/spec/fixtures/requests/json-ld.identity.txt deleted file mode 100644 index 8810526cb1..0000000000 --- a/spec/fixtures/requests/json-ld.identity.txt +++ /dev/null @@ -1,100 +0,0 @@ -HTTP/1.1 200 OK -Accept-Ranges: bytes -Access-Control-Allow-Headers: DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Accept-Encoding -Access-Control-Allow-Origin: * -Content-Type: application/ld+json -Date: Tue, 01 May 2018 23:28:21 GMT -Etag: "e26-547a6fc75b04a-gzip" -Last-Modified: Fri, 03 Feb 2017 21:30:09 GMT -Server: Apache/2.4.7 (Ubuntu) -Vary: Accept-Encoding -Transfer-Encoding: chunked - -{ - "@context": { - "id": "@id", - "type": "@type", - - "cred": "https://w3id.org/credentials#", - "dc": "http://purl.org/dc/terms/", - "identity": "https://w3id.org/identity#", - "perm": "https://w3id.org/permissions#", - "ps": "https://w3id.org/payswarm#", - "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - "rdfs": "http://www.w3.org/2000/01/rdf-schema#", - "sec": "https://w3id.org/security#", - "schema": "http://schema.org/", - "xsd": "http://www.w3.org/2001/XMLSchema#", - - "Group": "https://www.w3.org/ns/activitystreams#Group", - - "claim": {"@id": "cred:claim", "@type": "@id"}, - "credential": {"@id": "cred:credential", "@type": "@id"}, - "issued": {"@id": "cred:issued", "@type": "xsd:dateTime"}, - "issuer": {"@id": "cred:issuer", "@type": "@id"}, - "recipient": {"@id": "cred:recipient", "@type": "@id"}, - "Credential": "cred:Credential", - "CryptographicKeyCredential": "cred:CryptographicKeyCredential", - - "about": {"@id": "schema:about", "@type": "@id"}, - "address": {"@id": "schema:address", "@type": "@id"}, - "addressCountry": "schema:addressCountry", - "addressLocality": "schema:addressLocality", - "addressRegion": "schema:addressRegion", - "comment": "rdfs:comment", - "created": {"@id": "dc:created", "@type": "xsd:dateTime"}, - "creator": {"@id": "dc:creator", "@type": "@id"}, - "description": "schema:description", - "email": "schema:email", - "familyName": "schema:familyName", - "givenName": "schema:givenName", - "image": {"@id": "schema:image", "@type": "@id"}, - "label": "rdfs:label", - "name": "schema:name", - "postalCode": "schema:postalCode", - "streetAddress": "schema:streetAddress", - "title": "dc:title", - "url": {"@id": "schema:url", "@type": "@id"}, - "Person": "schema:Person", - "PostalAddress": "schema:PostalAddress", - "Organization": "schema:Organization", - - "identityService": {"@id": "identity:identityService", "@type": "@id"}, - "idp": {"@id": "identity:idp", "@type": "@id"}, - "Identity": "identity:Identity", - - "paymentProcessor": "ps:processor", - "preferences": {"@id": "ps:preferences", "@type": "@vocab"}, - - "cipherAlgorithm": "sec:cipherAlgorithm", - "cipherData": "sec:cipherData", - "cipherKey": "sec:cipherKey", - "digestAlgorithm": "sec:digestAlgorithm", - "digestValue": "sec:digestValue", - "domain": "sec:domain", - "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, - "initializationVector": "sec:initializationVector", - "member": {"@id": "schema:member", "@type": "@id"}, - "memberOf": {"@id": "schema:memberOf", "@type": "@id"}, - "nonce": "sec:nonce", - "normalizationAlgorithm": "sec:normalizationAlgorithm", - "owner": {"@id": "sec:owner", "@type": "@id"}, - "password": "sec:password", - "privateKey": {"@id": "sec:privateKey", "@type": "@id"}, - "privateKeyPem": "sec:privateKeyPem", - "publicKey": {"@id": "sec:publicKey", "@type": "@id"}, - "publicKeyPem": "sec:publicKeyPem", - "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"}, - "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"}, - "signature": "sec:signature", - "signatureAlgorithm": "sec:signatureAlgorithm", - "signatureValue": "sec:signatureValue", - "CryptographicKey": "sec:Key", - "EncryptedMessage": "sec:EncryptedMessage", - "GraphSignature2012": "sec:GraphSignature2012", - "LinkedDataSignature2015": "sec:LinkedDataSignature2015", - - "accessControl": {"@id": "perm:accessControl", "@type": "@id"}, - "writePermission": {"@id": "perm:writePermission", "@type": "@id"} - } -} diff --git a/spec/fixtures/requests/json-ld.security.txt b/spec/fixtures/requests/json-ld.security.txt deleted file mode 100644 index 0d29903e60..0000000000 --- a/spec/fixtures/requests/json-ld.security.txt +++ /dev/null @@ -1,61 +0,0 @@ -HTTP/1.1 200 OK -Accept-Ranges: bytes -Access-Control-Allow-Headers: DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Accept-Encoding -Access-Control-Allow-Origin: * -Content-Type: application/ld+json -Date: Wed, 02 May 2018 16:25:32 GMT -Etag: "7e3-5651ec0f7c5ed-gzip" -Last-Modified: Tue, 13 Feb 2018 21:34:04 GMT -Server: Apache/2.4.7 (Ubuntu) -Vary: Accept-Encoding -Content-Length: 2019 - -{ - "@context": { - "id": "@id", - "type": "@type", - - "dc": "http://purl.org/dc/terms/", - "sec": "https://w3id.org/security#", - "xsd": "http://www.w3.org/2001/XMLSchema#", - - "EcdsaKoblitzSignature2016": "sec:EcdsaKoblitzSignature2016", - "Ed25519Signature2018": "sec:Ed25519Signature2018", - "EncryptedMessage": "sec:EncryptedMessage", - "GraphSignature2012": "sec:GraphSignature2012", - "LinkedDataSignature2015": "sec:LinkedDataSignature2015", - "LinkedDataSignature2016": "sec:LinkedDataSignature2016", - "CryptographicKey": "sec:Key", - - "authenticationTag": "sec:authenticationTag", - "canonicalizationAlgorithm": "sec:canonicalizationAlgorithm", - "cipherAlgorithm": "sec:cipherAlgorithm", - "cipherData": "sec:cipherData", - "cipherKey": "sec:cipherKey", - "created": {"@id": "dc:created", "@type": "xsd:dateTime"}, - "creator": {"@id": "dc:creator", "@type": "@id"}, - "digestAlgorithm": "sec:digestAlgorithm", - "digestValue": "sec:digestValue", - "domain": "sec:domain", - "encryptionKey": "sec:encryptionKey", - "expiration": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, - "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, - "initializationVector": "sec:initializationVector", - "iterationCount": "sec:iterationCount", - "nonce": "sec:nonce", - "normalizationAlgorithm": "sec:normalizationAlgorithm", - "owner": {"@id": "sec:owner", "@type": "@id"}, - "password": "sec:password", - "privateKey": {"@id": "sec:privateKey", "@type": "@id"}, - "privateKeyPem": "sec:privateKeyPem", - "publicKey": {"@id": "sec:publicKey", "@type": "@id"}, - "publicKeyBase58": "sec:publicKeyBase58", - "publicKeyPem": "sec:publicKeyPem", - "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"}, - "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"}, - "salt": "sec:salt", - "signature": "sec:signature", - "signatureAlgorithm": "sec:signingAlgorithm", - "signatureValue": "sec:signatureValue" - } -} diff --git a/spec/lib/activitypub/linked_data_signature_spec.rb b/spec/lib/activitypub/linked_data_signature_spec.rb index 6a6ad1a706..d5b713b347 100644 --- a/spec/lib/activitypub/linked_data_signature_spec.rb +++ b/spec/lib/activitypub/linked_data_signature_spec.rb @@ -18,10 +18,6 @@ RSpec.describe ActivityPub::LinkedDataSignature do let(:json) { raw_json.merge('signature' => signature) } - before do - stub_jsonld_contexts! - end - describe '#verify_actor!' do context 'when signature matches' do let(:raw_signature) do diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 0bbf8fc52d..8d9677f6ce 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -100,14 +100,6 @@ RSpec.configure do |config| Capybara.current_driver = :rack_test end - config.before :each, type: :controller do - stub_jsonld_contexts! - end - - config.before :each, type: :service do - stub_jsonld_contexts! - end - config.before :suite do if RUN_SYSTEM_SPECS Webpacker.compile @@ -198,9 +190,3 @@ def stub_reset_connection_pools allow(ActiveRecord::Base).to receive(:establish_connection) allow(RedisConfiguration).to receive(:establish_pool) end - -def stub_jsonld_contexts! - stub_request(:get, 'https://www.w3.org/ns/activitystreams').to_return(request_fixture('json-ld.activitystreams.txt')) - stub_request(:get, 'https://w3id.org/identity/v1').to_return(request_fixture('json-ld.identity.txt')) - stub_request(:get, 'https://w3id.org/security/v1').to_return(request_fixture('json-ld.security.txt')) -end diff --git a/spec/services/after_block_domain_from_account_service_spec.rb b/spec/services/after_block_domain_from_account_service_spec.rb index 9bfaa35807..05af125997 100644 --- a/spec/services/after_block_domain_from_account_service_spec.rb +++ b/spec/services/after_block_domain_from_account_service_spec.rb @@ -9,7 +9,6 @@ RSpec.describe AfterBlockDomainFromAccountService, type: :service do let!(:alice) { Fabricate(:account, username: 'alice') } before do - stub_jsonld_contexts! allow(ActivityPub::DeliveryWorker).to receive(:perform_async) end From f5bc1f20e2ba4bafcd8e57c01413c60002696b58 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Oct 2023 09:41:23 -0400 Subject: [PATCH 08/35] Add coverage for `ExistingUsernameValidator` (#25592) Co-authored-by: Claire --- app/validators/existing_username_validator.rb | 43 ++++++---- .../existing_username_validator_spec.rb | 83 +++++++++++++++++++ 2 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 spec/validators/existing_username_validator_spec.rb diff --git a/app/validators/existing_username_validator.rb b/app/validators/existing_username_validator.rb index 037d92f39b..09d53ca680 100644 --- a/app/validators/existing_username_validator.rb +++ b/app/validators/existing_username_validator.rb @@ -2,25 +2,40 @@ class ExistingUsernameValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) - return if value.blank? + @value = value + return if @value.blank? - usernames_and_domains = value.split(',').filter_map do |str| - username, domain = str.strip.gsub(/\A@/, '').split('@', 2) + if options[:multiple] + record.errors.add(attribute, not_found_multiple_message) if usernames_with_no_accounts.any? + elsif usernames_with_no_accounts.any? || usernames_and_domains.size > 1 + record.errors.add(attribute, not_found_message) + end + end + + private + + def usernames_and_domains + @value.split(',').filter_map do |string| + username, domain = string.strip.gsub(/\A@/, '').split('@', 2) domain = nil if TagManager.instance.local_domain?(domain) next if username.blank? - [str, username, domain] - end - - usernames_with_no_accounts = usernames_and_domains.filter_map do |(str, username, domain)| - str unless Account.find_remote(username, domain) - end - - if options[:multiple] - record.errors.add(attribute, I18n.t('existing_username_validator.not_found_multiple', usernames: usernames_with_no_accounts.join(', '))) if usernames_with_no_accounts.any? - elsif usernames_with_no_accounts.any? || usernames_and_domains.size > 1 - record.errors.add(attribute, I18n.t('existing_username_validator.not_found')) + [string, username, domain] end end + + def usernames_with_no_accounts + usernames_and_domains.filter_map do |(string, username, domain)| + string unless Account.find_remote(username, domain) + end + end + + def not_found_multiple_message + I18n.t('existing_username_validator.not_found_multiple', usernames: usernames_with_no_accounts.join(', ')) + end + + def not_found_message + I18n.t('existing_username_validator.not_found') + end end diff --git a/spec/validators/existing_username_validator_spec.rb b/spec/validators/existing_username_validator_spec.rb new file mode 100644 index 0000000000..4f1dd55a17 --- /dev/null +++ b/spec/validators/existing_username_validator_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe ExistingUsernameValidator do + let(:record_class) do + Class.new do + include ActiveModel::Validations + attr_accessor :contact, :friends + + def self.name + 'Record' + end + + validates :contact, existing_username: true + validates :friends, existing_username: { multiple: true } + end + end + let(:record) { record_class.new } + + describe '#validate_each' do + context 'with a nil value' do + it 'does not add errors' do + record.contact = nil + + expect(record).to be_valid + expect(record.errors).to be_empty + end + end + + context 'when there are no accounts' do + it 'adds errors to the record' do + record.contact = 'user@example.com' + + expect(record).to_not be_valid + expect(record.errors.first.attribute).to eq(:contact) + expect(record.errors.first.type).to eq I18n.t('existing_username_validator.not_found') + end + end + + context 'when there are accounts' do + before { Fabricate(:account, domain: 'example.com', username: 'user') } + + context 'when the value does not match' do + it 'adds errors to the record' do + record.contact = 'friend@other.host' + + expect(record).to_not be_valid + expect(record.errors.first.attribute).to eq(:contact) + expect(record.errors.first.type).to eq I18n.t('existing_username_validator.not_found') + end + + context 'when multiple is true' do + it 'adds errors to the record' do + record.friends = 'friend@other.host' + + expect(record).to_not be_valid + expect(record.errors.first.attribute).to eq(:friends) + expect(record.errors.first.type).to eq I18n.t('existing_username_validator.not_found_multiple', usernames: 'friend@other.host') + end + end + end + + context 'when the value does match' do + it 'does not add errors to the record' do + record.contact = 'user@example.com' + + expect(record).to be_valid + expect(record.errors).to be_empty + end + + context 'when multiple is true' do + it 'does not add errors to the record' do + record.friends = 'user@example.com' + + expect(record).to be_valid + expect(record.errors).to be_empty + end + end + end + end + end +end From d9caa6ed240d02e4d0cd385ac558abfdd311efba Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Mon, 16 Oct 2023 10:49:12 -0300 Subject: [PATCH 09/35] Migrate to request specs in `/api/v1/admin/accounts` (#25563) --- .rubocop_todo.yml | 2 - .../api/v1/admin/accounts_controller_spec.rb | 174 -------- spec/requests/api/v1/admin/accounts_spec.rb | 401 ++++++++++++++++++ 3 files changed, 401 insertions(+), 176 deletions(-) delete mode 100644 spec/controllers/api/v1/admin/accounts_controller_spec.rb create mode 100644 spec/requests/api/v1/admin/accounts_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d58bee4ba8..29d9f484c0 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -178,7 +178,6 @@ RSpec/LetSetup: - 'spec/controllers/admin/reports/actions_controller_spec.rb' - 'spec/controllers/admin/statuses_controller_spec.rb' - 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb' - - 'spec/controllers/api/v1/admin/accounts_controller_spec.rb' - 'spec/controllers/api/v1/filters_controller_spec.rb' - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb' - 'spec/controllers/api/v2/filters/keywords_controller_spec.rb' @@ -415,7 +414,6 @@ Rails/SkipsModelValidations: - 'lib/mastodon/cli/accounts.rb' - 'lib/mastodon/cli/main.rb' - 'lib/mastodon/cli/maintenance.rb' - - 'spec/controllers/api/v1/admin/accounts_controller_spec.rb' - 'spec/lib/activitypub/activity/follow_spec.rb' - 'spec/services/follow_service_spec.rb' - 'spec/services/update_account_service_spec.rb' diff --git a/spec/controllers/api/v1/admin/accounts_controller_spec.rb b/spec/controllers/api/v1/admin/accounts_controller_spec.rb deleted file mode 100644 index 4b56b25479..0000000000 --- a/spec/controllers/api/v1/admin/accounts_controller_spec.rb +++ /dev/null @@ -1,174 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Api::V1::Admin::AccountsController do - render_views - - let(:role) { UserRole.find_by(name: 'Moderator') } - let(:user) { Fabricate(:user, role: role) } - let(:scopes) { 'admin:read admin:write' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:account) { Fabricate(:account) } - - before do - allow(controller).to receive(:doorkeeper_token) { token } - end - - describe 'GET #index' do - let!(:remote_account) { Fabricate(:account, domain: 'example.org') } - let!(:other_remote_account) { Fabricate(:account, domain: 'foo.bar') } - let!(:suspended_account) { Fabricate(:account, suspended: true) } - let!(:suspended_remote) { Fabricate(:account, domain: 'foo.bar', suspended: true) } - let!(:disabled_account) { Fabricate(:user, disabled: true).account } - let!(:pending_account) { Fabricate(:user, approved: false).account } - let!(:admin_account) { user.account } - - let(:params) { {} } - - before do - pending_account.user.update(approved: false) - get :index, params: params - end - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - [ - [{ active: 'true', local: 'true', staff: 'true' }, [:admin_account]], - [{ by_domain: 'example.org', remote: 'true' }, [:remote_account]], - [{ suspended: 'true' }, [:suspended_account]], - [{ disabled: 'true' }, [:disabled_account]], - [{ pending: 'true' }, [:pending_account]], - ].each do |params, expected_results| - context "when called with #{params.inspect}" do - let(:params) { params } - - it "returns the correct accounts (#{expected_results.inspect})", :aggregate_failures do - expect(response).to have_http_status(200) - - json = body_as_json - - expect(json.map { |a| a[:id].to_i }).to eq(expected_results.map { |symbol| send(symbol).id }) - end - end - end - end - - describe 'GET #show' do - before do - get :show, params: { id: account.id } - end - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'returns http success' do - expect(response).to have_http_status(200) - end - end - - describe 'POST #approve' do - before do - account.user.update(approved: false) - post :approve, params: { id: account.id } - end - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'approves user', :aggregate_failures do - expect(response).to have_http_status(200) - expect(account.reload.user_approved?).to be true - - log_item = Admin::ActionLog.last - - expect(log_item).to_not be_nil - expect(log_item.action).to eq :approve - expect(log_item.account_id).to eq user.account_id - expect(log_item.target_id).to eq account.user.id - end - end - - describe 'POST #reject' do - before do - account.user.update(approved: false) - post :reject, params: { id: account.id } - end - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'removes user', :aggregate_failures do - expect(response).to have_http_status(200) - expect(User.where(id: account.user.id).count).to eq 0 - - log_item = Admin::ActionLog.last - - expect(log_item).to_not be_nil - expect(log_item.action).to eq :reject - expect(log_item.account_id).to eq user.account_id - expect(log_item.target_id).to eq account.user.id - end - end - - describe 'POST #enable' do - before do - account.user.update(disabled: true) - post :enable, params: { id: account.id } - end - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'enables user', :aggregate_failures do - expect(response).to have_http_status(200) - expect(account.reload.user_disabled?).to be false - end - end - - describe 'POST #unsuspend' do - before do - account.suspend! - post :unsuspend, params: { id: account.id } - end - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'unsuspends account', :aggregate_failures do - expect(response).to have_http_status(200) - expect(account.reload.suspended?).to be false - end - end - - describe 'POST #unsensitive' do - before do - account.touch(:sensitized_at) - post :unsensitive, params: { id: account.id } - end - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'unsensitizes account', :aggregate_failures do - expect(response).to have_http_status(200) - expect(account.reload.sensitized?).to be false - end - end - - describe 'POST #unsilence' do - before do - account.touch(:silenced_at) - post :unsilence, params: { id: account.id } - end - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'unsilences account', :aggregate_failures do - expect(response).to have_http_status(200) - expect(account.reload.silenced?).to be false - end - end -end diff --git a/spec/requests/api/v1/admin/accounts_spec.rb b/spec/requests/api/v1/admin/accounts_spec.rb new file mode 100644 index 0000000000..8e158f623d --- /dev/null +++ b/spec/requests/api/v1/admin/accounts_spec.rb @@ -0,0 +1,401 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Accounts' do + let(:role) { UserRole.find_by(name: 'Admin') } + let(:user) { Fabricate(:user, role: role) } + let(:scopes) { 'admin:read:accounts admin:write:accounts' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } + + describe 'GET /api/v1/admin/accounts' do + subject do + get '/api/v1/admin/accounts', headers: headers, params: params + end + + shared_examples 'a successful request' do + it 'returns the correct accounts', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(body_as_json.pluck(:id)).to match_array(expected_results.map { |a| a.id.to_s }) + end + end + + let!(:remote_account) { Fabricate(:account, domain: 'example.org') } + let!(:suspended_account) { Fabricate(:account, suspended: true) } + let!(:disabled_account) { Fabricate(:user, disabled: true).account } + let!(:pending_account) { Fabricate(:user, approved: false).account } + let!(:admin_account) { user.account } + let(:params) { {} } + + it_behaves_like 'forbidden for wrong scope', 'read read:accounts admin:write admin:write:accounts' + it_behaves_like 'forbidden for wrong role', '' + + context 'when requesting active local staff accounts' do + let(:expected_results) { [admin_account] } + let(:params) { { active: 'true', local: 'true', staff: 'true' } } + + it_behaves_like 'a successful request' + end + + context 'when requesting remote accounts from a specified domain' do + let(:expected_results) { [remote_account] } + let(:params) { { by_domain: 'example.org', remote: 'true' } } + + before do + Fabricate(:account, domain: 'foo.bar') + end + + it_behaves_like 'a successful request' + end + + context 'when requesting suspended accounts' do + let(:expected_results) { [suspended_account] } + let(:params) { { suspended: 'true' } } + + before do + Fabricate(:account, domain: 'foo.bar', suspended: true) + end + + it_behaves_like 'a successful request' + end + + context 'when requesting disabled accounts' do + let(:expected_results) { [disabled_account] } + let(:params) { { disabled: 'true' } } + + it_behaves_like 'a successful request' + end + + context 'when requesting pending accounts' do + let(:expected_results) { [pending_account] } + let(:params) { { pending: 'true' } } + + before do + pending_account.user.update(approved: false) + end + + it_behaves_like 'a successful request' + end + + context 'when no parameter is given' do + let(:expected_results) { [disabled_account, pending_account, admin_account] } + + it_behaves_like 'a successful request' + end + + context 'with limit param' do + let(:params) { { limit: 2 } } + + it 'returns only the requested number of accounts', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(body_as_json.size).to eq(params[:limit]) + end + end + end + + describe 'GET /api/v1/admin/accounts/:id' do + subject do + get "/api/v1/admin/accounts/#{account.id}", headers: headers + end + + let(:account) { Fabricate(:account) } + + it_behaves_like 'forbidden for wrong scope', 'read read:accounts admin:write admin:write:accounts' + it_behaves_like 'forbidden for wrong role', '' + + it 'returns the requested account successfully', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(body_as_json).to match( + a_hash_including(id: account.id.to_s, username: account.username, email: account.user.email) + ) + end + + context 'when the account is not found' do + it 'returns http not found' do + get '/api/v1/admin/accounts/-1', headers: headers + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST /api/v1/admin/accounts/:id/approve' do + subject do + post "/api/v1/admin/accounts/#{account.id}/approve", headers: headers + end + + let(:account) { Fabricate(:account) } + + context 'when the account is pending' do + before do + account.user.update(approved: false) + end + + it_behaves_like 'forbidden for wrong scope', 'write write:accounts read admin:read' + it_behaves_like 'forbidden for wrong role', '' + + it 'approves the user successfully', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(account.reload.user_approved?).to be(true) + end + + it 'logs action', :aggregate_failures do + subject + + log_item = Admin::ActionLog.last + + expect(log_item).to be_present + expect(log_item.action).to eq :approve + expect(log_item.account_id).to eq user.account_id + expect(log_item.target_id).to eq account.user.id + end + end + + context 'when the account is already approved' do + it 'returns http forbidden' do + subject + + expect(response).to have_http_status(403) + end + end + + context 'when the account is not found' do + it 'returns http not found' do + post '/api/v1/admin/accounts/-1/approve', headers: headers + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST /api/v1/admin/accounts/:id/reject' do + subject do + post "/api/v1/admin/accounts/#{account.id}/reject", headers: headers + end + + let(:account) { Fabricate(:account) } + + context 'when the account is pending' do + before do + account.user.update(approved: false) + end + + it_behaves_like 'forbidden for wrong scope', 'write write:accounts read admin:read' + it_behaves_like 'forbidden for wrong role', '' + + it 'removes the user successfully', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(User.where(id: account.user.id)).to_not exist + end + + it 'logs action', :aggregate_failures do + subject + + log_item = Admin::ActionLog.last + + expect(log_item).to be_present + expect(log_item.action).to eq :reject + expect(log_item.account_id).to eq user.account_id + expect(log_item.target_id).to eq account.user.id + end + end + + context 'when account is already approved' do + it 'returns http forbidden' do + subject + + expect(response).to have_http_status(403) + end + end + + context 'when the account is not found' do + it 'returns http not found' do + post '/api/v1/admin/accounts/-1/reject', headers: headers + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST /api/v1/admin/accounts/:id/enable' do + subject do + post "/api/v1/admin/accounts/#{account.id}/enable", headers: headers + end + + let(:account) { Fabricate(:account) } + + before do + account.user.update(disabled: true) + end + + it_behaves_like 'forbidden for wrong scope', 'write write:accounts read admin:read' + it_behaves_like 'forbidden for wrong role', '' + + it 'enables the user successfully', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(account.reload.user_disabled?).to be false + end + + context 'when the account is not found' do + it 'returns http not found' do + post '/api/v1/admin/accounts/-1/enable', headers: headers + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST /api/v1/admin/accounts/:id/unsuspend' do + subject do + post "/api/v1/admin/accounts/#{account.id}/unsuspend", headers: headers + end + + let(:account) { Fabricate(:account) } + + context 'when the account is suspended' do + before do + account.suspend! + end + + it_behaves_like 'forbidden for wrong scope', 'write write:accounts read admin:read' + it_behaves_like 'forbidden for wrong role', '' + + it 'unsuspends the account successfully', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(account.reload.suspended?).to be false + end + end + + context 'when the account is not suspended' do + it 'returns http forbidden' do + subject + + expect(response).to have_http_status(403) + end + end + + context 'when the account is not found' do + it 'returns http not found' do + post '/api/v1/admin/accounts/-1/unsuspend', headers: headers + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST /api/v1/admin/accounts/:id/unsensitive' do + subject do + post "/api/v1/admin/accounts/#{account.id}/unsensitive", headers: headers + end + + let(:account) { Fabricate(:account) } + + before do + account.update(sensitized_at: 10.days.ago) + end + + it_behaves_like 'forbidden for wrong scope', 'write write:accounts read admin:read' + it_behaves_like 'forbidden for wrong role', '' + + it 'unsensitizes the account successfully', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(account.reload.sensitized?).to be false + end + + context 'when the account is not found' do + it 'returns http not found' do + post '/api/v1/admin/accounts/-1/unsensitive', headers: headers + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST /api/v1/admin/accounts/:id/unsilence' do + subject do + post "/api/v1/admin/accounts/#{account.id}/unsilence", headers: headers + end + + let(:account) { Fabricate(:account) } + + before do + account.update(silenced_at: 3.days.ago) + end + + it_behaves_like 'forbidden for wrong scope', 'write write:accounts read admin:read' + it_behaves_like 'forbidden for wrong role', '' + + it 'unsilences the account successfully', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(account.reload.silenced?).to be false + end + + context 'when the account is not found' do + it 'returns http not found' do + post '/api/v1/admin/accounts/-1/unsilence', headers: headers + + expect(response).to have_http_status(404) + end + end + end + + describe 'DELETE /api/v1/admin/accounts/:id' do + subject do + delete "/api/v1/admin/accounts/#{account.id}", headers: headers + end + + let(:account) { Fabricate(:account) } + + context 'when account is suspended' do + before do + account.suspend! + end + + it_behaves_like 'forbidden for wrong scope', 'write write:accounts read admin:read' + it_behaves_like 'forbidden for wrong role', '' + + it 'deletes the account successfully', :aggregate_failures do + allow(Admin::AccountDeletionWorker).to receive(:perform_async) + subject + + expect(response).to have_http_status(200) + expect(Admin::AccountDeletionWorker).to have_received(:perform_async).with(account.id).once + end + end + + context 'when account is not suspended' do + it 'returns http forbidden' do + subject + + expect(response).to have_http_status(403) + end + end + + context 'when the account is not found' do + it 'returns http not found' do + delete '/api/v1/admin/accounts/-1', headers: headers + + expect(response).to have_http_status(404) + end + end + end +end From 8e6116503d697ed3b9206ae10dd913f8adf64ae8 Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Mon, 16 Oct 2023 11:03:35 -0300 Subject: [PATCH 10/35] Migrate to request specs in `/api/v1/blocks` (#25517) --- .../api/v1/blocks_controller_spec.rb | 68 ---------------- spec/requests/api/v1/blocks_spec.rb | 80 +++++++++++++++++++ 2 files changed, 80 insertions(+), 68 deletions(-) delete mode 100644 spec/controllers/api/v1/blocks_controller_spec.rb create mode 100644 spec/requests/api/v1/blocks_spec.rb diff --git a/spec/controllers/api/v1/blocks_controller_spec.rb b/spec/controllers/api/v1/blocks_controller_spec.rb deleted file mode 100644 index ba63560a96..0000000000 --- a/spec/controllers/api/v1/blocks_controller_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Api::V1::BlocksController do - render_views - - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:blocks' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - - before { allow(controller).to receive(:doorkeeper_token) { token } } - - describe 'GET #index' do - it 'limits according to limit parameter', :aggregate_failures do - Array.new(2) { Fabricate(:block, account: user.account) } - get :index, params: { limit: 1 } - - expect(response).to have_http_status(200) - expect(body_as_json.size).to eq 1 - end - - it 'queries blocks in range according to max_id', :aggregate_failures do - blocks = Array.new(2) { Fabricate(:block, account: user.account) } - - get :index, params: { max_id: blocks[1] } - - expect(response).to have_http_status(200) - expect(body_as_json.size).to eq 1 - expect(body_as_json[0][:id]).to eq blocks[0].target_account_id.to_s - end - - it 'queries blocks in range according to since_id', :aggregate_failures do - blocks = Array.new(2) { Fabricate(:block, account: user.account) } - - get :index, params: { since_id: blocks[0] } - - expect(response).to have_http_status(200) - expect(body_as_json.size).to eq 1 - expect(body_as_json[0][:id]).to eq blocks[1].target_account_id.to_s - end - - it 'sets pagination header for next path', :aggregate_failures do - blocks = Array.new(2) { Fabricate(:block, account: user.account) } - get :index, params: { limit: 1, since_id: blocks[0] } - - expect(response).to have_http_status(200) - expect(response.headers['Link'].find_link(%w(rel next)).href).to eq api_v1_blocks_url(limit: 1, max_id: blocks[1]) - end - - it 'sets pagination header for previous path', :aggregate_failures do - block = Fabricate(:block, account: user.account) - get :index - - expect(response).to have_http_status(200) - expect(response.headers['Link'].find_link(%w(rel prev)).href).to eq api_v1_blocks_url(since_id: block) - end - - context 'with wrong scopes' do - let(:scopes) { 'write:blocks' } - - it 'returns http forbidden' do - get :index - expect(response).to have_http_status(403) - end - end - end -end diff --git a/spec/requests/api/v1/blocks_spec.rb b/spec/requests/api/v1/blocks_spec.rb new file mode 100644 index 0000000000..62543157c3 --- /dev/null +++ b/spec/requests/api/v1/blocks_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Blocks' do + let(:user) { Fabricate(:user) } + let(:scopes) { 'read:blocks' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } + + describe 'GET /api/v1/blocks' do + subject do + get '/api/v1/blocks', headers: headers, params: params + end + + let!(:blocks) { Fabricate.times(3, :block, account: user.account) } + let(:params) { {} } + + let(:expected_response) do + blocks.map { |block| a_hash_including(id: block.target_account.id.to_s, username: block.target_account.username) } + end + + it_behaves_like 'forbidden for wrong scope', 'write write:blocks' + + it 'returns the blocked accounts', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(body_as_json).to match_array(expected_response) + end + + context 'with limit param' do + let(:params) { { limit: 2 } } + + it 'returns only the requested number of blocked accounts' do + subject + + expect(body_as_json.size).to eq(params[:limit]) + end + + it 'sets the correct pagination header for the prev path' do + subject + + expect(response.headers['Link'].find_link(%w(rel prev)).href).to eq(api_v1_blocks_url(limit: params[:limit], since_id: blocks.last.id)) + end + + it 'sets the correct pagination header for the next path' do + subject + + expect(response.headers['Link'].find_link(%w(rel next)).href).to eq(api_v1_blocks_url(limit: params[:limit], max_id: blocks[1].id)) + end + end + + context 'with max_id param' do + let(:params) { { max_id: blocks[1].id } } + + it 'queries the blocks in range according to max_id', :aggregate_failures do + subject + + response_body = body_as_json + + expect(response_body.size).to be 1 + expect(response_body[0][:id]).to eq(blocks[0].target_account.id.to_s) + end + end + + context 'with since_id param' do + let(:params) { { since_id: blocks[1].id } } + + it 'queries the blocks in range according to since_id', :aggregate_failures do + subject + + response_body = body_as_json + + expect(response_body.size).to be 1 + expect(response_body[0][:id]).to eq(blocks[2].target_account.id.to_s) + end + end + end +end From cafdaec6cfd4d0b4033f0495017651e0a81187ca Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Mon, 16 Oct 2023 11:03:48 -0300 Subject: [PATCH 11/35] Migrate to request specs in `/api/v1/lists/:id/accounts` (#25468) --- .../api/v1/lists/accounts_controller_spec.rb | 80 -------- spec/requests/api/v1/lists/accounts_spec.rb | 178 ++++++++++++++++++ 2 files changed, 178 insertions(+), 80 deletions(-) delete mode 100644 spec/controllers/api/v1/lists/accounts_controller_spec.rb create mode 100644 spec/requests/api/v1/lists/accounts_spec.rb diff --git a/spec/controllers/api/v1/lists/accounts_controller_spec.rb b/spec/controllers/api/v1/lists/accounts_controller_spec.rb deleted file mode 100644 index 21e155a508..0000000000 --- a/spec/controllers/api/v1/lists/accounts_controller_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Api::V1::Lists::AccountsController do - render_views - - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:list) { Fabricate(:list, account: user.account) } - - before do - follow = Fabricate(:follow, account: user.account) - list.accounts << follow.target_account - allow(controller).to receive(:doorkeeper_token) { token } - end - - describe 'GET #index' do - let(:scopes) { 'read:lists' } - - it 'returns http success' do - get :show, params: { list_id: list.id } - - expect(response).to have_http_status(200) - end - end - - describe 'POST #create' do - let(:scopes) { 'write:lists' } - let(:bob) { Fabricate(:account, username: 'bob') } - - context 'when the added account is followed' do - before do - user.account.follow!(bob) - post :create, params: { list_id: list.id, account_ids: [bob.id] } - end - - it 'adds account to the list', :aggregate_failures do - expect(response).to have_http_status(200) - expect(list.accounts.include?(bob)).to be true - end - end - - context 'when the added account has been sent a follow request' do - before do - user.account.follow_requests.create!(target_account: bob) - post :create, params: { list_id: list.id, account_ids: [bob.id] } - end - - it 'adds account to the list', :aggregate_failures do - expect(response).to have_http_status(200) - expect(list.accounts.include?(bob)).to be true - end - end - - context 'when the added account is not followed' do - before do - post :create, params: { list_id: list.id, account_ids: [bob.id] } - end - - it 'does not add the account to the list', :aggregate_failures do - expect(response).to have_http_status(404) - expect(list.accounts.include?(bob)).to be false - end - end - end - - describe 'DELETE #destroy' do - let(:scopes) { 'write:lists' } - - before do - delete :destroy, params: { list_id: list.id, account_ids: [list.accounts.first.id] } - end - - it 'removes account from the list', :aggregate_failures do - expect(response).to have_http_status(200) - expect(list.accounts.count).to eq 0 - end - end -end diff --git a/spec/requests/api/v1/lists/accounts_spec.rb b/spec/requests/api/v1/lists/accounts_spec.rb new file mode 100644 index 0000000000..4d2a168b34 --- /dev/null +++ b/spec/requests/api/v1/lists/accounts_spec.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Accounts' do + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:scopes) { 'read:lists write:lists' } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } + + describe 'GET /api/v1/lists/:id/accounts' do + subject do + get "/api/v1/lists/#{list.id}/accounts", headers: headers, params: params + end + + let(:params) { { limit: 0 } } + let(:list) { Fabricate(:list, account: user.account) } + let(:accounts) { Fabricate.times(3, :account) } + + let(:expected_response) do + accounts.map do |account| + a_hash_including(id: account.id.to_s, username: account.username, acct: account.acct) + end + end + + before do + accounts.each { |account| user.account.follow!(account) } + list.accounts << accounts + end + + it_behaves_like 'forbidden for wrong scope', 'write write:lists' + + it 'returns the accounts in the requested list', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(body_as_json).to match_array(expected_response) + end + + context 'with limit param' do + let(:params) { { limit: 1 } } + + it 'returns only the requested number of accounts' do + subject + + expect(body_as_json.size).to eq(params[:limit]) + end + end + end + + describe 'POST /api/v1/lists/:id/accounts' do + subject do + post "/api/v1/lists/#{list.id}/accounts", headers: headers, params: params + end + + let(:list) { Fabricate(:list, account: user.account) } + let(:bob) { Fabricate(:account, username: 'bob') } + let(:params) { { account_ids: [bob.id] } } + + it_behaves_like 'forbidden for wrong scope', 'read read:lists' + + context 'when the added account is followed' do + before do + user.account.follow!(bob) + end + + it 'adds account to the list', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(list.accounts).to include(bob) + end + end + + context 'when the added account has been sent a follow request' do + before do + user.account.follow_requests.create!(target_account: bob) + end + + it 'adds account to the list', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(list.accounts).to include(bob) + end + end + + context 'when the added account is not followed' do + it 'does not add the account to the list', :aggregate_failures do + subject + + expect(response).to have_http_status(404) + expect(list.accounts).to_not include(bob) + end + end + + context 'when the list is not owned by the requesting user' do + let(:list) { Fabricate(:list) } + + before do + user.account.follow!(bob) + end + + it 'returns http not found' do + subject + + expect(response).to have_http_status(404) + end + end + + context 'when account is already in the list' do + before do + user.account.follow!(bob) + list.accounts << bob + end + + it 'returns http unprocessable entity' do + subject + + expect(response).to have_http_status(422) + end + end + end + + describe 'DELETE /api/v1/lists/:id/accounts' do + subject do + delete "/api/v1/lists/#{list.id}/accounts", headers: headers, params: params + end + + context 'when the list is owned by the requesting user' do + let(:list) { Fabricate(:list, account: user.account) } + let(:bob) { Fabricate(:account, username: 'bob') } + let(:peter) { Fabricate(:account, username: 'peter') } + let(:params) { { account_ids: [bob.id] } } + + before do + user.account.follow!(bob) + user.account.follow!(peter) + list.accounts << [bob, peter] + end + + it 'removes the specified account from the list', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(list.accounts).to_not include(bob) + end + + it 'does not remove any other account from the list' do + subject + + expect(list.accounts).to include(peter) + end + + context 'when the specified account is not in the list' do + let(:params) { { account_ids: [0] } } + + it 'does not remove any account from the list', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(list.accounts).to contain_exactly(bob, peter) + end + end + end + + context 'when the list is not owned by the requesting user' do + let(:list) { Fabricate(:list) } + let(:params) { {} } + + it 'returns http not found' do + subject + + expect(response).to have_http_status(404) + end + end + end +end From c91c0175db1cc8b954a977d29472886234ce9586 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Oct 2023 10:04:29 -0400 Subject: [PATCH 12/35] Extract `card` partial from `disputes/strikes/show` view (#27426) --- app/views/disputes/strikes/_card.html.haml | 38 ++++++++++++++++++ app/views/disputes/strikes/show.html.haml | 46 +--------------------- 2 files changed, 39 insertions(+), 45 deletions(-) create mode 100644 app/views/disputes/strikes/_card.html.haml diff --git a/app/views/disputes/strikes/_card.html.haml b/app/views/disputes/strikes/_card.html.haml new file mode 100644 index 0000000000..55551cc7d0 --- /dev/null +++ b/app/views/disputes/strikes/_card.html.haml @@ -0,0 +1,38 @@ +.strike-card + - unless strike.none_action? + %p= t "user_mailer.warning.explanation.#{strike.action}", instance: Rails.configuration.x.local_domain + - if strike.text.present? + = linkify(strike.text) + - if strike.report && !strike.report.other? + %p + %strong= t('user_mailer.warning.reason') + = t("user_mailer.warning.categories.#{strike.report.category}") + - if strike.report.violation? && strike.report.rule_ids.present? + %ul.strike-card__rules + - strike.report.rules.each do |rule| + %li + %span.strike-card__rules__text= rule.text + - if strike.status_ids.present? && !strike.status_ids.empty? + %p + %strong= t('user_mailer.warning.statuses') + .strike-card__statuses-list + - status_map = strike.statuses.includes(:application, :media_attachments).index_by(&:id) + - strike.status_ids.each do |status_id| + .strike-card__statuses-list__item + - if (status = status_map[status_id.to_i]) + .one-liner + .emojify= one_line_preview(status) + - status.ordered_media_attachments.each do |media_attachment| + %abbr{ title: media_attachment.description } + = fa_icon 'link' + = media_attachment.file_file_name + .strike-card__statuses-list__item__meta + = link_to ActivityPub::TagManager.instance.url_for(status), target: '_blank', rel: 'noopener noreferrer' do + %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) + - unless status.application.nil? + · + = status.application.name + - else + .one-liner= t('disputes.strikes.status', id: status_id) + .strike-card__statuses-list__item__meta + = t('disputes.strikes.status_removed') diff --git a/app/views/disputes/strikes/show.html.haml b/app/views/disputes/strikes/show.html.haml index de883bd873..1c16e0bbf5 100644 --- a/app/views/disputes/strikes/show.html.haml +++ b/app/views/disputes/strikes/show.html.haml @@ -21,51 +21,7 @@ .report-header .report-header__card - .strike-card - - unless @strike.none_action? - %p= t "user_mailer.warning.explanation.#{@strike.action}", instance: Rails.configuration.x.local_domain - - - if @strike.text.present? - = linkify(@strike.text) - - - if @strike.report && !@strike.report.other? - %p - %strong= t('user_mailer.warning.reason') - = t("user_mailer.warning.categories.#{@strike.report.category}") - - - if @strike.report.violation? && @strike.report.rule_ids.present? - %ul.strike-card__rules - - @strike.report.rules.each do |rule| - %li - %span.strike-card__rules__text= rule.text - - - if @strike.status_ids.present? && !@strike.status_ids.empty? - %p - %strong= t('user_mailer.warning.statuses') - - .strike-card__statuses-list - - status_map = @strike.statuses.includes(:application, :media_attachments).index_by(&:id) - - - @strike.status_ids.each do |status_id| - .strike-card__statuses-list__item - - if (status = status_map[status_id.to_i]) - .one-liner - .emojify= one_line_preview(status) - - - status.ordered_media_attachments.each do |media_attachment| - %abbr{ title: media_attachment.description } - = fa_icon 'link' - = media_attachment.file_file_name - .strike-card__statuses-list__item__meta - = link_to ActivityPub::TagManager.instance.url_for(status), target: '_blank', rel: 'noopener noreferrer' do - %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) - - unless status.application.nil? - · - = status.application.name - - else - .one-liner= t('disputes.strikes.status', id: status_id) - .strike-card__statuses-list__item__meta - = t('disputes.strikes.status_removed') + = render 'card', strike: @strike .report-header__details .report-header__details__item From 08a376cbcbb2fa46c32c43937b77ddb081418af8 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Oct 2023 10:36:28 -0400 Subject: [PATCH 13/35] Fix `Style/CombinableLoops` cop (#27429) --- .rubocop_todo.yml | 6 ------ app/models/form/custom_emoji_batch.rb | 18 +++++++++++------- app/models/form/ip_block_batch.rb | 6 +++++- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 29d9f484c0..980339f496 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -517,12 +517,6 @@ Style/ClassVars: Exclude: - 'config/initializers/devise.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/CombinableLoops: - Exclude: - - 'app/models/form/custom_emoji_batch.rb' - - 'app/models/form/ip_block_batch.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedVars. Style/FetchEnvVar: diff --git a/app/models/form/custom_emoji_batch.rb b/app/models/form/custom_emoji_batch.rb index 484415f902..c63996e069 100644 --- a/app/models/form/custom_emoji_batch.rb +++ b/app/models/form/custom_emoji_batch.rb @@ -34,7 +34,7 @@ class Form::CustomEmojiBatch end def update! - custom_emojis.each { |custom_emoji| authorize(custom_emoji, :update?) } + verify_authorization(:update?) category = if category_id.present? CustomEmojiCategory.find(category_id) @@ -49,7 +49,7 @@ class Form::CustomEmojiBatch end def list! - custom_emojis.each { |custom_emoji| authorize(custom_emoji, :update?) } + verify_authorization(:update?) custom_emojis.each do |custom_emoji| custom_emoji.update(visible_in_picker: true) @@ -58,7 +58,7 @@ class Form::CustomEmojiBatch end def unlist! - custom_emojis.each { |custom_emoji| authorize(custom_emoji, :update?) } + verify_authorization(:update?) custom_emojis.each do |custom_emoji| custom_emoji.update(visible_in_picker: false) @@ -67,7 +67,7 @@ class Form::CustomEmojiBatch end def enable! - custom_emojis.each { |custom_emoji| authorize(custom_emoji, :enable?) } + verify_authorization(:enable?) custom_emojis.each do |custom_emoji| custom_emoji.update(disabled: false) @@ -76,7 +76,7 @@ class Form::CustomEmojiBatch end def disable! - custom_emojis.each { |custom_emoji| authorize(custom_emoji, :disable?) } + verify_authorization(:disable?) custom_emojis.each do |custom_emoji| custom_emoji.update(disabled: true) @@ -85,7 +85,7 @@ class Form::CustomEmojiBatch end def copy! - custom_emojis.each { |custom_emoji| authorize(custom_emoji, :copy?) } + verify_authorization(:copy?) custom_emojis.each do |custom_emoji| copied_custom_emoji = custom_emoji.copy! @@ -94,11 +94,15 @@ class Form::CustomEmojiBatch end def delete! - custom_emojis.each { |custom_emoji| authorize(custom_emoji, :destroy?) } + verify_authorization(:destroy?) custom_emojis.each do |custom_emoji| custom_emoji.destroy log_action :destroy, custom_emoji end end + + def verify_authorization(permission) + custom_emojis.each { |custom_emoji| authorize(custom_emoji, permission) } + end end diff --git a/app/models/form/ip_block_batch.rb b/app/models/form/ip_block_batch.rb index f6fe9b5935..bdfeb91c8a 100644 --- a/app/models/form/ip_block_batch.rb +++ b/app/models/form/ip_block_batch.rb @@ -21,11 +21,15 @@ class Form::IpBlockBatch end def delete! - ip_blocks.each { |ip_block| authorize(ip_block, :destroy?) } + verify_authorization(:destroy?) ip_blocks.each do |ip_block| ip_block.destroy log_action :destroy, ip_block end end + + def verify_authorization(permission) + ip_blocks.each { |ip_block| authorize(ip_block, permission) } + end end From e0ed0f8c7c6d74534ba896ebe0d3a25495f27f0d Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Mon, 16 Oct 2023 12:15:24 -0300 Subject: [PATCH 14/35] Migrate to request specs in `/api/v1/notifications` (#25553) --- .../api/v1/notifications_controller_spec.rb | 137 ------------- spec/requests/api/v1/notifications_spec.rb | 183 ++++++++++++++++++ 2 files changed, 183 insertions(+), 137 deletions(-) delete mode 100644 spec/controllers/api/v1/notifications_controller_spec.rb create mode 100644 spec/requests/api/v1/notifications_spec.rb diff --git a/spec/controllers/api/v1/notifications_controller_spec.rb b/spec/controllers/api/v1/notifications_controller_spec.rb deleted file mode 100644 index 6615848b83..0000000000 --- a/spec/controllers/api/v1/notifications_controller_spec.rb +++ /dev/null @@ -1,137 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Api::V1::NotificationsController do - render_views - - let(:user) { Fabricate(:user, account_attributes: { username: 'alice' }) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:other) { Fabricate(:user) } - let(:third) { Fabricate(:user) } - - before do - allow(controller).to receive(:doorkeeper_token) { token } - end - - describe 'GET #show' do - let(:scopes) { 'read:notifications' } - - it 'returns http success' do - notification = Fabricate(:notification, account: user.account) - get :show, params: { id: notification.id } - - expect(response).to have_http_status(200) - end - end - - describe 'POST #dismiss' do - let(:scopes) { 'write:notifications' } - - it 'destroys the notification' do - notification = Fabricate(:notification, account: user.account) - post :dismiss, params: { id: notification.id } - - expect(response).to have_http_status(200) - expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - describe 'POST #clear' do - let(:scopes) { 'write:notifications' } - - it 'clears notifications for the account' do - notification = Fabricate(:notification, account: user.account) - post :clear - - expect(notification.account.reload.notifications).to be_empty - expect(response).to have_http_status(200) - end - end - - describe 'GET #index' do - let(:scopes) { 'read:notifications' } - - before do - first_status = PostStatusService.new.call(user.account, text: 'Test') - @reblog_of_first_status = ReblogService.new.call(other.account, first_status) - mentioning_status = PostStatusService.new.call(other.account, text: 'Hello @alice') - @mention_from_status = mentioning_status.mentions.first - @favourite = FavouriteService.new.call(other.account, first_status) - @second_favourite = FavouriteService.new.call(third.account, first_status) - @follow = FollowService.new.call(other.account, user.account) - end - - describe 'with no options' do - before do - get :index - end - - it 'returns expected notification types', :aggregate_failures do - expect(response).to have_http_status(200) - - expect(body_json_types).to include 'reblog' - expect(body_json_types).to include 'mention' - expect(body_json_types).to include 'favourite' - expect(body_json_types).to include 'follow' - end - end - - describe 'with account_id param' do - before do - get :index, params: { account_id: third.account.id } - end - - it 'returns only notifications from specified user', :aggregate_failures do - expect(response).to have_http_status(200) - - expect(body_json_account_ids.uniq).to eq [third.account.id.to_s] - end - - def body_json_account_ids - body_as_json.map { |x| x[:account][:id] } - end - end - - describe 'with invalid account_id param' do - before do - get :index, params: { account_id: 'foo' } - end - - it 'returns nothing', :aggregate_failures do - expect(response).to have_http_status(200) - - expect(body_as_json.size).to eq 0 - end - end - - describe 'with exclude_types param' do - before do - get :index, params: { exclude_types: %w(mention) } - end - - it 'returns everything but excluded type', :aggregate_failures do - expect(response).to have_http_status(200) - - expect(body_as_json.size).to_not eq 0 - expect(body_json_types.uniq).to_not include 'mention' - end - end - - describe 'with types param' do - before do - get :index, params: { types: %w(mention) } - end - - it 'returns only requested type', :aggregate_failures do - expect(response).to have_http_status(200) - - expect(body_json_types.uniq).to eq ['mention'] - end - end - - def body_json_types - body_as_json.pluck(:type) - end - end -end diff --git a/spec/requests/api/v1/notifications_spec.rb b/spec/requests/api/v1/notifications_spec.rb new file mode 100644 index 0000000000..7a879c35b7 --- /dev/null +++ b/spec/requests/api/v1/notifications_spec.rb @@ -0,0 +1,183 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Notifications' do + let(:user) { Fabricate(:user, account_attributes: { username: 'alice' }) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:scopes) { 'read:notifications write:notifications' } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } + + describe 'GET /api/v1/notifications' do + subject do + get '/api/v1/notifications', headers: headers, params: params + end + + let(:bob) { Fabricate(:user) } + let(:tom) { Fabricate(:user) } + let(:params) { {} } + + before do + first_status = PostStatusService.new.call(user.account, text: 'Test') + ReblogService.new.call(bob.account, first_status) + mentioning_status = PostStatusService.new.call(bob.account, text: 'Hello @alice') + mentioning_status.mentions.first + FavouriteService.new.call(bob.account, first_status) + FavouriteService.new.call(tom.account, first_status) + FollowService.new.call(bob.account, user.account) + end + + it_behaves_like 'forbidden for wrong scope', 'write write:notifications' + + context 'with no options' do + it 'returns expected notification types', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(body_json_types).to include 'reblog' + expect(body_json_types).to include 'mention' + expect(body_json_types).to include 'favourite' + expect(body_json_types).to include 'follow' + end + end + + context 'with account_id param' do + let(:params) { { account_id: tom.account.id } } + + it 'returns only notifications from specified user', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(body_json_account_ids.uniq).to eq [tom.account.id.to_s] + end + + def body_json_account_ids + body_as_json.map { |x| x[:account][:id] } + end + end + + context 'with invalid account_id param' do + let(:params) { { account_id: 'foo' } } + + it 'returns nothing', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(body_as_json.size).to eq 0 + end + end + + context 'with exclude_types param' do + let(:params) { { exclude_types: %w(mention) } } + + it 'returns everything but excluded type', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(body_as_json.size).to_not eq 0 + expect(body_json_types.uniq).to_not include 'mention' + end + end + + context 'with types param' do + let(:params) { { types: %w(mention) } } + + it 'returns only requested type', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(body_json_types.uniq).to eq ['mention'] + end + end + + context 'with limit param' do + let(:params) { { limit: 3 } } + + it 'returns the requested number of notifications paginated', :aggregate_failures do + subject + + notifications = user.account.notifications + + expect(body_as_json.size).to eq(params[:limit]) + expect(response.headers['Link'].find_link(%w(rel prev)).href).to eq(api_v1_notifications_url(limit: params[:limit], min_id: notifications.last.id.to_s)) + expect(response.headers['Link'].find_link(%w(rel next)).href).to eq(api_v1_notifications_url(limit: params[:limit], max_id: notifications[2].id.to_s)) + end + end + + def body_json_types + body_as_json.pluck(:type) + end + end + + describe 'GET /api/v1/notifications/:id' do + subject do + get "/api/v1/notifications/#{notification.id}", headers: headers + end + + let(:notification) { Fabricate(:notification, account: user.account) } + + it_behaves_like 'forbidden for wrong scope', 'write write:notifications' + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + + context 'when notification belongs to someone else' do + let(:notification) { Fabricate(:notification) } + + it 'returns http not found' do + subject + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST /api/v1/notifications/:id/dismiss' do + subject do + post "/api/v1/notifications/#{notification.id}/dismiss", headers: headers + end + + let!(:notification) { Fabricate(:notification, account: user.account) } + + it_behaves_like 'forbidden for wrong scope', 'read read:notifications' + + it 'destroys the notification' do + subject + + expect(response).to have_http_status(200) + expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + context 'when notification belongs to someone else' do + let(:notification) { Fabricate(:notification) } + + it 'returns http not found' do + subject + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST /api/v1/notifications/clear' do + subject do + post '/api/v1/notifications/clear', headers: headers + end + + before do + Fabricate.times(3, :notification, account: user.account) + end + + it_behaves_like 'forbidden for wrong scope', 'read read:notifications' + + it 'clears notifications for the account' do + subject + + expect(user.account.reload.notifications).to be_empty + expect(response).to have_http_status(200) + end + end +end From 708299bb0d17ec60f51226f43c26bd66e6cb8adc Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Oct 2023 11:20:28 -0400 Subject: [PATCH 15/35] Misc coverage improvements for `Admin::` and `Settings::` controllers (#25346) --- .../admin/disputes/appeals_controller_spec.rb | 10 ++++ .../admin/domain_blocks_controller_spec.rb | 11 ++++ .../export_domain_allows_controller_spec.rb | 8 +++ .../export_domain_blocks_controller_spec.rb | 8 +++ .../admin/instances_controller_spec.rb | 57 +++++++++++++++++++ .../admin/settings/about_controller_spec.rb | 8 +++ .../settings/appearance_controller_spec.rb | 8 +++ .../content_retention_controller_spec.rb | 8 +++ .../settings/discovery_controller_spec.rb | 8 +++ .../settings/registrations_controller_spec.rb | 8 +++ .../controllers/admin/tags_controller_spec.rb | 22 +++++++ .../admin/webhooks_controller_spec.rb | 18 ++++++ .../settings/imports_controller_spec.rb | 13 +++++ 13 files changed, 187 insertions(+) diff --git a/spec/controllers/admin/disputes/appeals_controller_spec.rb b/spec/controllers/admin/disputes/appeals_controller_spec.rb index 3c3f23f529..4afe074921 100644 --- a/spec/controllers/admin/disputes/appeals_controller_spec.rb +++ b/spec/controllers/admin/disputes/appeals_controller_spec.rb @@ -15,6 +15,16 @@ RSpec.describe Admin::Disputes::AppealsController do let(:strike) { Fabricate(:account_warning, target_account: target_account, action: :suspend) } let(:appeal) { Fabricate(:appeal, strike: strike, account: target_account) } + describe 'GET #index' do + let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + it 'lists appeals' do + get :index + + expect(response).to have_http_status(200) + end + end + describe 'POST #approve' do let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } diff --git a/spec/controllers/admin/domain_blocks_controller_spec.rb b/spec/controllers/admin/domain_blocks_controller_spec.rb index 9be55906ed..13826be366 100644 --- a/spec/controllers/admin/domain_blocks_controller_spec.rb +++ b/spec/controllers/admin/domain_blocks_controller_spec.rb @@ -165,6 +165,17 @@ RSpec.describe Admin::DomainBlocksController do end end + describe 'GET #edit' do + let(:domain_block) { Fabricate(:domain_block) } + + it 'returns http success' do + get :edit, params: { id: domain_block.id } + + expect(assigns(:domain_block)).to be_instance_of(DomainBlock) + expect(response).to have_http_status(200) + end + end + describe 'PUT #update' do subject do post :update, params: { :id => domain_block.id, :domain_block => { domain: 'example.com', severity: new_severity }, 'confirm' => '' } diff --git a/spec/controllers/admin/export_domain_allows_controller_spec.rb b/spec/controllers/admin/export_domain_allows_controller_spec.rb index 9d50c04aad..e1e5ecc1f0 100644 --- a/spec/controllers/admin/export_domain_allows_controller_spec.rb +++ b/spec/controllers/admin/export_domain_allows_controller_spec.rb @@ -9,6 +9,14 @@ RSpec.describe Admin::ExportDomainAllowsController do sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user end + describe 'GET #new' do + it 'returns http success' do + get :new + + expect(response).to have_http_status(200) + end + end + describe 'GET #export' do it 'renders instances' do Fabricate(:domain_allow, domain: 'good.domain') diff --git a/spec/controllers/admin/export_domain_blocks_controller_spec.rb b/spec/controllers/admin/export_domain_blocks_controller_spec.rb index 1a63077736..5a282c9572 100644 --- a/spec/controllers/admin/export_domain_blocks_controller_spec.rb +++ b/spec/controllers/admin/export_domain_blocks_controller_spec.rb @@ -9,6 +9,14 @@ RSpec.describe Admin::ExportDomainBlocksController do sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user end + describe 'GET #new' do + it 'returns http success' do + get :new + + expect(response).to have_http_status(200) + end + end + describe 'GET #export' do it 'renders instances' do Fabricate(:domain_block, domain: 'bad.domain', severity: 'silence', public_comment: 'bad server') diff --git a/spec/controllers/admin/instances_controller_spec.rb b/spec/controllers/admin/instances_controller_spec.rb index dd772d1036..5fed5d98d2 100644 --- a/spec/controllers/admin/instances_controller_spec.rb +++ b/spec/controllers/admin/instances_controller_spec.rb @@ -34,6 +34,63 @@ RSpec.describe Admin::InstancesController do end end + describe 'GET #show' do + it 'shows an instance page' do + get :show, params: { id: account_popular_main.domain } + + expect(response).to have_http_status(200) + end + end + + describe 'POST #clear_delivery_errors' do + let(:tracker) { instance_double(DeliveryFailureTracker, clear_failures!: true) } + + before { allow(DeliveryFailureTracker).to receive(:new).and_return(tracker) } + + it 'clears instance delivery errors' do + post :clear_delivery_errors, params: { id: account_popular_main.domain } + + expect(response).to redirect_to(admin_instance_path(account_popular_main.domain)) + expect(tracker).to have_received(:clear_failures!) + end + end + + describe 'POST #restart_delivery' do + let(:tracker) { instance_double(DeliveryFailureTracker, track_success!: true) } + + before { allow(DeliveryFailureTracker).to receive(:new).and_return(tracker) } + + context 'with an unavailable instance' do + before { Fabricate(:unavailable_domain, domain: account_popular_main.domain) } + + it 'tracks success on the instance' do + post :restart_delivery, params: { id: account_popular_main.domain } + + expect(response).to redirect_to(admin_instance_path(account_popular_main.domain)) + expect(tracker).to have_received(:track_success!) + end + end + + context 'with an available instance' do + it 'does not track success on the instance' do + post :restart_delivery, params: { id: account_popular_main.domain } + + expect(response).to redirect_to(admin_instance_path(account_popular_main.domain)) + expect(tracker).to_not have_received(:track_success!) + end + end + end + + describe 'POST #stop_delivery' do + it 'clears instance delivery errors' do + expect do + post :stop_delivery, params: { id: account_popular_main.domain } + end.to change(UnavailableDomain, :count).by(1) + + expect(response).to redirect_to(admin_instance_path(account_popular_main.domain)) + end + end + describe 'DELETE #destroy' do subject { delete :destroy, params: { id: Instance.first.id } } diff --git a/spec/controllers/admin/settings/about_controller_spec.rb b/spec/controllers/admin/settings/about_controller_spec.rb index 2ae26090b6..f322cb4434 100644 --- a/spec/controllers/admin/settings/about_controller_spec.rb +++ b/spec/controllers/admin/settings/about_controller_spec.rb @@ -18,4 +18,12 @@ describe Admin::Settings::AboutController do expect(response).to have_http_status(:success) end end + + describe 'PUT #update' do + it 'updates the settings' do + put :update, params: { form_admin_settings: { site_extended_description: 'new site description' } } + + expect(response).to redirect_to(admin_settings_about_path) + end + end end diff --git a/spec/controllers/admin/settings/appearance_controller_spec.rb b/spec/controllers/admin/settings/appearance_controller_spec.rb index 65b29acc3e..ea6f3b7833 100644 --- a/spec/controllers/admin/settings/appearance_controller_spec.rb +++ b/spec/controllers/admin/settings/appearance_controller_spec.rb @@ -18,4 +18,12 @@ describe Admin::Settings::AppearanceController do expect(response).to have_http_status(:success) end end + + describe 'PUT #update' do + it 'updates the settings' do + put :update, params: { form_admin_settings: { custom_css: 'html { display: inline; }' } } + + expect(response).to redirect_to(admin_settings_appearance_path) + end + end end diff --git a/spec/controllers/admin/settings/content_retention_controller_spec.rb b/spec/controllers/admin/settings/content_retention_controller_spec.rb index 53ce84d189..fb6a3d2848 100644 --- a/spec/controllers/admin/settings/content_retention_controller_spec.rb +++ b/spec/controllers/admin/settings/content_retention_controller_spec.rb @@ -18,4 +18,12 @@ describe Admin::Settings::ContentRetentionController do expect(response).to have_http_status(:success) end end + + describe 'PUT #update' do + it 'updates the settings' do + put :update, params: { form_admin_settings: { media_cache_retention_period: '2' } } + + expect(response).to redirect_to(admin_settings_content_retention_path) + end + end end diff --git a/spec/controllers/admin/settings/discovery_controller_spec.rb b/spec/controllers/admin/settings/discovery_controller_spec.rb index c7307ffc88..33109e3c01 100644 --- a/spec/controllers/admin/settings/discovery_controller_spec.rb +++ b/spec/controllers/admin/settings/discovery_controller_spec.rb @@ -18,4 +18,12 @@ describe Admin::Settings::DiscoveryController do expect(response).to have_http_status(:success) end end + + describe 'PUT #update' do + it 'updates the settings' do + put :update, params: { form_admin_settings: { trends: '1' } } + + expect(response).to redirect_to(admin_settings_discovery_path) + end + end end diff --git a/spec/controllers/admin/settings/registrations_controller_spec.rb b/spec/controllers/admin/settings/registrations_controller_spec.rb index 3fc1f9d132..e076544603 100644 --- a/spec/controllers/admin/settings/registrations_controller_spec.rb +++ b/spec/controllers/admin/settings/registrations_controller_spec.rb @@ -18,4 +18,12 @@ describe Admin::Settings::RegistrationsController do expect(response).to have_http_status(:success) end end + + describe 'PUT #update' do + it 'updates the settings' do + put :update, params: { form_admin_settings: { registrations_mode: 'open' } } + + expect(response).to redirect_to(admin_settings_registrations_path) + end + end end diff --git a/spec/controllers/admin/tags_controller_spec.rb b/spec/controllers/admin/tags_controller_spec.rb index 313298f14a..4e06adaca6 100644 --- a/spec/controllers/admin/tags_controller_spec.rb +++ b/spec/controllers/admin/tags_controller_spec.rb @@ -20,4 +20,26 @@ RSpec.describe Admin::TagsController do expect(response).to have_http_status(200) end end + + describe 'PUT #update' do + let!(:tag) { Fabricate(:tag, listable: false) } + + context 'with valid params' do + it 'updates the tag' do + put :update, params: { id: tag.id, tag: { listable: '1' } } + + expect(response).to redirect_to(admin_tag_path(tag.id)) + expect(tag.reload).to be_listable + end + end + + context 'with invalid params' do + it 'does not update the tag' do + put :update, params: { id: tag.id, tag: { name: 'cant-change-name' } } + + expect(response).to have_http_status(200) + expect(response).to render_template(:show) + end + end + end end diff --git a/spec/controllers/admin/webhooks_controller_spec.rb b/spec/controllers/admin/webhooks_controller_spec.rb index 0ccfbbcc6e..17d8506025 100644 --- a/spec/controllers/admin/webhooks_controller_spec.rb +++ b/spec/controllers/admin/webhooks_controller_spec.rb @@ -86,6 +86,24 @@ describe Admin::WebhooksController do end end + describe 'POST #enable' do + it 'enables the webhook' do + post :enable, params: { id: webhook.id } + + expect(webhook.reload).to be_enabled + expect(response).to redirect_to(admin_webhook_path(webhook)) + end + end + + describe 'POST #disable' do + it 'disables the webhook' do + post :disable, params: { id: webhook.id } + + expect(webhook.reload).to_not be_enabled + expect(response).to redirect_to(admin_webhook_path(webhook)) + end + end + describe 'DELETE #destroy' do it 'destroys the record' do expect do diff --git a/spec/controllers/settings/imports_controller_spec.rb b/spec/controllers/settings/imports_controller_spec.rb index 76e1e4ecb0..35d2f08193 100644 --- a/spec/controllers/settings/imports_controller_spec.rb +++ b/spec/controllers/settings/imports_controller_spec.rb @@ -252,6 +252,19 @@ RSpec.describe Settings::ImportsController do include_examples 'export failed rows', "https://foo.com/1\nhttps://foo.com/2\n" end + + context 'with lists' do + let(:import_type) { 'lists' } + + let!(:rows) do + [ + { 'list_name' => 'Amigos', 'acct' => 'user@example.com' }, + { 'list_name' => 'Frenemies', 'acct' => 'user@org.org' }, + ].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) } + end + + include_examples 'export failed rows', "Amigos,user@example.com\nFrenemies,user@org.org\n" + end end describe 'POST #create' do From 893b2f33fd92a320146e6abf172e21b1b340ae24 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Oct 2023 11:52:06 -0400 Subject: [PATCH 16/35] Extract shared example for cacheable response in specs (#25388) --- spec/controllers/accounts_controller_spec.rb | 25 +++---------------- .../collections_controller_spec.rb | 16 ------------ .../activitypub/outboxes_controller_spec.rb | 16 ------------ .../activitypub/replies_controller_spec.rb | 16 ------------ spec/controllers/statuses_controller_spec.rb | 23 ++--------------- spec/support/examples/cache.rb | 22 ++++++++++++++++ 6 files changed, 27 insertions(+), 91 deletions(-) create mode 100644 spec/support/examples/cache.rb diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index 5a8585b069..d46fdb108d 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -7,25 +7,6 @@ RSpec.describe AccountsController do let(:account) { Fabricate(:account) } - shared_examples 'cacheable response' do - it 'does not set cookies' do - expect(response.cookies).to be_empty - expect(response.headers['Set-Cookies']).to be_nil - end - - it 'does not set sessions' do - expect(session).to be_empty - end - - it 'returns Vary header' do - expect(response.headers['Vary']).to include 'Accept' - end - - it 'returns public Cache-Control header' do - expect(response.headers['Cache-Control']).to include 'public' - end - end - describe 'GET #show' do let(:format) { 'html' } @@ -186,7 +167,7 @@ RSpec.describe AccountsController do expect(response.media_type).to eq 'application/activity+json' end - it_behaves_like 'cacheable response' + it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' it 'renders account' do json = body_as_json @@ -244,7 +225,7 @@ RSpec.describe AccountsController do expect(response.media_type).to eq 'application/activity+json' end - it_behaves_like 'cacheable response' + it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' it 'renders account' do json = body_as_json @@ -311,7 +292,7 @@ RSpec.describe AccountsController do expect(response).to have_http_status(200) end - it_behaves_like 'cacheable response' + it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' end context 'with a normal account in an RSS request' do diff --git a/spec/controllers/activitypub/collections_controller_spec.rb b/spec/controllers/activitypub/collections_controller_spec.rb index e2802cf565..cf484ff5a4 100644 --- a/spec/controllers/activitypub/collections_controller_spec.rb +++ b/spec/controllers/activitypub/collections_controller_spec.rb @@ -7,22 +7,6 @@ RSpec.describe ActivityPub::CollectionsController do let!(:private_pinned) { Fabricate(:status, account: account, text: 'secret private stuff', visibility: :private) } let(:remote_account) { nil } - shared_examples 'cacheable response' do - it 'does not set cookies' do - expect(response.cookies).to be_empty - expect(response.headers['Set-Cookies']).to be_nil - end - - it 'does not set sessions' do - response - expect(session).to be_empty - end - - it 'returns public Cache-Control header' do - expect(response.headers['Cache-Control']).to include 'public' - end - end - before do allow(controller).to receive(:signed_request_actor).and_return(remote_account) diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/controllers/activitypub/outboxes_controller_spec.rb index 6946fdfcff..53c4f0c09c 100644 --- a/spec/controllers/activitypub/outboxes_controller_spec.rb +++ b/spec/controllers/activitypub/outboxes_controller_spec.rb @@ -5,22 +5,6 @@ require 'rails_helper' RSpec.describe ActivityPub::OutboxesController do let!(:account) { Fabricate(:account) } - shared_examples 'cacheable response' do - it 'does not set cookies' do - expect(response.cookies).to be_empty - expect(response.headers['Set-Cookies']).to be_nil - end - - it 'does not set sessions' do - response - expect(session).to be_empty - end - - it 'returns public Cache-Control header' do - expect(response.headers['Cache-Control']).to include 'public' - end - end - before do Fabricate(:status, account: account, visibility: :public) Fabricate(:status, account: account, visibility: :unlisted) diff --git a/spec/controllers/activitypub/replies_controller_spec.rb b/spec/controllers/activitypub/replies_controller_spec.rb index c7b65f004d..7e6e0ffb0d 100644 --- a/spec/controllers/activitypub/replies_controller_spec.rb +++ b/spec/controllers/activitypub/replies_controller_spec.rb @@ -8,22 +8,6 @@ RSpec.describe ActivityPub::RepliesController do let(:remote_reply_id) { 'https://foobar.com/statuses/1234' } let(:remote_querier) { nil } - shared_examples 'cacheable response' do - it 'does not set cookies' do - expect(response.cookies).to be_empty - expect(response.headers['Set-Cookies']).to be_nil - end - - it 'does not set sessions' do - response - expect(session).to be_empty - end - - it 'returns public Cache-Control header' do - expect(response.headers['Cache-Control']).to include 'public' - end - end - shared_examples 'common behavior' do context 'when status is private' do let(:parent_visibility) { :private } diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb index bd98929c02..8b715824b8 100644 --- a/spec/controllers/statuses_controller_spec.rb +++ b/spec/controllers/statuses_controller_spec.rb @@ -5,25 +5,6 @@ require 'rails_helper' describe StatusesController do render_views - shared_examples 'cacheable response' do - it 'does not set cookies' do - expect(response.cookies).to be_empty - expect(response.headers['Set-Cookies']).to be_nil - end - - it 'does not set sessions' do - expect(session).to be_empty - end - - it 'returns Vary header' do - expect(response.headers['Vary']).to include 'Accept, Accept-Language, Cookie' - end - - it 'returns public Cache-Control header' do - expect(response.headers['Cache-Control']).to include 'public' - end - end - describe 'GET #show' do let(:account) { Fabricate(:account) } let(:status) { Fabricate(:status, account: account) } @@ -88,7 +69,7 @@ describe StatusesController do context 'with JSON' do let(:format) { 'json' } - it_behaves_like 'cacheable response' + it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' it 'renders ActivityPub Note object successfully', :aggregate_failures do expect(response).to have_http_status(200) @@ -371,7 +352,7 @@ describe StatusesController do context 'with JSON' do let(:format) { 'json' } - it_behaves_like 'cacheable response' + it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' it 'renders ActivityPub Note object successfully', :aggregate_failures do expect(response).to have_http_status(200) diff --git a/spec/support/examples/cache.rb b/spec/support/examples/cache.rb new file mode 100644 index 0000000000..43cfbade82 --- /dev/null +++ b/spec/support/examples/cache.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +shared_examples 'cacheable response' do |expects_vary: false| + it 'does not set cookies' do + expect(response.cookies).to be_empty + expect(response.headers['Set-Cookies']).to be_nil + end + + it 'does not set sessions' do + expect(session).to be_empty + end + + if expects_vary + it 'returns Vary header' do + expect(response.headers['Vary']).to include(expects_vary) + end + end + + it 'returns public Cache-Control header' do + expect(response.headers['Cache-Control']).to include('public') + end +end From 8d0f12f776bcf7b6e0f1caabc812c5f473094a6a Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Oct 2023 12:02:15 -0400 Subject: [PATCH 17/35] Extract partials from `admin/accounts/show` view (#27428) --- app/views/admin/accounts/_buttons.html.haml | 41 ++++ app/views/admin/accounts/_counters.html.haml | 43 ++++ .../admin/accounts/_local_account.html.haml | 82 +++++++ .../admin/accounts/_remote_account.html.haml | 15 ++ app/views/admin/accounts/show.html.haml | 210 +----------------- 5 files changed, 185 insertions(+), 206 deletions(-) create mode 100644 app/views/admin/accounts/_buttons.html.haml create mode 100644 app/views/admin/accounts/_counters.html.haml create mode 100644 app/views/admin/accounts/_local_account.html.haml create mode 100644 app/views/admin/accounts/_remote_account.html.haml diff --git a/app/views/admin/accounts/_buttons.html.haml b/app/views/admin/accounts/_buttons.html.haml new file mode 100644 index 0000000000..6eb141abc9 --- /dev/null +++ b/app/views/admin/accounts/_buttons.html.haml @@ -0,0 +1,41 @@ +- if account.suspended? + %hr.spacer/ + - if account.suspension_origin_remote? + %p.muted-hint= deletion_request.present? ? t('admin.accounts.remote_suspension_reversible_hint_html', date: content_tag(:strong, l(deletion_request.due_at.to_date))) : t('admin.accounts.remote_suspension_irreversible') + - else + %p.muted-hint= deletion_request.present? ? t('admin.accounts.suspension_reversible_hint_html', date: content_tag(:strong, l(deletion_request.due_at.to_date))) : t('admin.accounts.suspension_irreversible') + = link_to t('admin.accounts.undo_suspension'), unsuspend_admin_account_path(account.id), method: :post, class: 'button' if can?(:unsuspend, account) + = link_to t('admin.accounts.redownload'), redownload_admin_account_path(account.id), method: :post, class: 'button' if can?(:redownload, account) && account.suspension_origin_remote? + - if deletion_request.present? + = link_to t('admin.accounts.delete'), admin_account_path(account.id), method: :delete, class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, account) +- else + .action-buttons + %div + - if account.local? && account.user_approved? + = link_to t('admin.accounts.warn'), new_admin_account_action_path(account.id, type: 'none'), class: 'button' if can?(:warn, account) + - if account.user_disabled? + = link_to t('admin.accounts.enable'), enable_admin_account_path(account.id), method: :post, class: 'button' if can?(:enable, account.user) + - else + = link_to t('admin.accounts.disable'), new_admin_account_action_path(account.id, type: 'disable'), class: 'button' if can?(:disable, account.user) + - if account.sensitized? + = link_to t('admin.accounts.undo_sensitized'), unsensitive_admin_account_path(account.id), method: :post, class: 'button' if can?(:unsensitive, account) + - elsif !account.local? || account.user_approved? + = link_to t('admin.accounts.sensitive'), new_admin_account_action_path(account.id, type: 'sensitive'), class: 'button' if can?(:sensitive, account) + - if account.silenced? + = link_to t('admin.accounts.undo_silenced'), unsilence_admin_account_path(account.id), method: :post, class: 'button' if can?(:unsilence, account) + - elsif !account.local? || account.user_approved? + = link_to t('admin.accounts.silence'), new_admin_account_action_path(account.id, type: 'silence'), class: 'button' if can?(:silence, account) + - if account.local? + - if account.user_pending? + = link_to t('admin.accounts.approve'), approve_admin_account_path(account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:approve, account.user) + = link_to t('admin.accounts.reject'), reject_admin_account_path(account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:reject, account.user) + - unless account.user_confirmed? + = link_to t('admin.accounts.confirm'), admin_account_confirmation_path(account.id), method: :post, class: 'button' if can?(:confirm, account.user) + - if !account.local? || account.user_approved? + = link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(account.id, type: 'suspend'), class: 'button' if can?(:suspend, account) + %div + - if account.local? + - if !account.memorial? && account.user_approved? + = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, account) + - else + = link_to t('admin.accounts.redownload'), redownload_admin_account_path(account.id), method: :post, class: 'button' if can?(:redownload, account) diff --git a/app/views/admin/accounts/_counters.html.haml b/app/views/admin/accounts/_counters.html.haml new file mode 100644 index 0000000000..00ab98d094 --- /dev/null +++ b/app/views/admin/accounts/_counters.html.haml @@ -0,0 +1,43 @@ +.dashboard__counters.admin-account-counters + %div + = link_to admin_account_statuses_path(account.id) do + .dashboard__counters__num= number_with_delimiter account.statuses_count + .dashboard__counters__label= t 'admin.accounts.statuses' + %div + = link_to admin_account_statuses_path(account.id, { media: true }) do + .dashboard__counters__num= number_to_human_size account.media_attachments.sum('file_file_size') + .dashboard__counters__label= t 'admin.accounts.media_attachments' + %div + = link_to admin_account_relationships_path(account.id, location: account.local? ? nil : 'local', relationship: 'followed_by') do + .dashboard__counters__num= number_with_delimiter account.local_followers_count + .dashboard__counters__label= t 'admin.accounts.followers' + %div + = link_to admin_reports_path(account_id: account.id) do + .dashboard__counters__num= number_with_delimiter account.reports.count + .dashboard__counters__label= t 'admin.accounts.show.created_reports' + %div + = link_to admin_reports_path(target_account_id: account.id) do + .dashboard__counters__num= number_with_delimiter account.targeted_reports.count + .dashboard__counters__label= t 'admin.accounts.show.targeted_reports' + %div + = link_to admin_action_logs_path(target_account_id: account.id) do + .dashboard__counters__text + - if account.local? && account.user.nil? + = t('admin.accounts.deleted') + - elsif account.memorial? + = t('admin.accounts.memorialized') + - elsif account.suspended? + = t('admin.accounts.suspended') + - elsif account.silenced? + = t('admin.accounts.silenced') + - elsif account.local? && account.user&.disabled? + = t('admin.accounts.disabled') + - elsif account.local? && !account.user&.confirmed? + = t('admin.accounts.confirming') + - elsif account.local? && !account.user_approved? + = t('admin.accounts.pending') + - elsif account.sensitized? + = t('admin.accounts.sensitive') + - else + = t('admin.accounts.no_limits_imposed') + .dashboard__counters__label= t 'admin.accounts.login_status' diff --git a/app/views/admin/accounts/_local_account.html.haml b/app/views/admin/accounts/_local_account.html.haml new file mode 100644 index 0000000000..4b361fc8d1 --- /dev/null +++ b/app/views/admin/accounts/_local_account.html.haml @@ -0,0 +1,82 @@ +- if account.avatar? + %tr + %th= t('admin.accounts.avatar') + %td= table_link_to 'trash', t('admin.accounts.remove_avatar'), remove_avatar_admin_account_path(account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:remove_avatar, account) + %td +- if account.header? + %tr + %th= t('admin.accounts.header') + %td= table_link_to 'trash', t('admin.accounts.remove_header'), remove_header_admin_account_path(account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:remove_header, account) + %td +%tr + %th= t('admin.accounts.role') + %td + - if account.user_role&.everyone? + = t('admin.accounts.no_role_assigned') + - else + = account.user_role&.name + %td + = table_link_to 'vcard', t('admin.accounts.change_role.label'), admin_user_role_path(account.user) if can?(:change_role, account.user) +%tr + %th{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= t('admin.accounts.email') + %td{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= account.user_email + %td= table_link_to 'edit', t('admin.accounts.change_email.label'), admin_account_change_email_path(account.id) if can?(:change_email, account.user) +%tr + %td= table_link_to 'search', t('admin.accounts.search_same_email_domain'), admin_accounts_path(email: "%@#{account.user_email.split('@').last}") +- if can?(:create, :email_domain_block) + %tr + %td= table_link_to 'ban', t('admin.accounts.add_email_domain_block'), new_admin_email_domain_block_path(_domain: account.user_email.split('@').last) +- if account.user_unconfirmed_email.present? + %tr + %th= t('admin.accounts.unconfirmed_email') + %td= account.user_unconfirmed_email + %td +%tr + %th= t('admin.accounts.email_status') + %td + - if account.user&.confirmed? + = t('admin.accounts.confirmed') + - else + = t('admin.accounts.confirming') + %td= table_link_to 'refresh', t('admin.accounts.resend_confirmation.send'), resend_admin_account_confirmation_path(account.id), method: :post if can?(:confirm, account.user) +%tr + %th{ rowspan: can?(:reset_password, account.user) ? 2 : 1 }= t('admin.accounts.security') + %td{ rowspan: can?(:reset_password, account.user) ? 2 : 1 } + - if account.user&.two_factor_enabled? + = t 'admin.accounts.security_measures.password_and_2fa' + - else + = t 'admin.accounts.security_measures.only_password' + %td + - if account.user&.two_factor_enabled? + = table_link_to 'unlock', t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(account.user.id), method: :delete if can?(:disable_2fa, account.user) +- if can?(:reset_password, account.user) + %tr + %td + = table_link_to 'key', t('admin.accounts.reset_password'), admin_account_reset_path(account.id), method: :create, data: { confirm: t('admin.accounts.are_you_sure') } +%tr + %th= t('simple_form.labels.defaults.locale') + %td= standard_locale_name(account.user_locale) + %td +%tr + %th= t('admin.accounts.joined') + %td + %time.formatted{ datetime: account.created_at.iso8601, title: l(account.created_at) }= l account.created_at + %td +- recent_ips = account.user.ips.order(used_at: :desc).to_a +- recent_ips.each_with_index do |recent_ip, i| + %tr + - if i.zero? + %th{ rowspan: recent_ips.size }= t('admin.accounts.most_recent_ip') + %td= recent_ip.ip + %td= table_link_to 'search', t('admin.accounts.search_same_ip'), admin_accounts_path(ip: recent_ip.ip) +%tr + %th= t('admin.accounts.most_recent_activity') + %td + - if account.user_current_sign_in_at + %time.formatted{ datetime: account.user_current_sign_in_at.iso8601, title: l(account.user_current_sign_in_at) }= l account.user_current_sign_in_at + %td +- if account.user&.invited? + %tr + %th= t('admin.accounts.invited_by') + %td= admin_account_link_to account.user.invite.user.account + %td diff --git a/app/views/admin/accounts/_remote_account.html.haml b/app/views/admin/accounts/_remote_account.html.haml new file mode 100644 index 0000000000..99996e1d46 --- /dev/null +++ b/app/views/admin/accounts/_remote_account.html.haml @@ -0,0 +1,15 @@ +%tr + %th= t('admin.accounts.inbox_url') + %td + = account.inbox_url + = fa_icon DeliveryFailureTracker.available?(account.inbox_url) ? 'check' : 'times' + %td + = table_link_to 'search', domain_block.present? ? t('admin.domain_blocks.view') : t('admin.accounts.view_domain'), admin_instance_path(account.domain) +%tr + %th= t('admin.accounts.shared_inbox_url') + %td + = account.shared_inbox_url + = fa_icon DeliveryFailureTracker.available?(account.shared_inbox_url) ? 'check' : 'times' + %td + - if domain_block.nil? + = table_link_to 'ban', t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: account.domain) diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 7801ef1913..80b8fc92c2 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -27,49 +27,7 @@ %div .account__header__content.emojify= prerender_custom_emojis(account_bio_format(account), account.emojis) -.dashboard__counters.admin-account-counters - %div - = link_to admin_account_statuses_path(@account.id) do - .dashboard__counters__num= number_with_delimiter @account.statuses_count - .dashboard__counters__label= t 'admin.accounts.statuses' - %div - = link_to admin_account_statuses_path(@account.id, { media: true }) do - .dashboard__counters__num= number_to_human_size @account.media_attachments.sum('file_file_size') - .dashboard__counters__label= t 'admin.accounts.media_attachments' - %div - = link_to admin_account_relationships_path(@account.id, location: @account.local? ? nil : 'local', relationship: 'followed_by') do - .dashboard__counters__num= number_with_delimiter @account.local_followers_count - .dashboard__counters__label= t 'admin.accounts.followers' - %div - = link_to admin_reports_path(account_id: @account.id) do - .dashboard__counters__num= number_with_delimiter @account.reports.count - .dashboard__counters__label= t '.created_reports' - %div - = link_to admin_reports_path(target_account_id: @account.id) do - .dashboard__counters__num= number_with_delimiter @account.targeted_reports.count - .dashboard__counters__label= t '.targeted_reports' - %div - = link_to admin_action_logs_path(target_account_id: @account.id) do - .dashboard__counters__text - - if @account.local? && @account.user.nil? - = t('admin.accounts.deleted') - - elsif @account.memorial? - = t('admin.accounts.memorialized') - - elsif @account.suspended? - = t('admin.accounts.suspended') - - elsif @account.silenced? - = t('admin.accounts.silenced') - - elsif @account.local? && @account.user&.disabled? - = t('admin.accounts.disabled') - - elsif @account.local? && !@account.user&.confirmed? - = t('admin.accounts.confirming') - - elsif @account.local? && !@account.user_approved? - = t('admin.accounts.pending') - - elsif @account.sensitized? - = t('admin.accounts.sensitive') - - else - = t('admin.accounts.no_limits_imposed') - .dashboard__counters__label= t 'admin.accounts.login_status' += render 'admin/accounts/counters', account: @account - if @account.local? && @account.user.nil? = link_to t('admin.accounts.unblock_email'), unblock_email_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unblock_email, @account) && CanonicalEmailBlock.exists?(reference_account_id: @account.id) @@ -78,171 +36,11 @@ %table.table.inline-table %tbody - if @account.local? - - if @account.avatar? - %tr - %th= t('admin.accounts.avatar') - %td= table_link_to 'trash', t('admin.accounts.remove_avatar'), remove_avatar_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:remove_avatar, @account) - %td - - - if @account.header? - %tr - %th= t('admin.accounts.header') - %td= table_link_to 'trash', t('admin.accounts.remove_header'), remove_header_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:remove_header, @account) - %td - - %tr - %th= t('admin.accounts.role') - %td - - if @account.user_role&.everyone? - = t('admin.accounts.no_role_assigned') - - else - = @account.user_role&.name - %td - = table_link_to 'vcard', t('admin.accounts.change_role.label'), admin_user_role_path(@account.user) if can?(:change_role, @account.user) - - %tr - %th{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= t('admin.accounts.email') - %td{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= @account.user_email - %td= table_link_to 'edit', t('admin.accounts.change_email.label'), admin_account_change_email_path(@account.id) if can?(:change_email, @account.user) - - %tr - %td= table_link_to 'search', t('admin.accounts.search_same_email_domain'), admin_accounts_path(email: "%@#{@account.user_email.split('@').last}") - - - if can?(:create, :email_domain_block) - %tr - %td= table_link_to 'ban', t('admin.accounts.add_email_domain_block'), new_admin_email_domain_block_path(_domain: @account.user_email.split('@').last) - - - if @account.user_unconfirmed_email.present? - %tr - %th= t('admin.accounts.unconfirmed_email') - %td= @account.user_unconfirmed_email - %td - - %tr - %th= t('admin.accounts.email_status') - %td - - if @account.user&.confirmed? - = t('admin.accounts.confirmed') - - else - = t('admin.accounts.confirming') - %td= table_link_to 'refresh', t('admin.accounts.resend_confirmation.send'), resend_admin_account_confirmation_path(@account.id), method: :post if can?(:confirm, @account.user) - %tr - %th{ rowspan: can?(:reset_password, @account.user) ? 2 : 1 }= t('admin.accounts.security') - %td{ rowspan: can?(:reset_password, @account.user) ? 2 : 1 } - - if @account.user&.two_factor_enabled? - = t 'admin.accounts.security_measures.password_and_2fa' - - else - = t 'admin.accounts.security_measures.only_password' - %td - - if @account.user&.two_factor_enabled? - = table_link_to 'unlock', t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete if can?(:disable_2fa, @account.user) - - - if can?(:reset_password, @account.user) - %tr - %td - = table_link_to 'key', t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, data: { confirm: t('admin.accounts.are_you_sure') } - - %tr - %th= t('simple_form.labels.defaults.locale') - %td= standard_locale_name(@account.user_locale) - %td - - %tr - %th= t('admin.accounts.joined') - %td - %time.formatted{ datetime: @account.created_at.iso8601, title: l(@account.created_at) }= l @account.created_at - %td - - - recent_ips = @account.user.ips.order(used_at: :desc).to_a - - - recent_ips.each_with_index do |recent_ip, i| - %tr - - if i.zero? - %th{ rowspan: recent_ips.size }= t('admin.accounts.most_recent_ip') - %td= recent_ip.ip - %td= table_link_to 'search', t('admin.accounts.search_same_ip'), admin_accounts_path(ip: recent_ip.ip) - - %tr - %th= t('admin.accounts.most_recent_activity') - %td - - if @account.user_current_sign_in_at - %time.formatted{ datetime: @account.user_current_sign_in_at.iso8601, title: l(@account.user_current_sign_in_at) }= l @account.user_current_sign_in_at - %td - - - if @account.user&.invited? - %tr - %th= t('admin.accounts.invited_by') - %td= admin_account_link_to @account.user.invite.user.account - %td - + = render 'admin/accounts/local_account', account: @account - else - %tr - %th= t('admin.accounts.inbox_url') - %td - = @account.inbox_url - = fa_icon DeliveryFailureTracker.available?(@account.inbox_url) ? 'check' : 'times' - %td - = table_link_to 'search', @domain_block.present? ? t('admin.domain_blocks.view') : t('admin.accounts.view_domain'), admin_instance_path(@account.domain) - %tr - %th= t('admin.accounts.shared_inbox_url') - %td - = @account.shared_inbox_url - = fa_icon DeliveryFailureTracker.available?(@account.shared_inbox_url) ? 'check' : 'times' - %td - - if @domain_block.nil? - = table_link_to 'ban', t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain) + = render 'admin/accounts/remote_account', account: @account, domain_block: @domain_block - - if @account.suspended? - %hr.spacer/ - - - if @account.suspension_origin_remote? - %p.muted-hint= @deletion_request.present? ? t('admin.accounts.remote_suspension_reversible_hint_html', date: content_tag(:strong, l(@deletion_request.due_at.to_date))) : t('admin.accounts.remote_suspension_irreversible') - - else - %p.muted-hint= @deletion_request.present? ? t('admin.accounts.suspension_reversible_hint_html', date: content_tag(:strong, l(@deletion_request.due_at.to_date))) : t('admin.accounts.suspension_irreversible') - - = link_to t('admin.accounts.undo_suspension'), unsuspend_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsuspend, @account) - = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account) && @account.suspension_origin_remote? - - - if @deletion_request.present? - = link_to t('admin.accounts.delete'), admin_account_path(@account.id), method: :delete, class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, @account) - - else - .action-buttons - %div - - if @account.local? && @account.user_approved? - = link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account) - - - if @account.user_disabled? - = link_to t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post, class: 'button' if can?(:enable, @account.user) - - else - = link_to t('admin.accounts.disable'), new_admin_account_action_path(@account.id, type: 'disable'), class: 'button' if can?(:disable, @account.user) - - - if @account.sensitized? - = link_to t('admin.accounts.undo_sensitized'), unsensitive_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsensitive, @account) - - elsif !@account.local? || @account.user_approved? - = link_to t('admin.accounts.sensitive'), new_admin_account_action_path(@account.id, type: 'sensitive'), class: 'button' if can?(:sensitive, @account) - - - if @account.silenced? - = link_to t('admin.accounts.undo_silenced'), unsilence_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsilence, @account) - - elsif !@account.local? || @account.user_approved? - = link_to t('admin.accounts.silence'), new_admin_account_action_path(@account.id, type: 'silence'), class: 'button' if can?(:silence, @account) - - - if @account.local? - - if @account.user_pending? - = link_to t('admin.accounts.approve'), approve_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:approve, @account.user) - = link_to t('admin.accounts.reject'), reject_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:reject, @account.user) - - - unless @account.user_confirmed? - = link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' if can?(:confirm, @account.user) - - - if !@account.local? || @account.user_approved? - = link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button' if can?(:suspend, @account) - - %div - - if @account.local? - - if !@account.memorial? && @account.user_approved? - = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account) - - else - = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account) + = render 'admin/accounts/buttons', account: @account, deletion_request: @deletion_request %hr.spacer/ From 33b073f77daa52c04d6615feaec0edf7651ad413 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Oct 2023 12:07:17 -0400 Subject: [PATCH 18/35] Extract partials from `admin/reports/show` view (#27427) --- app/views/admin/reports/_comment.html.haml | 24 ++++ .../admin/reports/_header_card.html.haml | 46 +++++++ .../admin/reports/_header_details.html.haml | 53 +++++++ app/views/admin/reports/show.html.haml | 130 +----------------- 4 files changed, 126 insertions(+), 127 deletions(-) create mode 100644 app/views/admin/reports/_comment.html.haml create mode 100644 app/views/admin/reports/_header_card.html.haml create mode 100644 app/views/admin/reports/_header_details.html.haml diff --git a/app/views/admin/reports/_comment.html.haml b/app/views/admin/reports/_comment.html.haml new file mode 100644 index 0000000000..8c07210af9 --- /dev/null +++ b/app/views/admin/reports/_comment.html.haml @@ -0,0 +1,24 @@ +- if report.account.instance_actor? + %p= t('admin.reports.comment_description_html', name: content_tag(:strong, site_hostname, class: 'username')) +- elsif report.account.local? + %p= t('admin.reports.comment_description_html', name: content_tag(:strong, report.account.username, class: 'username')) +- else + %p= t('admin.reports.comment_description_html', name: t('admin.reports.remote_user_placeholder', instance: report.account.domain)) +.report-notes + .report-notes__item + - if report.account.local? && !report.account.instance_actor? + = image_tag report.account.avatar.url, class: 'report-notes__item__avatar' + - else + = image_tag(full_asset_url('avatars/original/missing.png', skip_pipeline: true), class: 'report-notes__item__avatar') + .report-notes__item__header + %span.username + - if report.account.instance_actor? + = site_hostname + - elsif report.account.local? + = link_to report.account.username, admin_account_path(report.account_id) + - else + = link_to report.account.domain, admin_instance_path(report.account.domain) + %time.relative-formatted{ datetime: report.created_at.iso8601 } + = l report.created_at.to_date + .report-notes__item__content + = simple_format(h(report.comment)) diff --git a/app/views/admin/reports/_header_card.html.haml b/app/views/admin/reports/_header_card.html.haml new file mode 100644 index 0000000000..6fd8b4ecc8 --- /dev/null +++ b/app/views/admin/reports/_header_card.html.haml @@ -0,0 +1,46 @@ +.report-header__card + .account-card + .account-card__header + = image_tag report.target_account.header.url, alt: '' + .account-card__title + .account-card__title__avatar + = image_tag report.target_account.avatar.url, alt: '' + .display-name + %bdi + %strong.emojify.p-name= display_name(report.target_account, custom_emojify: true) + %span + = acct(report.target_account) + = fa_icon('lock') if report.target_account.locked? + - if report.target_account.note.present? + .account-card__bio.emojify + = prerender_custom_emojis(account_bio_format(report.target_account), report.target_account.emojis) + .account-card__actions + .account-card__counters + .account-card__counters__item + = friendly_number_to_human report.target_account.statuses_count + %small= t('accounts.posts', count: report.target_account.statuses_count).downcase + .account-card__counters__item + = friendly_number_to_human report.target_account.followers_count + %small= t('accounts.followers', count: report.target_account.followers_count).downcase + .account-card__counters__item + = friendly_number_to_human report.target_account.following_count + %small= t('accounts.following', count: report.target_account.following_count).downcase + .account-card__actions__button + = link_to t('admin.reports.view_profile'), admin_account_path(report.target_account_id), class: 'button' + .report-header__details.report-header__details--horizontal + .report-header__details__item + .report-header__details__item__header + %strong= t('admin.accounts.joined') + .report-header__details__item__content + %time.time-ago{ datetime: report.target_account.created_at.iso8601, title: l(report.target_account.created_at) }= l report.target_account.created_at + .report-header__details__item + .report-header__details__item__header + %strong= t('accounts.last_active') + .report-header__details__item__content + - if report.target_account.last_status_at.present? + %time.time-ago{ datetime: report.target_account.last_status_at.to_date.iso8601, title: l(report.target_account.last_status_at.to_date) }= l report.target_account.last_status_at + .report-header__details__item + .report-header__details__item__header + %strong= t('admin.accounts.strikes') + .report-header__details__item__content + = report.target_account.previous_strikes_count diff --git a/app/views/admin/reports/_header_details.html.haml b/app/views/admin/reports/_header_details.html.haml new file mode 100644 index 0000000000..5878cd2ff8 --- /dev/null +++ b/app/views/admin/reports/_header_details.html.haml @@ -0,0 +1,53 @@ +.report-header__details + .report-header__details__item + .report-header__details__item__header + %strong= t('admin.reports.created_at') + .report-header__details__item__content + %time.formatted{ datetime: report.created_at.iso8601 } + .report-header__details__item + .report-header__details__item__header + %strong= t('admin.reports.reported_by') + .report-header__details__item__content + - if report.account.instance_actor? + = site_hostname + - elsif report.account.local? + = admin_account_link_to report.account + - else + = report.account.domain + .report-header__details__item + .report-header__details__item__header + %strong= t('admin.reports.status') + .report-header__details__item__content + - if report.action_taken? + = t('admin.reports.resolved') + - else + = t('admin.reports.unresolved') + - unless report.target_account.local? + .report-header__details__item + .report-header__details__item__header + %strong= t('admin.reports.forwarded') + .report-header__details__item__content + - if report.forwarded? + = t('simple_form.yes') + - else + = t('simple_form.no') + - if report.action_taken_by_account.present? + .report-header__details__item + .report-header__details__item__header + %strong= t('admin.reports.action_taken_by') + .report-header__details__item__content + = admin_account_link_to report.action_taken_by_account + - else + .report-header__details__item + .report-header__details__item__header + %strong= t('admin.reports.assigned') + .report-header__details__item__content + - if report.assigned_account.nil? + = t 'admin.reports.no_one_assigned' + - else + = admin_account_link_to report.assigned_account + — + - if report.assigned_account != current_user.account + = table_link_to 'user', t('admin.reports.assign_to_self'), assign_to_self_admin_report_path(report), method: :post + - elsif !report.assigned_account.nil? + = table_link_to 'trash', t('admin.reports.unassign'), unassign_admin_report_path(report), method: :post diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index 41ce73cfcf..13a4d48344 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -8,106 +8,8 @@ = link_to t('admin.reports.mark_as_unresolved'), reopen_admin_report_path(@report), method: :post, class: 'button' .report-header - .report-header__card - .account-card - .account-card__header - = image_tag @report.target_account.header.url, alt: '' - .account-card__title - .account-card__title__avatar - = image_tag @report.target_account.avatar.url, alt: '' - .display-name - %bdi - %strong.emojify.p-name= display_name(@report.target_account, custom_emojify: true) - %span - = acct(@report.target_account) - = fa_icon('lock') if @report.target_account.locked? - - if @report.target_account.note.present? - .account-card__bio.emojify - = prerender_custom_emojis(account_bio_format(@report.target_account), @report.target_account.emojis) - .account-card__actions - .account-card__counters - .account-card__counters__item - = friendly_number_to_human @report.target_account.statuses_count - %small= t('accounts.posts', count: @report.target_account.statuses_count).downcase - .account-card__counters__item - = friendly_number_to_human @report.target_account.followers_count - %small= t('accounts.followers', count: @report.target_account.followers_count).downcase - .account-card__counters__item - = friendly_number_to_human @report.target_account.following_count - %small= t('accounts.following', count: @report.target_account.following_count).downcase - .account-card__actions__button - = link_to t('admin.reports.view_profile'), admin_account_path(@report.target_account_id), class: 'button' - .report-header__details.report-header__details--horizontal - .report-header__details__item - .report-header__details__item__header - %strong= t('admin.accounts.joined') - .report-header__details__item__content - %time.time-ago{ datetime: @report.target_account.created_at.iso8601, title: l(@report.target_account.created_at) }= l @report.target_account.created_at - .report-header__details__item - .report-header__details__item__header - %strong= t('accounts.last_active') - .report-header__details__item__content - - if @report.target_account.last_status_at.present? - %time.time-ago{ datetime: @report.target_account.last_status_at.to_date.iso8601, title: l(@report.target_account.last_status_at.to_date) }= l @report.target_account.last_status_at - .report-header__details__item - .report-header__details__item__header - %strong= t('admin.accounts.strikes') - .report-header__details__item__content - = @report.target_account.previous_strikes_count - - .report-header__details - .report-header__details__item - .report-header__details__item__header - %strong= t('admin.reports.created_at') - .report-header__details__item__content - %time.formatted{ datetime: @report.created_at.iso8601 } - .report-header__details__item - .report-header__details__item__header - %strong= t('admin.reports.reported_by') - .report-header__details__item__content - - if @report.account.instance_actor? - = site_hostname - - elsif @report.account.local? - = admin_account_link_to @report.account - - else - = @report.account.domain - .report-header__details__item - .report-header__details__item__header - %strong= t('admin.reports.status') - .report-header__details__item__content - - if @report.action_taken? - = t('admin.reports.resolved') - - else - = t('admin.reports.unresolved') - - unless @report.target_account.local? - .report-header__details__item - .report-header__details__item__header - %strong= t('admin.reports.forwarded') - .report-header__details__item__content - - if @report.forwarded? - = t('simple_form.yes') - - else - = t('simple_form.no') - - if @report.action_taken_by_account.present? - .report-header__details__item - .report-header__details__item__header - %strong= t('admin.reports.action_taken_by') - .report-header__details__item__content - = admin_account_link_to @report.action_taken_by_account - - else - .report-header__details__item - .report-header__details__item__header - %strong= t('admin.reports.assigned') - .report-header__details__item__content - - if @report.assigned_account.nil? - = t 'admin.reports.no_one_assigned' - - else - = admin_account_link_to @report.assigned_account - — - - if @report.assigned_account != current_user.account - = table_link_to 'user', t('admin.reports.assign_to_self'), assign_to_self_admin_report_path(@report), method: :post - - elsif !@report.assigned_account.nil? - = table_link_to 'trash', t('admin.reports.unassign'), unassign_admin_report_path(@report), method: :post + = render 'admin/reports/header_card', report: @report + = render 'admin/reports/header_details', report: @report %hr.spacer @@ -118,33 +20,7 @@ = react_admin_component :report_reason_selector, id: @report.id, category: @report.category, rule_ids: @report.rule_ids&.map(&:to_s), disabled: @report.action_taken? - if @report.comment.present? - - if @report.account.instance_actor? - %p= t('admin.reports.comment_description_html', name: content_tag(:strong, site_hostname, class: 'username')) - - elsif @report.account.local? - %p= t('admin.reports.comment_description_html', name: content_tag(:strong, @report.account.username, class: 'username')) - - else - %p= t('admin.reports.comment_description_html', name: t('admin.reports.remote_user_placeholder', instance: @report.account.domain)) - - .report-notes - .report-notes__item - - if @report.account.local? && !@report.account.instance_actor? - = image_tag @report.account.avatar.url, class: 'report-notes__item__avatar' - - else - = image_tag(full_asset_url('avatars/original/missing.png', skip_pipeline: true), class: 'report-notes__item__avatar') - - .report-notes__item__header - %span.username - - if @report.account.instance_actor? - = site_hostname - - elsif @report.account.local? - = link_to @report.account.username, admin_account_path(@report.account_id) - - else - = link_to @report.account.domain, admin_instance_path(@report.account.domain) - %time.relative-formatted{ datetime: @report.created_at.iso8601 } - = l @report.created_at.to_date - - .report-notes__item__content - = simple_format(h(@report.comment)) + = render 'admin/reports/comment', report: @report %hr.spacer/ From fc9ab61448af94513524f14f5fd287d2a26ceeab Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Oct 2023 12:20:58 -0400 Subject: [PATCH 19/35] Expand spec coverage of `CLI::Media` (#27437) --- spec/lib/mastodon/cli/media_spec.rb | 69 +++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/spec/lib/mastodon/cli/media_spec.rb b/spec/lib/mastodon/cli/media_spec.rb index 29f7d424a9..9543640e96 100644 --- a/spec/lib/mastodon/cli/media_spec.rb +++ b/spec/lib/mastodon/cli/media_spec.rb @@ -4,9 +4,78 @@ require 'rails_helper' require 'mastodon/cli/media' describe Mastodon::CLI::Media do + let(:cli) { described_class.new } + describe '.exit_on_failure?' do it 'returns true' do expect(described_class.exit_on_failure?).to be true end end + + describe '#remove' do + context 'with --prune-profiles and --remove-headers' do + let(:options) { { prune_profiles: true, remove_headers: true } } + + it 'warns about usage and exits' do + expect { cli.invoke(:remove, [], options) }.to output( + a_string_including('--prune-profiles and --remove-headers should not be specified simultaneously') + ).to_stdout.and raise_error(SystemExit) + end + end + + context 'with --include-follows but not including --prune-profiles and --remove-headers' do + let(:options) { { include_follows: true } } + + it 'warns about usage and exits' do + expect { cli.invoke(:remove, [], options) }.to output( + a_string_including('--include-follows can only be used with --prune-profiles or --remove-headers') + ).to_stdout.and raise_error(SystemExit) + end + end + + context 'with a relevant account' do + let!(:account) do + Fabricate(:account, domain: 'example.com', updated_at: 1.month.ago, last_webfingered_at: 1.month.ago, avatar: attachment_fixture('attachment.jpg'), header: attachment_fixture('attachment.jpg')) + end + + context 'with --prune-profiles' do + let(:options) { { prune_profiles: true } } + + it 'removes account avatars' do + expect { cli.invoke(:remove, [], options) }.to output( + a_string_including('Visited 1') + ).to_stdout + + expect(account.reload.avatar).to be_blank + end + end + + context 'with --remove-headers' do + let(:options) { { remove_headers: true } } + + it 'removes account header' do + expect { cli.invoke(:remove, [], options) }.to output( + a_string_including('Visited 1') + ).to_stdout + + expect(account.reload.header).to be_blank + end + end + end + + context 'with a relevant media attachment' do + let!(:media_attachment) { Fabricate(:media_attachment, remote_url: 'https://example.com/image.jpg', created_at: 1.month.ago) } + + context 'without options' do + it 'removes account avatars' do + expect { cli.invoke(:remove) }.to output( + a_string_including('Removed 1') + ).to_stdout + + expect(media_attachment.reload.file).to be_blank + expect(media_attachment.reload.thumbnail).to be_blank + end + end + end + end end From 108470341788befa4602f315798c8de55d170553 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 17 Oct 2023 13:03:54 +0200 Subject: [PATCH 20/35] Update changelog (#27440) --- CHANGELOG.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fab3104bac..f9303f0115 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,46 @@ All notable changes to this project will be documented in this file. +## [4.2.1] - 2023-10-10 + +### Added + +- Add redirection on `/deck` URLs for logged-out users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27128)) +- Add support for v4.2.0 migrations to `tootctl maintenance fix-duplicates` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27147)) + +### Changed + +- Change some worker lock TTLs to be shorter-lived ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27246)) +- Change user archive export allowed period from 7 days to 6 days ([suddjian](https://github.com/mastodon/mastodon/pull/27200)) + +### Fixed + +- Fix duplicate reports being sent when reporting some remote posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27355)) +- Fix clicking on already-opened thread post scrolling to the top of the thread ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27331), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27338), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27350)) +- Fix some remote posts getting truncated ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27307)) +- Fix some cases of infinite scroll code trying to fetch inaccessible posts in a loop ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27286)) +- Fix `Vary` headers not being set on some redirects ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27272)) +- Fix mentions being matched in some URL query strings ([mjankowski](https://github.com/mastodon/mastodon/pull/25656)) +- Fix unexpected linebreak in version string in the Web UI ([vmstan](https://github.com/mastodon/mastodon/pull/26986)) +- Fix double scroll bars in some columns in advanced interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27187)) +- Fix boosts of local users being filtered in account timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27204)) +- Fix multiple instances of the trend refresh scheduler sometimes running at once ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27253)) +- Fix importer returning negative row estimates ([jgillich](https://github.com/mastodon/mastodon/pull/27258)) +- Fix incorrectly keeping outdated update notices absent from the API endpoint ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27021)) +- Fix import progress not updating on certain failures ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27247)) +- Fix websocket connections being incorrectly decremented twice on errors ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/27238)) +- Fix explore prompt appearing because of posts being received out of order ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27211)) +- Fix explore prompt sometimes showing up when the home TL is loading ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27062)) +- Fix link handling of mentions in user profiles when logged out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27185)) +- Fix filtering audit log for entries about disabling 2FA ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27186)) +- Fix notification toasts not respecting reduce-motion ([c960657](https://github.com/mastodon/mastodon/pull/27178)) +- Fix retention dashboard not displaying correct month ([vmstan](https://github.com/mastodon/mastodon/pull/27180)) +- Fix tIME chunk not being properly removed from PNG uploads ([TheEssem](https://github.com/mastodon/mastodon/pull/27111)) +- Fix division by zero in video in bitrate computation code ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27129)) +- Fix inefficient queries in “Follows and followers” as well as several admin pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27116), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27306)) +- Fix ActiveRecord using two connection pools when no replica is defined ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27061)) +- Fix the search documentation URL in system checks ([renchap](https://github.com/mastodon/mastodon/pull/27036)) + ## [4.2.0] - 2023-09-21 The following changelog entries focus on changes visible to users, administrators, client developers or federated software developers, but there has also been a lot of code modernization, refactoring, and tooling work, in particular by [@danielmbrasil](https://github.com/danielmbrasil), [@mjankowski](https://github.com/mjankowski), [@nschonni](https://github.com/nschonni), [@renchap](https://github.com/renchap), and [@takayamaki](https://github.com/takayamaki). From 19900f647e81cb7078b9d329c4b57477068c4064 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 17 Oct 2023 07:05:28 -0400 Subject: [PATCH 21/35] Add coverage for `UnreservedUsernameValidator` (#25590) --- .../unreserved_username_validator.rb | 29 ++++- .../unreserved_username_validator_spec.rb | 123 ++++++++++++++---- 2 files changed, 122 insertions(+), 30 deletions(-) diff --git a/app/validators/unreserved_username_validator.rb b/app/validators/unreserved_username_validator.rb index f82f4b91d0..55a8c835fa 100644 --- a/app/validators/unreserved_username_validator.rb +++ b/app/validators/unreserved_username_validator.rb @@ -11,16 +11,31 @@ class UnreservedUsernameValidator < ActiveModel::Validator private - def pam_controlled? - return false unless Devise.pam_authentication && Devise.pam_controlled_service - - Rpam2.account(Devise.pam_controlled_service, @username).present? + def reserved_username? + pam_username_reserved? || settings_username_reserved? end - def reserved_username? - return true if pam_controlled? - return false unless Setting.reserved_usernames + def pam_username_reserved? + pam_controlled? && pam_reserves_username? + end + def pam_controlled? + Devise.pam_authentication && Devise.pam_controlled_service + end + + def pam_reserves_username? + Rpam2.account(Devise.pam_controlled_service, @username) + end + + def settings_username_reserved? + settings_has_reserved_usernames? && settings_reserves_username? + end + + def settings_has_reserved_usernames? + Setting.reserved_usernames.present? + end + + def settings_reserves_username? Setting.reserved_usernames.include?(@username.downcase) end end diff --git a/spec/validators/unreserved_username_validator_spec.rb b/spec/validators/unreserved_username_validator_spec.rb index 6f353eeafd..0eb5f83683 100644 --- a/spec/validators/unreserved_username_validator_spec.rb +++ b/spec/validators/unreserved_username_validator_spec.rb @@ -2,41 +2,118 @@ require 'rails_helper' -RSpec.describe UnreservedUsernameValidator, type: :validator do - describe '#validate' do - before do - allow(validator).to receive(:reserved_username?) { reserved_username } - validator.validate(account) +describe UnreservedUsernameValidator do + let(:record_class) do + Class.new do + include ActiveModel::Validations + attr_accessor :username + + validates_with UnreservedUsernameValidator end + end + let(:record) { record_class.new } - let(:validator) { described_class.new } - let(:account) { instance_double(Account, username: username, errors: errors) } - let(:errors) { instance_double(ActiveModel::Errors, add: nil) } + describe '#validate' do + context 'when username is nil' do + it 'does not add errors' do + record.username = nil - context 'when @username is blank?' do - let(:username) { nil } - - it 'not calls errors.add' do - expect(errors).to_not have_received(:add).with(:username, any_args) + expect(record).to be_valid + expect(record.errors).to be_empty end end - context 'when @username is not blank?' do - let(:username) { 'f' } + context 'when PAM is enabled' do + before do + allow(Devise).to receive(:pam_authentication).and_return(true) + end - context 'with reserved_username?' do - let(:reserved_username) { true } + context 'with a pam service available' do + let(:service) { double } + let(:pam_class) do + Class.new do + def self.account(service, username); end + end + end - it 'calls errors.add' do - expect(errors).to have_received(:add).with(:username, :reserved) + before do + stub_const('Rpam2', pam_class) + allow(Devise).to receive(:pam_controlled_service).and_return(service) + end + + context 'when the account exists' do + before do + allow(Rpam2).to receive(:account).with(service, 'username').and_return(true) + end + + it 'adds errors to the record' do + record.username = 'username' + + expect(record).to_not be_valid + expect(record.errors.first.attribute).to eq(:username) + expect(record.errors.first.type).to eq(:reserved) + end + end + + context 'when the account does not exist' do + before do + allow(Rpam2).to receive(:account).with(service, 'username').and_return(false) + end + + it 'does not add errors to the record' do + record.username = 'username' + + expect(record).to be_valid + expect(record.errors).to be_empty + end end end - context 'when username is not reserved' do - let(:reserved_username) { false } + context 'without a pam service' do + before do + allow(Devise).to receive(:pam_controlled_service).and_return(false) + end - it 'not calls errors.add' do - expect(errors).to_not have_received(:add).with(:username, any_args) + context 'when there are not any reserved usernames' do + before do + stub_reserved_usernames(nil) + end + + it 'does not add errors to the record' do + record.username = 'username' + + expect(record).to be_valid + expect(record.errors).to be_empty + end + end + + context 'when there are reserved usernames' do + before do + stub_reserved_usernames(%w(alice bob)) + end + + context 'when the username is reserved' do + it 'adds errors to the record' do + record.username = 'alice' + + expect(record).to_not be_valid + expect(record.errors.first.attribute).to eq(:username) + expect(record.errors.first.type).to eq(:reserved) + end + end + + context 'when the username is not reserved' do + it 'does not add errors to the record' do + record.username = 'chris' + + expect(record).to be_valid + expect(record.errors).to be_empty + end + end + end + + def stub_reserved_usernames(value) + allow(Setting).to receive(:[]).with('reserved_usernames').and_return(value) end end end From d54fec24e5ab97c0529172ee30d45350453e5b3c Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 17 Oct 2023 07:06:16 -0400 Subject: [PATCH 22/35] Add coverage for `CLI::PreviewCards#remove` command (#27441) --- spec/lib/mastodon/cli/preview_cards_spec.rb | 43 +++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/spec/lib/mastodon/cli/preview_cards_spec.rb b/spec/lib/mastodon/cli/preview_cards_spec.rb index b4b018b3be..1e064ed58e 100644 --- a/spec/lib/mastodon/cli/preview_cards_spec.rb +++ b/spec/lib/mastodon/cli/preview_cards_spec.rb @@ -4,9 +4,52 @@ require 'rails_helper' require 'mastodon/cli/preview_cards' describe Mastodon::CLI::PreviewCards do + let(:cli) { described_class.new } + describe '.exit_on_failure?' do it 'returns true' do expect(described_class.exit_on_failure?).to be true end end + + describe '#remove' do + context 'with relevant preview cards' do + before do + Fabricate(:preview_card, updated_at: 10.years.ago, type: :link) + Fabricate(:preview_card, updated_at: 10.months.ago, type: :photo) + Fabricate(:preview_card, updated_at: 10.days.ago, type: :photo) + end + + context 'with no arguments' do + it 'deletes thumbnails for local preview cards' do + expect { cli.invoke(:remove) }.to output( + a_string_including('Removed 2 preview cards') + .and(a_string_including('approx. 119 KB')) + ).to_stdout + end + end + + context 'with the --link option' do + let(:options) { { link: true } } + + it 'deletes thumbnails for local preview cards' do + expect { cli.invoke(:remove, [], options) }.to output( + a_string_including('Removed 1 link-type preview cards') + .and(a_string_including('approx. 59.6 KB')) + ).to_stdout + end + end + + context 'with the --days option' do + let(:options) { { days: 365 } } + + it 'deletes thumbnails for local preview cards' do + expect { cli.invoke(:remove, [], options) }.to output( + a_string_including('Removed 1 preview cards') + .and(a_string_including('approx. 59.6 KB')) + ).to_stdout + end + end + end + end end From 12bb7be8b558122e44a66a4337e7db999c11e0b3 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 17 Oct 2023 07:32:10 -0400 Subject: [PATCH 23/35] Spec speed ups on `AccountsController` spec (#25391) --- spec/controllers/accounts_controller_spec.rb | 638 ++++++++----------- 1 file changed, 254 insertions(+), 384 deletions(-) diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index d46fdb108d..cc9e3198b6 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -7,449 +7,319 @@ RSpec.describe AccountsController do let(:account) { Fabricate(:account) } - describe 'GET #show' do - let(:format) { 'html' } + shared_examples 'unapproved account check' do + before { account.user.update(approved: false) } - let!(:status) { Fabricate(:status, account: account) } - let!(:status_reply) { Fabricate(:status, account: account, thread: Fabricate(:status)) } - let!(:status_self_reply) { Fabricate(:status, account: account, thread: status) } - let!(:status_media) { Fabricate(:status, account: account) } - let!(:status_pinned) { Fabricate(:status, account: account) } - let!(:status_private) { Fabricate(:status, account: account, visibility: :private) } - let!(:status_direct) { Fabricate(:status, account: account, visibility: :direct) } - let!(:status_reblog) { Fabricate(:status, account: account, reblog: Fabricate(:status)) } + it 'returns http not found' do + get :show, params: { username: account.username, format: format } + expect(response).to have_http_status(404) + end + end + + shared_examples 'permanently suspended account check' do before do - status_media.media_attachments << Fabricate(:media_attachment, account: account, type: :image) - account.pinned_statuses << status_pinned - account.pinned_statuses << status_private + account.suspend! + account.deletion_request.destroy end - shared_examples 'preliminary checks' do - context 'when account is not approved' do - before do - account.user.update(approved: false) - end + it 'returns http gone' do + get :show, params: { username: account.username, format: format } - it 'returns http not found' do - get :show, params: { username: account.username, format: format } - expect(response).to have_http_status(404) - end + expect(response).to have_http_status(410) + end + end + + shared_examples 'temporarily suspended account check' do |code: 403| + before { account.suspend! } + + it 'returns appropriate http response code' do + get :show, params: { username: account.username, format: format } + + expect(response).to have_http_status(code) + end + end + + describe 'GET #show' do + context 'with basic account status checks' do + context 'with HTML' do + let(:format) { 'html' } + + it_behaves_like 'unapproved account check' + it_behaves_like 'permanently suspended account check' + it_behaves_like 'temporarily suspended account check' + end + + context 'with JSON' do + let(:format) { 'json' } + + it_behaves_like 'unapproved account check' + it_behaves_like 'permanently suspended account check' + it_behaves_like 'temporarily suspended account check', code: 200 + end + + context 'with RSS' do + let(:format) { 'rss' } + + it_behaves_like 'unapproved account check' + it_behaves_like 'permanently suspended account check' + it_behaves_like 'temporarily suspended account check' end end - context 'with HTML' do - let(:format) { 'html' } - - it_behaves_like 'preliminary checks' - - context 'when account is permanently suspended' do - before do - account.suspend! - account.deletion_request.destroy - end - - it 'returns http gone' do - get :show, params: { username: account.username, format: format } - expect(response).to have_http_status(410) - end - end - - context 'when account is temporarily suspended' do - before do - account.suspend! - end - - it 'returns http forbidden' do - get :show, params: { username: account.username, format: format } - expect(response).to have_http_status(403) - end - end - - shared_examples 'common response characteristics' do - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'returns Link header' do - expect(response.headers['Link'].to_s).to include ActivityPub::TagManager.instance.uri_for(account) - end - - it 'renders show template' do - expect(response).to render_template(:show) - end - end - - context 'with a normal account in an HTML request' do - before do - get :show, params: { username: account.username, format: format } - end - - it_behaves_like 'common response characteristics' - end - - context 'with replies' do - before do - allow(controller).to receive(:replies_requested?).and_return(true) - get :show, params: { username: account.username, format: format } - end - - it_behaves_like 'common response characteristics' - end - - context 'with media' do - before do - allow(controller).to receive(:media_requested?).and_return(true) - get :show, params: { username: account.username, format: format } - end - - it_behaves_like 'common response characteristics' - end - - context 'with tag' do - let(:tag) { Fabricate(:tag) } - - let!(:status_tag) { Fabricate(:status, account: account) } - - before do - allow(controller).to receive(:tag_requested?).and_return(true) - status_tag.tags << tag - get :show, params: { username: account.username, format: format, tag: tag.to_param } - end - - it_behaves_like 'common response characteristics' - end - end - - context 'with JSON' do - let(:authorized_fetch_mode) { false } - let(:format) { 'json' } + context 'with existing statuses' do + let!(:status) { Fabricate(:status, account: account) } + let!(:status_reply) { Fabricate(:status, account: account, thread: Fabricate(:status)) } + let!(:status_self_reply) { Fabricate(:status, account: account, thread: status) } + let!(:status_media) { Fabricate(:status, account: account) } + let!(:status_pinned) { Fabricate(:status, account: account) } + let!(:status_private) { Fabricate(:status, account: account, visibility: :private) } + let!(:status_direct) { Fabricate(:status, account: account, visibility: :direct) } + let!(:status_reblog) { Fabricate(:status, account: account, reblog: Fabricate(:status)) } before do - allow(controller).to receive(:authorized_fetch_mode?).and_return(authorized_fetch_mode) + status_media.media_attachments << Fabricate(:media_attachment, account: account, type: :image) + account.pinned_statuses << status_pinned + account.pinned_statuses << status_private end - it_behaves_like 'preliminary checks' + context 'with HTML' do + let(:format) { 'html' } - context 'when account is suspended permanently' do - before do - account.suspend! - account.deletion_request.destroy + shared_examples 'common HTML response' do + it 'returns a standard HTML response', :aggregate_failures do + expect(response).to have_http_status(200) + + expect(response.headers['Link'].to_s).to include ActivityPub::TagManager.instance.uri_for(account) + + expect(response).to render_template(:show) + end end - it 'returns http gone' do - get :show, params: { username: account.username, format: format } - expect(response).to have_http_status(410) + context 'with a normal account in an HTML request' do + before do + get :show, params: { username: account.username, format: format } + end + + it_behaves_like 'common HTML response' + end + + context 'with replies' do + before do + allow(controller).to receive(:replies_requested?).and_return(true) + get :show, params: { username: account.username, format: format } + end + + it_behaves_like 'common HTML response' + end + + context 'with media' do + before do + allow(controller).to receive(:media_requested?).and_return(true) + get :show, params: { username: account.username, format: format } + end + + it_behaves_like 'common HTML response' + end + + context 'with tag' do + let(:tag) { Fabricate(:tag) } + + let!(:status_tag) { Fabricate(:status, account: account) } + + before do + allow(controller).to receive(:tag_requested?).and_return(true) + status_tag.tags << tag + get :show, params: { username: account.username, format: format, tag: tag.to_param } + end + + it_behaves_like 'common HTML response' end end - context 'when account is suspended temporarily' do + context 'with JSON' do + let(:authorized_fetch_mode) { false } + let(:format) { 'json' } + before do - account.suspend! + allow(controller).to receive(:authorized_fetch_mode?).and_return(authorized_fetch_mode) end - it 'returns http success' do - get :show, params: { username: account.username, format: format } - expect(response).to have_http_status(200) - end - end + context 'with a normal account in a JSON request' do + before do + get :show, params: { username: account.username, format: format } + end - context 'with a normal account in a JSON request' do - before do - get :show, params: { username: account.username, format: format } + it 'returns a JSON version of the account', :aggregate_failures do + expect(response).to have_http_status(200) + + expect(response.media_type).to eq 'application/activity+json' + + expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) + end + + it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' + + context 'with authorized fetch mode' do + let(:authorized_fetch_mode) { true } + + it 'returns http unauthorized' do + expect(response).to have_http_status(401) + end + end end - it 'returns http success' do - expect(response).to have_http_status(200) + context 'when signed in' do + let(:user) { Fabricate(:user) } + + before do + sign_in(user) + get :show, params: { username: account.username, format: format } + end + + it 'returns a private JSON version of the account', :aggregate_failures do + expect(response).to have_http_status(200) + + expect(response.media_type).to eq 'application/activity+json' + + expect(response.headers['Cache-Control']).to include 'private' + + expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) + end end - it 'returns application/activity+json' do - expect(response.media_type).to eq 'application/activity+json' - end + context 'with signature' do + let(:remote_account) { Fabricate(:account, domain: 'example.com') } - it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' + before do + allow(controller).to receive(:signed_request_actor).and_return(remote_account) + get :show, params: { username: account.username, format: format } + end - it 'renders account' do - json = body_as_json - expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) - end + it 'returns a JSON version of the account', :aggregate_failures do + expect(response).to have_http_status(200) - context 'with authorized fetch mode' do - let(:authorized_fetch_mode) { true } + expect(response.media_type).to eq 'application/activity+json' - it 'returns http unauthorized' do - expect(response).to have_http_status(401) + expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) + end + + it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' + + context 'with authorized fetch mode' do + let(:authorized_fetch_mode) { true } + + it 'returns a private signature JSON version of the account', :aggregate_failures do + expect(response).to have_http_status(200) + + expect(response.media_type).to eq 'application/activity+json' + + expect(response.headers['Cache-Control']).to include 'private' + + expect(response.headers['Vary']).to include 'Signature' + + expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) + end end end end - context 'when signed in' do - let(:user) { Fabricate(:user) } - - before do - sign_in(user) - get :show, params: { username: account.username, format: format } - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'returns application/activity+json' do - expect(response.media_type).to eq 'application/activity+json' - end - - it 'returns private Cache-Control header' do - expect(response.headers['Cache-Control']).to include 'private' - end - - it 'renders account' do - json = body_as_json - expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) - end - end - - context 'with signature' do - let(:remote_account) { Fabricate(:account, domain: 'example.com') } - - before do - allow(controller).to receive(:signed_request_actor).and_return(remote_account) - get :show, params: { username: account.username, format: format } - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'returns application/activity+json' do - expect(response.media_type).to eq 'application/activity+json' - end - - it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' - - it 'renders account' do - json = body_as_json - expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) - end - - context 'with authorized fetch mode' do - let(:authorized_fetch_mode) { true } + context 'with RSS' do + let(:format) { 'rss' } + shared_examples 'common RSS response' do it 'returns http success' do expect(response).to have_http_status(200) end - it 'returns application/activity+json' do - expect(response.media_type).to eq 'application/activity+json' + it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' + end + + context 'with a normal account in an RSS request' do + before do + get :show, params: { username: account.username, format: format } end - it 'returns private Cache-Control header' do - expect(response.headers['Cache-Control']).to include 'private' - end + it_behaves_like 'common RSS response' - it 'returns Vary header with Signature' do - expect(response.headers['Vary']).to include 'Signature' - end - - it 'renders account' do - json = body_as_json - expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) + it 'responds with correct statuses', :aggregate_failures do + expect(response.body).to include_status_tag(status_media) + expect(response.body).to include_status_tag(status_self_reply) + expect(response.body).to include_status_tag(status) + expect(response.body).to_not include_status_tag(status_direct) + expect(response.body).to_not include_status_tag(status_private) + expect(response.body).to_not include_status_tag(status_reblog.reblog) + expect(response.body).to_not include_status_tag(status_reply) end end - end - end - context 'with RSS' do - let(:format) { 'rss' } + context 'with replies' do + before do + allow(controller).to receive(:replies_requested?).and_return(true) + get :show, params: { username: account.username, format: format } + end - it_behaves_like 'preliminary checks' + it_behaves_like 'common RSS response' - context 'when account is permanently suspended' do - before do - account.suspend! - account.deletion_request.destroy + it 'responds with correct statuses with replies', :aggregate_failures do + expect(response.body).to include_status_tag(status_media) + expect(response.body).to include_status_tag(status_reply) + expect(response.body).to include_status_tag(status_self_reply) + expect(response.body).to include_status_tag(status) + expect(response.body).to_not include_status_tag(status_direct) + expect(response.body).to_not include_status_tag(status_private) + expect(response.body).to_not include_status_tag(status_reblog.reblog) + end end - it 'returns http gone' do - get :show, params: { username: account.username, format: format } - expect(response).to have_http_status(410) - end - end + context 'with media' do + before do + allow(controller).to receive(:media_requested?).and_return(true) + get :show, params: { username: account.username, format: format } + end - context 'when account is temporarily suspended' do - before do - account.suspend! + it_behaves_like 'common RSS response' + + it 'responds with correct statuses with media', :aggregate_failures do + expect(response.body).to include_status_tag(status_media) + expect(response.body).to_not include_status_tag(status_direct) + expect(response.body).to_not include_status_tag(status_private) + expect(response.body).to_not include_status_tag(status_reblog.reblog) + expect(response.body).to_not include_status_tag(status_reply) + expect(response.body).to_not include_status_tag(status_self_reply) + expect(response.body).to_not include_status_tag(status) + end end - it 'returns http forbidden' do - get :show, params: { username: account.username, format: format } - expect(response).to have_http_status(403) - end - end + context 'with tag' do + let(:tag) { Fabricate(:tag) } - shared_examples 'common response characteristics' do - it 'returns http success' do - expect(response).to have_http_status(200) - end + let!(:status_tag) { Fabricate(:status, account: account) } - it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' - end + before do + allow(controller).to receive(:tag_requested?).and_return(true) + status_tag.tags << tag + get :show, params: { username: account.username, format: format, tag: tag.to_param } + end - context 'with a normal account in an RSS request' do - before do - get :show, params: { username: account.username, format: format } - end + it_behaves_like 'common RSS response' - it_behaves_like 'common response characteristics' - - it 'renders public status' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status)) - end - - it 'renders self-reply' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply)) - end - - it 'renders status with media' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media)) - end - - it 'does not render reblog' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) - end - - it 'does not render private status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) - end - - it 'does not render direct status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) - end - - it 'does not render reply to someone else' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) - end - end - - context 'with replies' do - before do - allow(controller).to receive(:replies_requested?).and_return(true) - get :show, params: { username: account.username, format: format } - end - - it_behaves_like 'common response characteristics' - - it 'renders public status' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status)) - end - - it 'renders self-reply' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply)) - end - - it 'renders status with media' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media)) - end - - it 'does not render reblog' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) - end - - it 'does not render private status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) - end - - it 'does not render direct status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) - end - - it 'renders reply to someone else' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reply)) - end - end - - context 'with media' do - before do - allow(controller).to receive(:media_requested?).and_return(true) - get :show, params: { username: account.username, format: format } - end - - it_behaves_like 'common response characteristics' - - it 'does not render public status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status)) - end - - it 'does not render self-reply' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply)) - end - - it 'renders status with media' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media)) - end - - it 'does not render reblog' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) - end - - it 'does not render private status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) - end - - it 'does not render direct status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) - end - - it 'does not render reply to someone else' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) - end - end - - context 'with tag' do - let(:tag) { Fabricate(:tag) } - - let!(:status_tag) { Fabricate(:status, account: account) } - - before do - allow(controller).to receive(:tag_requested?).and_return(true) - status_tag.tags << tag - get :show, params: { username: account.username, format: format, tag: tag.to_param } - end - - it_behaves_like 'common response characteristics' - - it 'does not render public status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status)) - end - - it 'does not render self-reply' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply)) - end - - it 'does not render status with media' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_media)) - end - - it 'does not render reblog' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) - end - - it 'does not render private status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) - end - - it 'does not render direct status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) - end - - it 'does not render reply to someone else' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) - end - - it 'renders status with tag' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_tag)) + it 'responds with correct statuses with a tag', :aggregate_failures do + expect(response.body).to include_status_tag(status_tag) + expect(response.body).to_not include_status_tag(status_direct) + expect(response.body).to_not include_status_tag(status_media) + expect(response.body).to_not include_status_tag(status_private) + expect(response.body).to_not include_status_tag(status_reblog.reblog) + expect(response.body).to_not include_status_tag(status_reply) + expect(response.body).to_not include_status_tag(status_self_reply) + expect(response.body).to_not include_status_tag(status) + end end end end end + + def include_status_tag(status) + include ActivityPub::TagManager.instance.url_for(status) + end end From c4bddc9855886b4ba60316fbcbc04375ee720da7 Mon Sep 17 00:00:00 2001 From: Victor Lee <43049052+leevic31@users.noreply.github.com> Date: Tue, 17 Oct 2023 08:56:24 -0400 Subject: [PATCH 24/35] Add spec for poll model (#23399) Co-authored-by: Nick Schonning Co-authored-by: Claire --- spec/models/poll_spec.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/spec/models/poll_spec.rb b/spec/models/poll_spec.rb index 8ae04ca41f..5aa5548cc8 100644 --- a/spec/models/poll_spec.rb +++ b/spec/models/poll_spec.rb @@ -29,4 +29,23 @@ describe Poll do end end end + + describe 'validations' do + context 'when valid' do + let(:poll) { Fabricate.build(:poll) } + + it 'is valid with valid attributes' do + expect(poll).to be_valid + end + end + + context 'when not valid' do + let(:poll) { Fabricate.build(:poll, expires_at: nil) } + + it 'is invalid without an expire date' do + poll.valid? + expect(poll).to model_have_error_on_field(:expires_at) + end + end + end end From 935d54124e80e9fe5365c724e5c8827a2b3ed5b3 Mon Sep 17 00:00:00 2001 From: Stanislas Signoud Date: Tue, 17 Oct 2023 14:59:07 +0200 Subject: [PATCH 25/35] Fix missing redirections to make sure /home redirect to the advanced UI (#27378) --- app/javascript/mastodon/features/ui/index.jsx | 2 ++ app/javascript/mastodon/initial_state.js | 1 + 2 files changed, 3 insertions(+) diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index ac5e2d9361..2f39be24a0 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -184,7 +184,9 @@ class SwitchingColumnsArea extends PureComponent { {singleColumn ? : null} {singleColumn && pathName.startsWith('/deck/') ? : null} + {/* Redirect old bookmarks (without /deck) with home-like routes to the advanced interface */} {!singleColumn && pathName === '/getting-started' ? : null} + {!singleColumn && pathName === '/home' ? : null} diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 11cd2a1673..bf5ce556e8 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -100,6 +100,7 @@ const initialPath = document.querySelector("head meta[name=initialPath]")?.getAt /** @type {boolean} */ export const hasMultiColumnPath = initialPath === '/' || initialPath === '/getting-started' + || initialPath === '/home' || initialPath.startsWith('/deck'); /** From 1fb4e40145f49bd335d22c7f560e770647fe77f2 Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Tue, 17 Oct 2023 14:59:39 +0200 Subject: [PATCH 26/35] Run scheduled actions only on the main repo (#27343) --- .github/workflows/build-nightly.yml | 1 + .github/workflows/crowdin-download.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/build-nightly.yml b/.github/workflows/build-nightly.yml index aa1f916afe..1790d5c84f 100644 --- a/.github/workflows/build-nightly.yml +++ b/.github/workflows/build-nightly.yml @@ -11,6 +11,7 @@ permissions: jobs: compute-suffix: runs-on: ubuntu-latest + if: github.repository == 'mastodon/mastodon' steps: - id: version_vars env: diff --git a/.github/workflows/crowdin-download.yml b/.github/workflows/crowdin-download.yml index e5385808de..8268788be4 100644 --- a/.github/workflows/crowdin-download.yml +++ b/.github/workflows/crowdin-download.yml @@ -11,6 +11,7 @@ permissions: jobs: download-translations: runs-on: ubuntu-latest + if: github.repository == 'mastodon/mastodon' steps: - name: Checkout From c0cda1adaf8a3b14d45be2844ff1688cf0f7cd18 Mon Sep 17 00:00:00 2001 From: Jeong Arm Date: Tue, 17 Oct 2023 22:24:45 +0900 Subject: [PATCH 27/35] Let use http_hidden_proxy on docker-compose (#27054) --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index d19f278f75..bcfa4c85f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -111,7 +111,7 @@ services: test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 6' || false"] ## Uncomment to enable federation with tor instances along with adding the following ENV variables - ## http_proxy=http://privoxy:8118 + ## http_hidden_proxy=http://privoxy:8118 ## ALLOW_ACCESS_TO_HIDDEN_SERVICE=true # tor: # image: sirboops/tor From b34a2b1b334f94cb5491e2472f92f22f4b8a7683 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 17 Oct 2023 15:30:12 +0200 Subject: [PATCH 28/35] Fix errors in CLI specs (#27399) --- spec/lib/mastodon/cli/accounts_spec.rb | 87 ++++++++++++-------------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/spec/lib/mastodon/cli/accounts_spec.rb b/spec/lib/mastodon/cli/accounts_spec.rb index a263d673de..5ecea5ea16 100644 --- a/spec/lib/mastodon/cli/accounts_spec.rb +++ b/spec/lib/mastodon/cli/accounts_spec.rb @@ -6,6 +6,24 @@ require 'mastodon/cli/accounts' describe Mastodon::CLI::Accounts do let(:cli) { described_class.new } + # `parallelize_with_progress` cannot run in transactions, so instead, + # stub it with an alternative implementation that runs sequentially + # and can run in transactions. + def stub_parallelize_with_progress! + allow(cli).to receive(:parallelize_with_progress) do |scope, &block| + aggregate = 0 + total = 0 + + scope.reorder(nil).find_each do |record| + value = block.call(record) + aggregate += value if value.is_a?(Integer) + total += 1 + end + + [total, aggregate] + end + end + describe '.exit_on_failure?' do it 'returns true' do expect(described_class.exit_on_failure?).to be true @@ -551,20 +569,15 @@ describe Mastodon::CLI::Accounts do let!(:follower_rony) { Fabricate(:account, username: 'rony') } let!(:follower_charles) { Fabricate(:account, username: 'charles') } let(:follow_service) { instance_double(FollowService, call: nil) } - let(:scope) { Account.local.without_suspended } before do - allow(cli).to receive(:parallelize_with_progress).and_yield(follower_bob) - .and_yield(follower_rony) - .and_yield(follower_charles) - .and_return([3, nil]) allow(FollowService).to receive(:new).and_return(follow_service) + stub_parallelize_with_progress! end it 'makes all local accounts follow the target account' do cli.follow(target_account.username) - expect(cli).to have_received(:parallelize_with_progress).with(scope).once expect(follow_service).to have_received(:call).with(follower_bob, target_account, any_args).once expect(follow_service).to have_received(:call).with(follower_rony, target_account, any_args).once expect(follow_service).to have_received(:call).with(follower_charles, target_account, any_args).once @@ -572,7 +585,7 @@ describe Mastodon::CLI::Accounts do it 'displays a successful message' do expect { cli.follow(target_account.username) }.to output( - a_string_including('OK, followed target from 3 accounts') + a_string_including("OK, followed target from #{Account.local.count} accounts") ).to_stdout end end @@ -592,26 +605,21 @@ describe Mastodon::CLI::Accounts do context 'when the given username is found' do let!(:target_account) { Fabricate(:account) } - let!(:follower_chris) { Fabricate(:account, username: 'chris') } - let!(:follower_rambo) { Fabricate(:account, username: 'rambo') } - let!(:follower_ana) { Fabricate(:account, username: 'ana') } + let!(:follower_chris) { Fabricate(:account, username: 'chris', domain: nil) } + let!(:follower_rambo) { Fabricate(:account, username: 'rambo', domain: nil) } + let!(:follower_ana) { Fabricate(:account, username: 'ana', domain: nil) } let(:unfollow_service) { instance_double(UnfollowService, call: nil) } - let(:scope) { target_account.followers.local } before do accounts = [follower_chris, follower_rambo, follower_ana] - accounts.each { |account| target_account.follow!(account) } - allow(cli).to receive(:parallelize_with_progress).and_yield(follower_chris) - .and_yield(follower_rambo) - .and_yield(follower_ana) - .and_return([3, nil]) + accounts.each { |account| account.follow!(target_account) } allow(UnfollowService).to receive(:new).and_return(unfollow_service) + stub_parallelize_with_progress! end it 'makes all local accounts unfollow the target account' do cli.unfollow(target_account.username) - expect(cli).to have_received(:parallelize_with_progress).with(scope).once expect(unfollow_service).to have_received(:call).with(follower_chris, target_account).once expect(unfollow_service).to have_received(:call).with(follower_rambo, target_account).once expect(unfollow_service).to have_received(:call).with(follower_ana, target_account).once @@ -671,6 +679,8 @@ describe Mastodon::CLI::Accounts do let(:scope) { Account.remote } before do + # TODO: we should be using `stub_parallelize_with_progress!` but + # this makes the assertions harder to write allow(cli).to receive(:parallelize_with_progress).and_yield(remote_account_example_com) .and_yield(account_example_net) .and_return([2, nil]) @@ -1112,26 +1122,19 @@ describe Mastodon::CLI::Accounts do describe '#cull' do let(:delete_account_service) { instance_double(DeleteAccountService, call: nil) } - let!(:tom) { Fabricate(:account, updated_at: 30.days.ago, username: 'tom', uri: 'https://example.com/users/tom', domain: 'example.com') } - let!(:bob) { Fabricate(:account, updated_at: 30.days.ago, last_webfingered_at: nil, username: 'bob', uri: 'https://example.org/users/bob', domain: 'example.org') } - let!(:gon) { Fabricate(:account, updated_at: 15.days.ago, last_webfingered_at: 15.days.ago, username: 'gon', uri: 'https://example.net/users/gon', domain: 'example.net') } - let!(:ana) { Fabricate(:account, username: 'ana', uri: 'https://example.com/users/ana', domain: 'example.com') } - let!(:tales) { Fabricate(:account, updated_at: 10.days.ago, last_webfingered_at: nil, username: 'tales', uri: 'https://example.net/users/tales', domain: 'example.net') } + let!(:tom) { Fabricate(:account, updated_at: 30.days.ago, username: 'tom', uri: 'https://example.com/users/tom', domain: 'example.com', protocol: :activitypub) } + let!(:bob) { Fabricate(:account, updated_at: 30.days.ago, last_webfingered_at: nil, username: 'bob', uri: 'https://example.org/users/bob', domain: 'example.org', protocol: :activitypub) } + let!(:gon) { Fabricate(:account, updated_at: 15.days.ago, last_webfingered_at: 15.days.ago, username: 'gon', uri: 'https://example.net/users/gon', domain: 'example.net', protocol: :activitypub) } + let!(:ana) { Fabricate(:account, username: 'ana', uri: 'https://example.com/users/ana', domain: 'example.com', protocol: :activitypub) } + let!(:tales) { Fabricate(:account, updated_at: 10.days.ago, last_webfingered_at: nil, username: 'tales', uri: 'https://example.net/users/tales', domain: 'example.net', protocol: :activitypub) } before do allow(DeleteAccountService).to receive(:new).and_return(delete_account_service) end context 'when no domain is specified' do - let(:scope) { Account.remote.where(protocol: :activitypub).partitioned } - before do - allow(cli).to receive(:parallelize_with_progress).and_yield(tom) - .and_yield(bob) - .and_yield(gon) - .and_yield(ana) - .and_yield(tales) - .and_return([5, 3]) + stub_parallelize_with_progress! stub_request(:head, 'https://example.org/users/bob').to_return(status: 404) stub_request(:head, 'https://example.net/users/gon').to_return(status: 410) stub_request(:head, 'https://example.net/users/tales').to_return(status: 200) @@ -1140,7 +1143,6 @@ describe Mastodon::CLI::Accounts do it 'deletes all inactive remote accounts that longer exist in the origin server' do cli.cull - expect(cli).to have_received(:parallelize_with_progress).with(scope).once expect(delete_account_service).to have_received(:call).with(bob, reserve_username: false).once expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once end @@ -1148,35 +1150,27 @@ describe Mastodon::CLI::Accounts do it 'does not delete any active remote account that still exists in the origin server' do cli.cull - expect(cli).to have_received(:parallelize_with_progress).with(scope).once expect(delete_account_service).to_not have_received(:call).with(tom, reserve_username: false) expect(delete_account_service).to_not have_received(:call).with(ana, reserve_username: false) expect(delete_account_service).to_not have_received(:call).with(tales, reserve_username: false) end it 'touches inactive remote accounts that have not been deleted' do - allow(tales).to receive(:touch) - - cli.cull - - expect(tales).to have_received(:touch).once + expect { cli.cull }.to(change { tales.reload.updated_at }) end it 'displays the summary correctly' do expect { cli.cull }.to output( - a_string_including('Visited 5 accounts, removed 3') + a_string_including('Visited 5 accounts, removed 2') ).to_stdout end end context 'when a domain is specified' do let(:domain) { 'example.net' } - let(:scope) { Account.remote.where(protocol: :activitypub, domain: domain).partitioned } before do - allow(cli).to receive(:parallelize_with_progress).and_yield(gon) - .and_yield(tales) - .and_return([2, 2]) + stub_parallelize_with_progress! stub_request(:head, 'https://example.net/users/gon').to_return(status: 410) stub_request(:head, 'https://example.net/users/tales').to_return(status: 404) end @@ -1184,13 +1178,12 @@ describe Mastodon::CLI::Accounts do it 'deletes inactive remote accounts that longer exist in the specified domain' do cli.cull(domain) - expect(cli).to have_received(:parallelize_with_progress).with(scope).once expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once expect(delete_account_service).to have_received(:call).with(tales, reserve_username: false).once end it 'displays the summary correctly' do - expect { cli.cull }.to output( + expect { cli.cull(domain) }.to output( a_string_including('Visited 2 accounts, removed 2') ).to_stdout end @@ -1199,7 +1192,9 @@ describe Mastodon::CLI::Accounts do context 'when a domain is unavailable' do shared_examples 'an unavailable domain' do before do - allow(cli).to receive(:parallelize_with_progress).and_yield(tales).and_return([1, 0]) + stub_parallelize_with_progress! + stub_request(:head, 'https://example.org/users/bob').to_return(status: 200) + stub_request(:head, 'https://example.net/users/gon').to_return(status: 200) end it 'skips accounts from the unavailable domain' do @@ -1210,7 +1205,7 @@ describe Mastodon::CLI::Accounts do it 'displays the summary correctly' do expect { cli.cull }.to output( - a_string_including("Visited 1 accounts, removed 0\nThe following domains were not available during the check:\n example.net") + a_string_including("Visited 5 accounts, removed 0\nThe following domains were not available during the check:\n example.net") ).to_stdout end end From 65cd0700e4cbc05372140d4fe33bfc6e2412a161 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 17 Oct 2023 09:58:42 -0400 Subject: [PATCH 29/35] Refactor domains cli to remove model call from regex (#25878) --- config/brakeman.ignore | 24 ------------------------ lib/mastodon/cli/domains.rb | 6 +++++- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/config/brakeman.ignore b/config/brakeman.ignore index 02ce23a075..9f85ccb6a4 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -33,30 +33,6 @@ ], "note": "" }, - { - "warning_type": "Denial of Service", - "warning_code": 76, - "fingerprint": "7b6abba5699755348e7ee82a4694bfbf574b41c7cce2d0db0f7c11ae3f983c72", - "check_name": "RegexDoS", - "message": "Model attribute used in regular expression", - "file": "lib/mastodon/cli/domains.rb", - "line": 128, - "link": "https://brakemanscanner.org/docs/warning_types/denial_of_service/", - "code": "/\\.?(#{DomainBlock.where(:severity => 1).pluck(:domain).map do\n Regexp.escape(domain)\n end.join(\"|\")})$/", - "render_path": null, - "location": { - "type": "method", - "class": "Mastodon::CLI::Domains", - "method": "crawl" - }, - "user_input": "DomainBlock.where(:severity => 1).pluck(:domain)", - "confidence": "Weak", - "cwe_id": [ - 20, - 185 - ], - "note": "" - }, { "warning_type": "Cross-Site Scripting", "warning_code": 4, diff --git a/lib/mastodon/cli/domains.rb b/lib/mastodon/cli/domains.rb index d17b253681..329f171672 100644 --- a/lib/mastodon/cli/domains.rb +++ b/lib/mastodon/cli/domains.rb @@ -125,7 +125,7 @@ module Mastodon::CLI failed = Concurrent::AtomicFixnum.new(0) start_at = Time.now.to_f seed = start ? [start] : Instance.pluck(:domain) - blocked_domains = /\.?(#{DomainBlock.where(severity: 1).pluck(:domain).map { |domain| Regexp.escape(domain) }.join('|')})$/ + blocked_domains = /\.?(#{Regexp.union(domain_block_suspended_domains).source})$/ progress = create_progress_bar pool = Concurrent::ThreadPoolExecutor.new(min_threads: 0, max_threads: options[:concurrency], idletime: 10, auto_terminate: true, max_queue: 0) @@ -189,6 +189,10 @@ module Mastodon::CLI private + def domain_block_suspended_domains + DomainBlock.suspend.pluck(:domain) + end + def stats_to_summary(stats, processed, failed, start_at) stats.compact! From 1996fb6fd6e5e44884c429d9c651825e0e72302f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 16:10:16 +0200 Subject: [PATCH 30/35] Update dependency puma to v6.4.0 (#27006) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index ce21bc0a20..4955e8283c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -513,7 +513,7 @@ GEM premailer (~> 1.7, >= 1.7.9) private_address_check (0.5.0) public_suffix (5.0.3) - puma (6.3.1) + puma (6.4.0) nio4r (~> 2.0) pundit (2.3.0) activesupport (>= 3.0.0) From 2528a0721f536f747fec9d60d7a3643bf33e7cdc Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 17 Oct 2023 10:10:50 -0400 Subject: [PATCH 31/35] Update `rubocop-capybara` to version 2.19.0 (#26982) --- Gemfile.lock | 2 +- spec/features/admin/domain_blocks_spec.rb | 18 +++++----- spec/features/admin/software_updates_spec.rb | 4 +-- spec/features/captcha_spec.rb | 2 +- spec/features/log_in_spec.rb | 6 ++-- spec/features/oauth_spec.rb | 36 ++++++++++---------- spec/support/stories/profile_stories.rb | 2 +- spec/system/new_statuses_spec.rb | 8 ++--- 8 files changed, 39 insertions(+), 39 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4955e8283c..ba44065d02 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -634,7 +634,7 @@ GEM unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.29.0) parser (>= 3.2.1.0) - rubocop-capybara (2.18.0) + rubocop-capybara (2.19.0) rubocop (~> 1.41) rubocop-factory_bot (2.23.1) rubocop (~> 1.33) diff --git a/spec/features/admin/domain_blocks_spec.rb b/spec/features/admin/domain_blocks_spec.rb index 4672c1e1a9..0d7b90c21c 100644 --- a/spec/features/admin/domain_blocks_spec.rb +++ b/spec/features/admin/domain_blocks_spec.rb @@ -13,7 +13,7 @@ describe 'blocking domains through the moderation interface' do fill_in 'domain_block_domain', with: 'example.com' select I18n.t('admin.domain_blocks.new.severity.silence'), from: 'domain_block_severity' - click_on I18n.t('admin.domain_blocks.new.create') + click_button I18n.t('admin.domain_blocks.new.create') expect(DomainBlock.exists?(domain: 'example.com', severity: 'silence')).to be true end @@ -25,13 +25,13 @@ describe 'blocking domains through the moderation interface' do fill_in 'domain_block_domain', with: 'example.com' select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' - click_on I18n.t('admin.domain_blocks.new.create') + click_button I18n.t('admin.domain_blocks.new.create') # It presents a confirmation screen expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) # Confirming creates a block - click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') + click_button I18n.t('admin.domain_blocks.confirm_suspension.confirm') expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be true end @@ -45,13 +45,13 @@ describe 'blocking domains through the moderation interface' do fill_in 'domain_block_domain', with: 'example.com' select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' - click_on I18n.t('admin.domain_blocks.new.create') + click_button I18n.t('admin.domain_blocks.new.create') # It presents a confirmation screen expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) # Confirming updates the block - click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') + click_button I18n.t('admin.domain_blocks.confirm_suspension.confirm') expect(domain_block.reload.severity).to eq 'suspend' end @@ -65,13 +65,13 @@ describe 'blocking domains through the moderation interface' do fill_in 'domain_block_domain', with: 'subdomain.example.com' select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' - click_on I18n.t('admin.domain_blocks.new.create') + click_button I18n.t('admin.domain_blocks.new.create') # It presents a confirmation screen expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'subdomain.example.com')) # Confirming creates the block - click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') + click_button I18n.t('admin.domain_blocks.confirm_suspension.confirm') expect(DomainBlock.where(domain: 'subdomain.example.com', severity: 'suspend')).to exist @@ -88,13 +88,13 @@ describe 'blocking domains through the moderation interface' do visit edit_admin_domain_block_path(domain_block) select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' - click_on I18n.t('generic.save_changes') + click_button I18n.t('generic.save_changes') # It presents a confirmation screen expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) # Confirming updates the block - click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') + click_button I18n.t('admin.domain_blocks.confirm_suspension.confirm') expect(domain_block.reload.severity).to eq 'suspend' end diff --git a/spec/features/admin/software_updates_spec.rb b/spec/features/admin/software_updates_spec.rb index 4a635d1a79..a2373d35a6 100644 --- a/spec/features/admin/software_updates_spec.rb +++ b/spec/features/admin/software_updates_spec.rb @@ -11,13 +11,13 @@ describe 'finding software updates through the admin interface' do it 'shows a link to the software updates page, which links to release notes' do visit settings_profile_path - click_on I18n.t('admin.critical_update_pending') + click_link I18n.t('admin.critical_update_pending') expect(page).to have_title(I18n.t('admin.software_updates.title')) expect(page).to have_content('99.99.99') - click_on I18n.t('admin.software_updates.release_notes') + click_link I18n.t('admin.software_updates.release_notes') expect(page).to have_current_path('https://github.com/mastodon/mastodon/releases/v99', url: true) end end diff --git a/spec/features/captcha_spec.rb b/spec/features/captcha_spec.rb index db89ff3e61..6ccf066208 100644 --- a/spec/features/captcha_spec.rb +++ b/spec/features/captcha_spec.rb @@ -27,7 +27,7 @@ describe 'email confirmation flow when captcha is enabled' do expect(user.reload.confirmed?).to be false # It redirects to app and confirms user - click_on I18n.t('challenge.confirm') + click_button I18n.t('challenge.confirm') expect(user.reload.confirmed?).to be true expect(page).to have_current_path(/\A#{client_app.confirmation_redirect_uri}/, url: true) end diff --git a/spec/features/log_in_spec.rb b/spec/features/log_in_spec.rb index c64e19d2b7..7e5196aba9 100644 --- a/spec/features/log_in_spec.rb +++ b/spec/features/log_in_spec.rb @@ -19,7 +19,7 @@ describe 'Log in' do it 'A valid email and password user is able to log in' do fill_in 'user_email', with: email fill_in 'user_password', with: password - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(subject).to have_css('div.app-holder') end @@ -27,7 +27,7 @@ describe 'Log in' do it 'A invalid email and password user is not able to log in' do fill_in 'user_email', with: 'invalid_email' fill_in 'user_password', with: 'invalid_password' - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(subject).to have_css('.flash-message', text: failure_message('invalid')) end @@ -38,7 +38,7 @@ describe 'Log in' do it 'A unconfirmed user is able to log in' do fill_in 'user_email', with: email fill_in 'user_password', with: password - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(subject).to have_css('div.admin-wrapper') end diff --git a/spec/features/oauth_spec.rb b/spec/features/oauth_spec.rb index 967956cc8e..0e612b56a5 100644 --- a/spec/features/oauth_spec.rb +++ b/spec/features/oauth_spec.rb @@ -20,7 +20,7 @@ describe 'Using OAuth from an external app' do expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) # Upon authorizing, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.authorize') + click_button I18n.t('doorkeeper.authorizations.buttons.authorize') expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) # It grants the app access to the account @@ -35,7 +35,7 @@ describe 'Using OAuth from an external app' do expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.deny')) # Upon denying, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.deny') + click_button I18n.t('doorkeeper.authorizations.buttons.deny') expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) # It does not grant the app access to the account @@ -63,17 +63,17 @@ describe 'Using OAuth from an external app' do # Failing to log-in presents the form again fill_in 'user_email', with: email fill_in 'user_password', with: 'wrong password' - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('auth.login')) # Logging in redirects to an authorization page fill_in 'user_email', with: email fill_in 'user_password', with: password - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) # Upon authorizing, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.authorize') + click_button I18n.t('doorkeeper.authorizations.buttons.authorize') expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) # It grants the app access to the account @@ -90,17 +90,17 @@ describe 'Using OAuth from an external app' do # Failing to log-in presents the form again fill_in 'user_email', with: email fill_in 'user_password', with: 'wrong password' - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('auth.login')) # Logging in redirects to an authorization page fill_in 'user_email', with: email fill_in 'user_password', with: password - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) # Upon denying, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.deny') + click_button I18n.t('doorkeeper.authorizations.buttons.deny') expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) # It does not grant the app access to the account @@ -120,27 +120,27 @@ describe 'Using OAuth from an external app' do # Failing to log-in presents the form again fill_in 'user_email', with: email fill_in 'user_password', with: 'wrong password' - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('auth.login')) # Logging in redirects to a two-factor authentication page fill_in 'user_email', with: email fill_in 'user_password', with: password - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp')) # Filling in an incorrect two-factor authentication code presents the form again fill_in 'user_otp_attempt', with: 'wrong' - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp')) # Filling in the correct TOTP code redirects to an app authorization page fill_in 'user_otp_attempt', with: user.current_otp - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) # Upon authorizing, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.authorize') + click_button I18n.t('doorkeeper.authorizations.buttons.authorize') expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) # It grants the app access to the account @@ -157,27 +157,27 @@ describe 'Using OAuth from an external app' do # Failing to log-in presents the form again fill_in 'user_email', with: email fill_in 'user_password', with: 'wrong password' - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('auth.login')) # Logging in redirects to a two-factor authentication page fill_in 'user_email', with: email fill_in 'user_password', with: password - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp')) # Filling in an incorrect two-factor authentication code presents the form again fill_in 'user_otp_attempt', with: 'wrong' - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp')) # Filling in the correct TOTP code redirects to an app authorization page fill_in 'user_otp_attempt', with: user.current_otp - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) # Upon denying, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.deny') + click_button I18n.t('doorkeeper.authorizations.buttons.deny') expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) # It does not grant the app access to the account diff --git a/spec/support/stories/profile_stories.rb b/spec/support/stories/profile_stories.rb index 2b345ddef1..82667ca080 100644 --- a/spec/support/stories/profile_stories.rb +++ b/spec/support/stories/profile_stories.rb @@ -18,7 +18,7 @@ module ProfileStories visit new_user_session_path fill_in 'user_email', with: email fill_in 'user_password', with: password - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') end def with_alice_as_local_user diff --git a/spec/system/new_statuses_spec.rb b/spec/system/new_statuses_spec.rb index 6faed6c808..244101f4d4 100644 --- a/spec/system/new_statuses_spec.rb +++ b/spec/system/new_statuses_spec.rb @@ -24,10 +24,10 @@ describe 'NewStatuses' do within('.compose-form') do fill_in "What's on your mind?", with: status_text - click_on 'Publish!' + click_button 'Publish!' end - expect(subject).to have_selector('.status__content__text', text: status_text) + expect(subject).to have_css('.status__content__text', text: status_text) end it 'can be posted again' do @@ -37,9 +37,9 @@ describe 'NewStatuses' do within('.compose-form') do fill_in "What's on your mind?", with: status_text - click_on 'Publish!' + click_button 'Publish!' end - expect(subject).to have_selector('.status__content__text', text: status_text) + expect(subject).to have_css('.status__content__text', text: status_text) end end From ad44d832b8eaf42526b7bb26c94b39bfa6795457 Mon Sep 17 00:00:00 2001 From: Jeong Arm Date: Tue, 17 Oct 2023 23:12:03 +0900 Subject: [PATCH 32/35] Support "in:public" search filter (#26976) --- app/javascript/mastodon/features/compose/components/search.jsx | 2 +- app/lib/search_query_transformer.rb | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/compose/components/search.jsx b/app/javascript/mastodon/features/compose/components/search.jsx index dfbfa98f5e..2525df7793 100644 --- a/app/javascript/mastodon/features/compose/components/search.jsx +++ b/app/javascript/mastodon/features/compose/components/search.jsx @@ -64,7 +64,7 @@ class Search extends PureComponent { { label: <>before: , action: e => { e.preventDefault(); this._insertText('before:'); } }, { label: <>during: , action: e => { e.preventDefault(); this._insertText('during:'); } }, { label: <>after: , action: e => { e.preventDefault(); this._insertText('after:'); } }, - { label: <>in: , action: e => { e.preventDefault(); this._insertText('in:'); } } + { label: <>in: , action: e => { e.preventDefault(); this._insertText('in:'); } } ]; setRef = c => { diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb index a45ae3d09b..927495eace 100644 --- a/app/lib/search_query_transformer.rb +++ b/app/lib/search_query_transformer.rb @@ -58,6 +58,8 @@ class SearchQueryTransformer < Parslet::Transform case @flags['in'] when 'library' [StatusesIndex] + when 'public' + [PublicStatusesIndex] else [PublicStatusesIndex, StatusesIndex] end From 0036de46bc2d00f7ed549e69b0b8f72165e4b2a7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 16:16:12 +0200 Subject: [PATCH 33/35] Update dependency rubocop-performance to v1.19.1 (#26949) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index ba44065d02..c3d1ca2099 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -638,7 +638,7 @@ GEM rubocop (~> 1.41) rubocop-factory_bot (2.23.1) rubocop (~> 1.33) - rubocop-performance (1.19.0) + rubocop-performance (1.19.1) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) rubocop-rails (2.20.2) From 71447ab19fce03c8a663a89c1b84816b209f192f Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 17 Oct 2023 10:57:19 -0400 Subject: [PATCH 34/35] Fix `Capybara/ClickLinkOrButtonStyle` cop in spec/features/admin area (#27445) --- spec/features/admin/accounts_spec.rb | 8 ++++---- spec/features/admin/custom_emojis_spec.rb | 2 +- spec/features/admin/email_domain_blocks_spec.rb | 2 +- spec/features/admin/ip_blocks_spec.rb | 2 +- spec/features/admin/statuses_spec.rb | 2 +- .../admin/trends/links/preview_card_providers_spec.rb | 2 +- spec/features/admin/trends/links_spec.rb | 2 +- spec/features/admin/trends/statuses_spec.rb | 2 +- spec/features/admin/trends/tags_spec.rb | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/spec/features/admin/accounts_spec.rb b/spec/features/admin/accounts_spec.rb index 6d7bab1844..ad9c51485a 100644 --- a/spec/features/admin/accounts_spec.rb +++ b/spec/features/admin/accounts_spec.rb @@ -22,7 +22,7 @@ describe 'Admin::Accounts' do context 'without selecting any accounts' do it 'displays a notice about account selection' do - click_on button_for_suspend + click_button button_for_suspend expect(page).to have_content(selection_error_text) end @@ -32,7 +32,7 @@ describe 'Admin::Accounts' do it 'suspends the account' do batch_checkbox_for(approved_user_account).check - click_on button_for_suspend + click_button button_for_suspend expect(approved_user_account.reload).to be_suspended end @@ -42,7 +42,7 @@ describe 'Admin::Accounts' do it 'approves the account user' do batch_checkbox_for(unapproved_user_account).check - click_on button_for_approve + click_button button_for_approve expect(unapproved_user_account.reload.user).to be_approved end @@ -52,7 +52,7 @@ describe 'Admin::Accounts' do it 'rejects and removes the account' do batch_checkbox_for(unapproved_user_account).check - click_on button_for_reject + click_button button_for_reject expect { unapproved_user_account.reload }.to raise_error(ActiveRecord::RecordNotFound) end diff --git a/spec/features/admin/custom_emojis_spec.rb b/spec/features/admin/custom_emojis_spec.rb index 8a8b6efcd1..3fea8f06fe 100644 --- a/spec/features/admin/custom_emojis_spec.rb +++ b/spec/features/admin/custom_emojis_spec.rb @@ -16,7 +16,7 @@ describe 'Admin::CustomEmojis' do context 'without selecting any records' do it 'displays a notice about selection' do - click_on button_for_enable + click_button button_for_enable expect(page).to have_content(selection_error_text) end diff --git a/spec/features/admin/email_domain_blocks_spec.rb b/spec/features/admin/email_domain_blocks_spec.rb index 14959cbe74..80efe72e95 100644 --- a/spec/features/admin/email_domain_blocks_spec.rb +++ b/spec/features/admin/email_domain_blocks_spec.rb @@ -16,7 +16,7 @@ describe 'Admin::EmailDomainBlocks' do context 'without selecting any records' do it 'displays a notice about selection' do - click_on button_for_delete + click_button button_for_delete expect(page).to have_content(selection_error_text) end diff --git a/spec/features/admin/ip_blocks_spec.rb b/spec/features/admin/ip_blocks_spec.rb index c9b16f6f78..465c889190 100644 --- a/spec/features/admin/ip_blocks_spec.rb +++ b/spec/features/admin/ip_blocks_spec.rb @@ -16,7 +16,7 @@ describe 'Admin::IpBlocks' do context 'without selecting any records' do it 'displays a notice about selection' do - click_on button_for_delete + click_button button_for_delete expect(page).to have_content(selection_error_text) end diff --git a/spec/features/admin/statuses_spec.rb b/spec/features/admin/statuses_spec.rb index 531d0de953..a21c901a92 100644 --- a/spec/features/admin/statuses_spec.rb +++ b/spec/features/admin/statuses_spec.rb @@ -17,7 +17,7 @@ describe 'Admin::Statuses' do context 'without selecting any records' do it 'displays a notice about selection' do - click_on button_for_report + click_button button_for_report expect(page).to have_content(selection_error_text) end diff --git a/spec/features/admin/trends/links/preview_card_providers_spec.rb b/spec/features/admin/trends/links/preview_card_providers_spec.rb index dca89117b1..cf9796abf3 100644 --- a/spec/features/admin/trends/links/preview_card_providers_spec.rb +++ b/spec/features/admin/trends/links/preview_card_providers_spec.rb @@ -16,7 +16,7 @@ describe 'Admin::Trends::Links::PreviewCardProviders' do context 'without selecting any records' do it 'displays a notice about selection' do - click_on button_for_allow + click_button button_for_allow expect(page).to have_content(selection_error_text) end diff --git a/spec/features/admin/trends/links_spec.rb b/spec/features/admin/trends/links_spec.rb index 99638bc069..8b1b991a5a 100644 --- a/spec/features/admin/trends/links_spec.rb +++ b/spec/features/admin/trends/links_spec.rb @@ -16,7 +16,7 @@ describe 'Admin::Trends::Links' do context 'without selecting any records' do it 'displays a notice about selection' do - click_on button_for_allow + click_button button_for_allow expect(page).to have_content(selection_error_text) end diff --git a/spec/features/admin/trends/statuses_spec.rb b/spec/features/admin/trends/statuses_spec.rb index 779a15d38f..a578ab0559 100644 --- a/spec/features/admin/trends/statuses_spec.rb +++ b/spec/features/admin/trends/statuses_spec.rb @@ -16,7 +16,7 @@ describe 'Admin::Trends::Statuses' do context 'without selecting any records' do it 'displays a notice about selection' do - click_on button_for_allow + click_button button_for_allow expect(page).to have_content(selection_error_text) end diff --git a/spec/features/admin/trends/tags_spec.rb b/spec/features/admin/trends/tags_spec.rb index 52e49c3a5d..7502bc8c6f 100644 --- a/spec/features/admin/trends/tags_spec.rb +++ b/spec/features/admin/trends/tags_spec.rb @@ -16,7 +16,7 @@ describe 'Admin::Trends::Tags' do context 'without selecting any records' do it 'displays a notice about selection' do - click_on button_for_allow + click_button button_for_allow expect(page).to have_content(selection_error_text) end From 5ef26d8fd50081c642b858a82bf0c5431b1c7e83 Mon Sep 17 00:00:00 2001 From: Nick Schonning Date: Tue, 17 Oct 2023 12:28:09 -0400 Subject: [PATCH 35/35] Add RSpec GitHub annotations gem (#24040) --- Gemfile | 3 +++ Gemfile.lock | 3 +++ spec/spec_helper.rb | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/Gemfile b/Gemfile index 17cbabd3be..98897536a3 100644 --- a/Gemfile +++ b/Gemfile @@ -106,6 +106,9 @@ group :test do # Used to split testing into chunks in CI gem 'rspec_chunked', '~> 0.6' + # Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab + gem 'rspec-github', '~> 2.4', require: false + # RSpec progress bar formatter gem 'fuubar', '~> 2.5' diff --git a/Gemfile.lock b/Gemfile.lock index c3d1ca2099..29cdc8aa4d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -602,6 +602,8 @@ GEM rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) + rspec-github (2.4.0) + rspec-core (~> 3.0) rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) @@ -885,6 +887,7 @@ DEPENDENCIES redis (~> 4.5) redis-namespace (~> 1.10) rqrcode (~> 2.2) + rspec-github (~> 2.4) rspec-rails (~> 6.0) rspec-sidekiq (~> 4.0) rspec_chunked (~> 0.6) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b4c20545f5..4d3c234a0e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -36,6 +36,12 @@ RSpec.configure do |config| config.after :suite do FileUtils.rm_rf(Dir[Rails.root.join('spec', 'test_files')]) end + + # Use the GitHub Annotations formatter for CI + if ENV['GITHUB_ACTIONS'] == 'true' + require 'rspec/github' + config.add_formatter RSpec::Github::Formatter + end end def body_as_json