diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml index f66a31447026da..040045566b65d7 100644 --- a/.haml-lint_todo.yml +++ b/.haml-lint_todo.yml @@ -29,3 +29,4 @@ linters: exclude: - 'app/views/application/_sidebar.html.haml' - 'app/views/admin/ng_rules/_ng_rule_fields.html.haml' + - 'app/views/admin/sensitive_words/_sensitive_word.html.haml' diff --git a/app/controllers/admin/sensitive_words_controller.rb b/app/controllers/admin/sensitive_words_controller.rb index f98fc4202da4ee..5fff9394e90b90 100644 --- a/app/controllers/admin/sensitive_words_controller.rb +++ b/app/controllers/admin/sensitive_words_controller.rb @@ -6,6 +6,7 @@ def show authorize :sensitive_words, :show? @admin_settings = Form::AdminSettings.new + @sensitive_words = ::SensitiveWord.caches.presence || [::SensitiveWord.new] end def create @@ -21,7 +22,7 @@ def create @admin_settings = Form::AdminSettings.new(settings_params) - if @admin_settings.save + if @admin_settings.save && ::SensitiveWord.save_from_raws(settings_params_test) flash[:notice] = I18n.t('generic.changes_saved_msg') redirect_to after_update_redirect_path else @@ -32,11 +33,8 @@ def create private def test_words - sensitive_words = settings_params['sensitive_words'].split(/\r\n|\r|\n/) - sensitive_words_for_full = settings_params['sensitive_words_for_full'].split(/\r\n|\r|\n/) - sensitive_words_all = settings_params['sensitive_words_all'].split(/\r\n|\r|\n/) - sensitive_words_all_for_full = settings_params['sensitive_words_all_for_full'].split(/\r\n|\r|\n/) - Admin::NgWord.reject_with_custom_words?('Sample text', sensitive_words + sensitive_words_for_full + sensitive_words_all + sensitive_words_all_for_full) + sensitive_words = settings_params_test['keywords'].compact.uniq + Admin::NgWord.reject_with_custom_words?('Sample text', sensitive_words) end def after_update_redirect_path @@ -46,5 +44,9 @@ def after_update_redirect_path def settings_params params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) end + + def settings_params_test + params.require(:form_admin_settings)[:sensitive_words_test] + end end end diff --git a/app/javascript/packs/admin.tsx b/app/javascript/packs/admin.tsx index a9a99d59e80b6d..206c872b4aba1f 100644 --- a/app/javascript/packs/admin.tsx +++ b/app/javascript/packs/admin.tsx @@ -272,6 +272,68 @@ Rails.delegate( }, ); +const addTableRow = (tableId: string) => { + const templateElement = document.querySelector(`#${tableId} .template-row`)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion + const tableElement = document.querySelector(`#${tableId} tbody`)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion + + if ( + typeof templateElement === 'undefined' || + typeof tableElement === 'undefined' + ) + return; + + let temporaryId = 0; + tableElement + .querySelectorAll('.temporary_id') + .forEach((input) => { + if (parseInt(input.value) + 1 > temporaryId) { + temporaryId = parseInt(input.value) + 1; + } + }); + + const cloned = templateElement.cloneNode(true) as HTMLTableRowElement; + cloned.className = ''; + cloned.querySelector('.temporary_id')!.value = // eslint-disable-line @typescript-eslint/no-non-null-assertion + temporaryId.toString(); + cloned + .querySelectorAll('input[type=checkbox]') + .forEach((input) => { + input.value = temporaryId.toString(); + }); + tableElement.appendChild(cloned); +}; + +const removeTableRow = (target: EventTarget | null, tableId: string) => { + const tableRowElement = (target as HTMLElement).closest('tr') as Node; + const tableElement = document.querySelector(`#${tableId} tbody`)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion + + if ( + typeof tableRowElement === 'undefined' || + typeof tableElement === 'undefined' + ) + return; + + tableElement.removeChild(tableRowElement); +}; + +Rails.delegate( + document, + '#sensitive-words-table .add-row-button', + 'click', + () => { + addTableRow('sensitive-words-table'); + }, +); + +Rails.delegate( + document, + '#sensitive-words-table .delete-row-button', + 'click', + ({ target }) => { + removeTableRow(target, 'sensitive-words-table'); + }, +); + async function mountReactComponent(element: Element) { const componentName = element.getAttribute('data-admin-component'); const stringProps = element.getAttribute('data-props'); diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 90bf3f80c0c02b..6ebaace499d9cf 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -1135,6 +1135,10 @@ code { margin-top: 10px; white-space: nowrap; } + + .template-row { + display: none; + } } .progress-tracker { diff --git a/app/models/admin/sensitive_word.rb b/app/models/admin/sensitive_word.rb index 8f9421f5b909e1..a766a3174dca80 100644 --- a/app/models/admin/sensitive_word.rb +++ b/app/models/admin/sensitive_word.rb @@ -5,12 +5,12 @@ class << self def sensitive?(text, spoiler_text, local: true) exposure_text = spoiler_text.presence || text - sensitive = (spoiler_text.blank? && sensitive_words_all.any? { |word| include?(text, word) }) || - sensitive_words_all_for_full.any? { |word| include?(exposure_text, word) } - return sensitive if sensitive || !local + sensitive_words = ::SensitiveWord.caches + sensitive_words.select!(&:remote) unless local - (spoiler_text.blank? && sensitive_words.any? { |word| include?(text, word) }) || - sensitive_words_for_full.any? { |word| include?(exposure_text, word) } + return sensitive_words.filter(&:spoiler).any? { |word| include?(spoiler_text, word) } if spoiler_text.present? + + sensitive_words.any? { |word| include?(exposure_text, word) } end def modified_text(text, spoiler_text) @@ -24,27 +24,11 @@ def alternative_text private def include?(text, word) - if word.start_with?('?') && word.size >= 2 - text =~ /#{word[1..]}/i + if word.regexp + text =~ /#{word.keyword}/ else - text.include?(word) + text.include?(word.keyword) end end - - def sensitive_words - Setting.sensitive_words || [] - end - - def sensitive_words_for_full - Setting.sensitive_words_for_full || [] - end - - def sensitive_words_all - Setting.sensitive_words_all || [] - end - - def sensitive_words_all_for_full - Setting.sensitive_words_all_for_full || [] - end end end diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 8a823fa1b75699..15c29b759d0777 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -51,10 +51,6 @@ class Form::AdminSettings post_hash_tags_max post_mentions_max post_stranger_mentions_max - sensitive_words - sensitive_words_for_full - sensitive_words_all - sensitive_words_all_for_full auto_warning_text authorized_fetch receive_other_servers_emoji_reaction @@ -128,10 +124,6 @@ class Form::AdminSettings STRING_ARRAY_KEYS = %i( ng_words ng_words_for_stranger_mention - sensitive_words - sensitive_words_for_full - sensitive_words_all - sensitive_words_all_for_full emoji_reaction_disallow_domains permit_new_account_domains ).freeze diff --git a/app/models/sensitive_word.rb b/app/models/sensitive_word.rb new file mode 100644 index 00000000000000..f88680aaae277d --- /dev/null +++ b/app/models/sensitive_word.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: sensitive_words +# +# id :bigint(8) not null, primary key +# keyword :string not null +# regexp :boolean default(FALSE), not null +# remote :boolean default(FALSE), not null +# spoiler :boolean default(TRUE), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class SensitiveWord < ApplicationRecord + attr_accessor :keywords, :regexps, :remotes, :spoilers + + class << self + def caches + Rails.cache.fetch('sensitive_words') { SensitiveWord.where.not(id: 0).order(:keyword).to_a } + end + + def save_from_hashes(rows) + unmatched = caches + matched = [] + + SensitiveWord.transaction do + rows.filter { |item| item[:keyword].present? }.each do |item| + exists = unmatched.find { |i| i.keyword == item[:keyword] } + + if exists.present? + unmatched.delete(exists) + matched << exists + + next if exists.regexp == item[:regexp] && exists.remote == item[:remote] && exists.spoiler == item[:spoiler] + + exists.update!(regexp: item[:regexp], remote: item[:remote], spoiler: item[:spoiler]) + elsif matched.none? { |i| i.keyword == item[:keyword] } + SensitiveWord.create!( + keyword: item[:keyword], + regexp: item[:regexp], + remote: item[:remote], + spoiler: item[:spoiler] + ) + end + end + + SensitiveWord.destroy(unmatched.map(&:id)) + end + + true + # rescue + # false + end + + def save_from_raws(rows) + regexps = rows['regexps'] || [] + remotes = rows['remotes'] || [] + spoilers = rows['spoilers'] || [] + + hashes = (rows['keywords'] || []).zip(rows['temporary_ids'] || []).map do |item| + temp_id = item[1] + { + keyword: item[0], + regexp: regexps.include?(temp_id), + remote: remotes.include?(temp_id), + spoiler: spoilers.include?(temp_id), + } + end + + save_from_hashes(hashes) + end + end + + private + + def invalidate_cache! + Rails.cache.delete('sensitive_words') + end +end diff --git a/app/views/admin/sensitive_words/_sensitive_word.html.haml b/app/views/admin/sensitive_words/_sensitive_word.html.haml new file mode 100644 index 00000000000000..de9c65714ee30c --- /dev/null +++ b/app/views/admin/sensitive_words/_sensitive_word.html.haml @@ -0,0 +1,12 @@ +- temporary_id = defined?(@temp_id) ? @temp_id += 1 : @temp_id = 1 +%tr{ class: template ? 'template-row' : nil } + %td= f.input :keywords, as: :string, input_html: { multiple: true, value: sensitive_word.keyword } + %td + .label_input__wrapper= f.check_box :regexps, { multiple: true, checked: sensitive_word.regexp }, temporary_id, nil + %td + .label_input__wrapper= f.check_box :remotes, { multiple: true, checked: sensitive_word.remote }, temporary_id, nil + %td + .label_input__wrapper= f.check_box :spoilers, { multiple: true, checked: sensitive_word.spoiler }, temporary_id, nil + %td + = hidden_field_tag :'form_admin_settings[sensitive_words_test][temporary_ids][]', temporary_id, class: 'temporary_id' + = link_to safe_join([fa_icon('times'), t('filters.index.delete')]), '#', class: 'table-action-link delete-row-button' diff --git a/app/views/admin/sensitive_words/show.html.haml b/app/views/admin/sensitive_words/show.html.haml index e9a896532ec87c..6f51419cc12e06 100644 --- a/app/views/admin/sensitive_words/show.html.haml +++ b/app/views/admin/sensitive_words/show.html.haml @@ -9,17 +9,31 @@ %p.lead= t 'admin.sensitive_words.hint' - .fields-group - = f.input :sensitive_words_for_full, wrapper: :with_label, as: :text, input_html: { rows: 8 }, label: t('admin.sensitive_words.keywords_for_all'), hint: t('admin.sensitive_words.keywords_for_all_hint') - - .fields-group - = f.input :sensitive_words, wrapper: :with_label, as: :text, input_html: { rows: 8 }, label: t('admin.sensitive_words.keywords'), hint: t('admin.sensitive_words.keywords_hint') - - .fields-group - = f.input :sensitive_words_all_for_full, wrapper: :with_label, as: :text, input_html: { rows: 8 }, label: t('admin.sensitive_words.keywords_all_for_all'), hint: t('admin.sensitive_words.keywords_for_all_hint') - - .fields-group - = f.input :sensitive_words_all, wrapper: :with_label, as: :text, input_html: { rows: 8 }, label: t('admin.sensitive_words.keywords_all'), hint: t('admin.sensitive_words.keywords_hint') + %p= t 'admin.sensitive_words.phrases.regexp_html' + %p= t 'admin.sensitive_words.phrases.remote_html' + %p= t 'admin.sensitive_words.phrases.spoiler_html' + + %hr/ + + .table-wrapper + %table.table.keywords-table#sensitive-words-table + %thead + %tr + %th= t('simple_form.labels.defaults.phrase') + %th= t('admin.sensitive_words.phrases.regexp_short') + %th= t('admin.sensitive_words.phrases.remote_short') + %th= t('admin.sensitive_words.phrases.spoiler_short') + %th + %tbody + = f.simple_fields_for :sensitive_words_test, @sensitive_words do |keyword| + = render partial: 'sensitive_word', collection: @sensitive_words, locals: { f: keyword, template: false } + + = f.simple_fields_for :sensitive_words_test, @sensitive_words do |keyword| + = render partial: 'sensitive_word', collection: [SensitiveWord.new], locals: { f: keyword, template: true } + %tfoot + %tr + %td{ colspan: 4 } + = link_to safe_join([fa_icon('plus'), t('filters.edit.add_keyword')]), '#', class: 'table-action-link add-row-button' .fields-group = f.input :auto_warning_text, wrapper: :with_label, input_html: { placeholder: t('admin.sensitive_words.alert') }, label: t('admin.sensitive_words.auto_warning_text'), hint: t('admin.sensitive_words.auto_warning_text_hint') diff --git a/config/locales/en.yml b/config/locales/en.yml index 4748200f55c242..ef20d689c6bac9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -963,11 +963,13 @@ en: auto_warning_text: Custom warning text auto_warning_text_hint: If not specified, the default warning text is used. hint: This keywords is applied to public posts only.. - keywords: Sensitive keywords for local only - keywords_all: Sensitive keywords for local+remote - keywords_all_for_all: Sensitive keywords for local+remote (Contains CW alert) - keywords_for_all: Sensitive keywords for local only (Contains CW alert) - keywords_for_all_hint: The first character of the line is "?". to use regular expressions + phrases: + remote_html: Rem ote にチェックの入っている項目は、リモートからの投稿にも適用されます。 + remote_short: Rem + regexp_html: Reg Exp にチェックの入っている項目は、正規表現を用いての比較となります。 + regexp_short: Reg + spoiler_html: War ning にチェックの入っている項目は、コンテンツ警告文にも適用されます。 + spoiler_short: War title: Sensitive words and moderation options settings: about: diff --git a/config/locales/ja.yml b/config/locales/ja.yml index f7e1fb855d93a1..b1b5b4ca5f0361 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -959,12 +959,13 @@ ja: auto_warning_text: カスタム警告文 auto_warning_text_hint: 指定しなかった場合は、各言語のデフォルト警告文が使用されます hint: センシティブなキーワードの設定は、当サーバーのローカルユーザーによる公開範囲「公開」「ローカル公開」「ログインユーザーのみ」に対して適用されます。 - keywords: ローカルの投稿に適用するセンシティブなキーワード(警告文は除外) - keywords_all: ローカル・リモートの投稿に適用するセンシティブなキーワード(警告文は除外) - keywords_all_for_all: ローカル・リモートの投稿に適用するセンシティブなキーワード(警告文にも適用) - keywords_for_all: ローカルの投稿に適用するセンシティブなキーワード(警告文にも適用) - keywords_for_all_hint: ここで指定したキーワードを含む投稿は強制的にCWになります。警告文にも含まれていればCWになります。行が「?」で始まっていれば正規表現が使えます - keywords_hint: ここで指定したキーワードを含む投稿は強制的にCWになります。ただし警告文に使用していた場合は無視されます + phrases: + remote_html: リモ ート にチェックの入っている項目は、リモートからの投稿にも適用されます。 + remote_short: リモ + regexp_html: 正規 表現 にチェックの入っている項目は、正規表現を用いての比較となります。 + regexp_short: 正規 + spoiler_html: 警告 文 にチェックの入っている項目は、コンテンツ警告文にも適用されます。 + spoiler_short: 警告 title: センシティブ単語と設定 settings: about: diff --git a/db/migrate/20240312230204_create_sensitive_words.rb b/db/migrate/20240312230204_create_sensitive_words.rb new file mode 100644 index 00000000000000..a33f492088923f --- /dev/null +++ b/db/migrate/20240312230204_create_sensitive_words.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +class CreateSensitiveWords < ActiveRecord::Migration[7.1] + class Setting < ApplicationRecord + def value + YAML.safe_load(self[:value], permitted_classes: [ActiveSupport::HashWithIndifferentAccess, Symbol]) if self[:value].present? + end + + def value=(new_value) + self[:value] = new_value.to_yaml + end + end + + class SensitiveWord < ApplicationRecord; end + + def normalized_keyword(keyword) + if regexp?(keyword) + keyword[1..] + else + keyword + end + end + + def regexp?(keyword) + keyword.start_with?('?') && keyword.size >= 2 + end + + def up + create_table :sensitive_words do |t| + t.string :keyword, null: false + t.boolean :regexp, null: false, default: false + t.boolean :remote, null: false, default: false + t.boolean :spoiler, null: false, default: true + + t.timestamps + end + + settings = Setting.where(var: %i(sensitive_words sensitive_words_for_full sensitive_words_all sensitive_words_all_for_full)) + sensitive_words = settings.find { |s| s.var == 'sensitive_words' }&.value&.compact_blank&.uniq || [] + sensitive_words_for_full = settings.find { |s| s.var == 'sensitive_words_for_full' }&.value&.compact_blank&.uniq || [] + sensitive_words_all = settings.find { |s| s.var == 'sensitive_words_all' }&.value&.compact_blank&.uniq || [] + sensitive_words_all_for_full = settings.find { |s| s.var == 'sensitive_words_all_for_full' }&.value&.compact_blank&.uniq || [] + + (sensitive_words + sensitive_words_for_full + sensitive_words_all + sensitive_words_all_for_full).compact.uniq.each do |word| + SensitiveWord.create!( + keyword: normalized_keyword(word), + regexp: regexp?(word), + remote: (sensitive_words_all + sensitive_words_all_for_full).include?(word), + spoiler: (sensitive_words_for_full + sensitive_words_all_for_full).include?(word) + ) + end + + settings.destroy_all + end + + def down + sensitive_words = SensitiveWord.where(remote: false, spoiler: false).map { |s| s.regexp ? "?#{s.keyword}" : s.keyword } + sensitive_words_for_full = SensitiveWord.where(remote: false, spoiler: true).map { |s| s.regexp ? "?#{s.keyword}" : s.keyword } + sensitive_words_all = SensitiveWord.where(remote: true, spoiler: false).map { |s| s.regexp ? "?#{s.keyword}" : s.keyword } + sensitive_words_all_for_full = SensitiveWord.where(remote: true, spoiler: true).map { |s| s.regexp ? "?#{s.keyword}" : s.keyword } + + Setting.where(var: %i(sensitive_words sensitive_words_for_full sensitive_words_all sensitive_words_all_for_full)).destroy_all + + Setting.new(var: :sensitive_words).tap { |s| s.value = sensitive_words }.save! + Setting.new(var: :sensitive_words_for_full).tap { |s| s.value = sensitive_words_for_full }.save! + Setting.new(var: :sensitive_words_all).tap { |s| s.value = sensitive_words_all }.save! + Setting.new(var: :sensitive_words_all_for_full).tap { |s| s.value = sensitive_words_all_for_full }.save! + + drop_table :sensitive_words + end +end diff --git a/db/schema.rb b/db/schema.rb index b9bca2c02fed7a..e90f1af2440da9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_03_10_123453) do +ActiveRecord::Schema[7.1].define(version: 2024_03_12_230204) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1233,6 +1233,15 @@ t.index ["scheduled_at"], name: "index_scheduled_statuses_on_scheduled_at" end + create_table "sensitive_words", force: :cascade do |t| + t.string "keyword", null: false + t.boolean "regexp", default: false, null: false + t.boolean "remote", default: false, null: false + t.boolean "spoiler", default: true, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "session_activations", force: :cascade do |t| t.string "session_id", null: false t.datetime "created_at", precision: nil, null: false diff --git a/spec/fabricators/sensitive_word_fabricator.rb b/spec/fabricators/sensitive_word_fabricator.rb new file mode 100644 index 00000000000000..0339f015b9cf39 --- /dev/null +++ b/spec/fabricators/sensitive_word_fabricator.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Fabricator(:sensitive_word) do + keyword { sequence(:keyword) { |i| "keyword_#{i}" } } +end diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 7c91f55203eb11..414b9673807e0a 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -2071,7 +2071,8 @@ def activity_for_object(json) end before do - Form::AdminSettings.new(sensitive_words_all: sensitive_words_all, sensitive_words: 'ipsum').save + Fabricate(:sensitive_word, keyword: sensitive_words_all, remote: true, spoiler: false) if sensitive_words_all.present? + Fabricate(:sensitive_word, keyword: 'ipsum') subject.perform end diff --git a/spec/models/admin/sensitive_word_spec.rb b/spec/models/admin/sensitive_word_spec.rb new file mode 100644 index 00000000000000..b07127dafaf0b9 --- /dev/null +++ b/spec/models/admin/sensitive_word_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Admin::SensitiveWord do + describe '#sensitive?' do + subject { described_class.sensitive?(text, spoiler_text, local: local) } + + let(:text) { 'This is a ohagi.' } + let(:spoiler_text) { '' } + let(:local) { true } + + context 'when a local post' do + it 'local word hits' do + Fabricate(:sensitive_word, keyword: 'ohagi', remote: false) + expect(subject).to be true + end + + it 'remote word hits' do + Fabricate(:sensitive_word, keyword: 'ohagi', remote: true) + expect(subject).to be true + end + end + + context 'when a remote post' do + let(:local) { false } + + it 'local word does not hit' do + Fabricate(:sensitive_word, keyword: 'ohagi', remote: false) + expect(subject).to be false + end + + it 'remote word hits' do + Fabricate(:sensitive_word, keyword: 'ohagi', remote: true) + expect(subject).to be true + end + end + + context 'when using regexp' do + it 'regexp hits with enable' do + Fabricate(:sensitive_word, keyword: 'oha[ghi]i', regexp: true) + expect(subject).to be true + end + + it 'regexp does not hit without enable' do + Fabricate(:sensitive_word, keyword: 'oha[ghi]i', regexp: false) + expect(subject).to be false + end + end + + context 'when spoiler text is set' do + let(:spoiler_text) { 'amy' } + + it 'sensitive word in content is escaped' do + Fabricate(:sensitive_word, keyword: 'ohagi', spoiler: false) + expect(subject).to be false + end + + it 'sensitive word in content is escaped even if spoiler is true' do + Fabricate(:sensitive_word, keyword: 'ohagi', spoiler: true) + expect(subject).to be false + end + + it 'non-spoiler word does not hit' do + Fabricate(:sensitive_word, keyword: 'amy', spoiler: false) + expect(subject).to be false + end + + it 'spoiler word hits' do + Fabricate(:sensitive_word, keyword: 'amy', spoiler: true) + expect(subject).to be true + end + end + end +end diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index 13b93a8acfaff6..5df7a2c96c07c3 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -758,7 +758,7 @@ def poll_option_json(name, votes) let(:content) { 'ng word aiueo' } it 'update status' do - Form::AdminSettings.new(sensitive_words_all: 'test').save + Fabricate(:sensitive_word, keyword: 'test', remote: true, spoiler: false) subject.call(status, json, json) expect(status.reload.text).to eq content @@ -770,7 +770,7 @@ def poll_option_json(name, votes) let(:content) { 'ng word test' } it 'update status' do - Form::AdminSettings.new(sensitive_words_all: 'test').save + Fabricate(:sensitive_word, keyword: 'test', remote: true, spoiler: false) subject.call(status, json, json) expect(status.reload.text).to eq content