From 43eff898a0b0f31aaf042d9d387aaece2627a01d Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 17 Mar 2021 10:09:55 +0100 Subject: Prepare Mastodon for Rails 6 (#15911) * Fix misuse of foreign_type * Fix use of removed "add_template_helper" * Use response.media_type instead of response.content_type in tests * Fix CSV export controller test on Rails 6 Rails 6 sets a "filename*" field in the Content-Disposition header to explicitly encode the filename as UTF-8. This changes checks the first part of the Content-Disposition header so it matches in both Rails 5 and Rails 6. * Fix emoji formatting with Rails 6 * Make emoji output more idiomatic and robust * Switch from redis-rails gem to built-in Rails redis cache storage --- lib/mastodon/redis_config.rb | 2 ++ 1 file changed, 2 insertions(+) (limited to 'lib') diff --git a/lib/mastodon/redis_config.rb b/lib/mastodon/redis_config.rb index c3c8ff800..3f2a8f7c2 100644 --- a/lib/mastodon/redis_config.rb +++ b/lib/mastodon/redis_config.rb @@ -27,6 +27,8 @@ namespace = ENV.fetch('REDIS_NAMESPACE', nil) cache_namespace = namespace ? namespace + '_cache' : 'cache' REDIS_CACHE_PARAMS = { + driver: :hiredis, + url: ENV['REDIS_URL'], expires_in: 10.minutes, namespace: cache_namespace, }.freeze -- cgit From a4dcaef53b97c58fd153de6f151b6fada40f3442 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 19 Mar 2021 02:42:43 +0100 Subject: Prepare Mastodon for zeitwerk autoloader (#15917) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Prepare Mastodon for zeitwerk autoloader (Rails 6) Add inflections and rename/move a few classes. In particular, app/lib/exceptions.rb and app/lib/sanitize_config.rb were manually loaded while still in autoload paths. * Add inflection for Url → URL --- app/lib/exceptions.rb | 23 ---- app/lib/formatter.rb | 1 - app/lib/sanitize_config.rb | 119 --------------------- app/validators/url_validator.rb | 2 +- config/application.rb | 3 +- config/initializers/inflections.rb | 4 + db/migrate/20160223165723_add_url_to_statuses.rb | 2 +- db/migrate/20160223165855_add_url_to_accounts.rb | 2 +- ...0322193748_add_avatar_remote_url_to_accounts.rb | 2 +- ...0318214217_add_header_remote_url_to_accounts.rb | 2 +- ...0171130000000_add_embed_url_to_preview_cards.rb | 2 +- ...3859_add_featured_collection_url_to_accounts.rb | 2 +- .../20200529214050_add_devices_url_to_accounts.rb | 2 +- lib/exceptions.rb | 23 ++++ lib/sanitize_ext/sanitize_config.rb | 119 +++++++++++++++++++++ spec/lib/sanitize_config_spec.rb | 1 - spec/validators/url_validator_spec.rb | 2 +- 17 files changed, 157 insertions(+), 154 deletions(-) delete mode 100644 app/lib/exceptions.rb delete mode 100644 app/lib/sanitize_config.rb create mode 100644 lib/exceptions.rb create mode 100644 lib/sanitize_ext/sanitize_config.rb (limited to 'lib') diff --git a/app/lib/exceptions.rb b/app/lib/exceptions.rb deleted file mode 100644 index 7c8e77871..000000000 --- a/app/lib/exceptions.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module Mastodon - class Error < StandardError; end - class NotPermittedError < Error; end - class ValidationError < Error; end - class HostValidationError < ValidationError; end - class LengthValidationError < ValidationError; end - class DimensionsValidationError < ValidationError; end - class StreamValidationError < ValidationError; end - class RaceConditionError < Error; end - class RateLimitExceededError < Error; end - - class UnexpectedResponseError < Error - def initialize(response = nil) - if response.respond_to? :uri - super("#{response.uri} returned code #{response.code}") - else - super - end - end - end -end diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 6fb5d5419..2611bcbae 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'singleton' -require_relative './sanitize_config' class Formatter include Singleton diff --git a/app/lib/sanitize_config.rb b/app/lib/sanitize_config.rb deleted file mode 100644 index a2e1d9d01..000000000 --- a/app/lib/sanitize_config.rb +++ /dev/null @@ -1,119 +0,0 @@ -# frozen_string_literal: true - -class Sanitize - module Config - HTTP_PROTOCOLS = %w( - http - https - ).freeze - - LINK_PROTOCOLS = %w( - http - https - dat - dweb - ipfs - ipns - ssb - gopher - xmpp - magnet - gemini - ).freeze - - CLASS_WHITELIST_TRANSFORMER = lambda do |env| - node = env[:node] - class_list = node['class']&.split(/[\t\n\f\r ]/) - - return unless class_list - - class_list.keep_if do |e| - next true if /^(h|p|u|dt|e)-/.match?(e) # microformats classes - next true if /^(mention|hashtag)$/.match?(e) # semantic classes - next true if /^(ellipsis|invisible)$/.match?(e) # link formatting classes - end - - node['class'] = class_list.join(' ') - end - - UNSUPPORTED_HREF_TRANSFORMER = lambda do |env| - return unless env[:node_name] == 'a' - - current_node = env[:node] - - scheme = begin - if current_node['href'] =~ Sanitize::REGEX_PROTOCOL - Regexp.last_match(1).downcase - else - :relative - end - end - - current_node.replace(current_node.text) unless LINK_PROTOCOLS.include?(scheme) - end - - UNSUPPORTED_ELEMENTS_TRANSFORMER = lambda do |env| - return unless %w(h1 h2 h3 h4 h5 h6 blockquote pre ul ol li).include?(env[:node_name]) - - current_node = env[:node] - - case env[:node_name] - when 'li' - current_node.traverse do |node| - next unless %w(p ul ol li).include?(node.name) - - node.add_next_sibling('
') if node.next_sibling - node.replace(node.children) unless node.text? - end - else - current_node.name = 'p' - end - end - - MASTODON_STRICT ||= freeze_config( - elements: %w(p br span a), - - attributes: { - 'a' => %w(href rel class), - 'span' => %w(class), - }, - - add_attributes: { - 'a' => { - 'rel' => 'nofollow noopener noreferrer', - 'target' => '_blank', - }, - }, - - protocols: {}, - - transformers: [ - CLASS_WHITELIST_TRANSFORMER, - UNSUPPORTED_ELEMENTS_TRANSFORMER, - UNSUPPORTED_HREF_TRANSFORMER, - ] - ) - - MASTODON_OEMBED ||= freeze_config merge( - RELAXED, - elements: RELAXED[:elements] + %w(audio embed iframe source video), - - attributes: merge( - RELAXED[:attributes], - 'audio' => %w(controls), - 'embed' => %w(height src type width), - 'iframe' => %w(allowfullscreen frameborder height scrolling src width), - 'source' => %w(src type), - 'video' => %w(controls height loop width), - 'div' => [:data] - ), - - protocols: merge( - RELAXED[:protocols], - 'embed' => { 'src' => HTTP_PROTOCOLS }, - 'iframe' => { 'src' => HTTP_PROTOCOLS }, - 'source' => { 'src' => HTTP_PROTOCOLS } - ) - ) - end -end diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb index d95a03fbf..f50abbe24 100644 --- a/app/validators/url_validator.rb +++ b/app/validators/url_validator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class UrlValidator < ActiveModel::EachValidator +class URLValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) record.errors.add(attribute, I18n.t('applications.invalid_url')) unless compliant?(value) end diff --git a/config/application.rb b/config/application.rb index 116eaf29d..0960247b3 100644 --- a/config/application.rb +++ b/config/application.rb @@ -6,8 +6,9 @@ require 'rails/all' # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) -require_relative '../app/lib/exceptions' +require_relative '../lib/exceptions' require_relative '../lib/enumerable' +require_relative '../lib/sanitize_ext/sanitize_config' require_relative '../lib/redis/namespace_extensions' require_relative '../lib/paperclip/url_generator_extensions' require_relative '../lib/paperclip/attachment_extensions' diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index ebb7541eb..9bc9a54b2 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -20,6 +20,10 @@ ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym 'JsonLd' inflect.acronym 'NodeInfo' inflect.acronym 'Ed25519' + inflect.acronym 'TOC' + inflect.acronym 'RSS' + inflect.acronym 'REST' + inflect.acronym 'URL' inflect.singular 'data', 'data' end diff --git a/db/migrate/20160223165723_add_url_to_statuses.rb b/db/migrate/20160223165723_add_url_to_statuses.rb index 80f4b3289..fee7f9c59 100644 --- a/db/migrate/20160223165723_add_url_to_statuses.rb +++ b/db/migrate/20160223165723_add_url_to_statuses.rb @@ -1,4 +1,4 @@ -class AddUrlToStatuses < ActiveRecord::Migration[4.2] +class AddURLToStatuses < ActiveRecord::Migration[4.2] def change add_column :statuses, :url, :string, null: true, default: nil end diff --git a/db/migrate/20160223165855_add_url_to_accounts.rb b/db/migrate/20160223165855_add_url_to_accounts.rb index c81b1c64f..a4db8814a 100644 --- a/db/migrate/20160223165855_add_url_to_accounts.rb +++ b/db/migrate/20160223165855_add_url_to_accounts.rb @@ -1,4 +1,4 @@ -class AddUrlToAccounts < ActiveRecord::Migration[4.2] +class AddURLToAccounts < ActiveRecord::Migration[4.2] def change add_column :accounts, :url, :string, null: true, default: nil end diff --git a/db/migrate/20160322193748_add_avatar_remote_url_to_accounts.rb b/db/migrate/20160322193748_add_avatar_remote_url_to_accounts.rb index f9c213d9b..0792863a3 100644 --- a/db/migrate/20160322193748_add_avatar_remote_url_to_accounts.rb +++ b/db/migrate/20160322193748_add_avatar_remote_url_to_accounts.rb @@ -1,4 +1,4 @@ -class AddAvatarRemoteUrlToAccounts < ActiveRecord::Migration[4.2] +class AddAvatarRemoteURLToAccounts < ActiveRecord::Migration[4.2] def change add_column :accounts, :avatar_remote_url, :string, null: true, default: nil end diff --git a/db/migrate/20170318214217_add_header_remote_url_to_accounts.rb b/db/migrate/20170318214217_add_header_remote_url_to_accounts.rb index 0ba38d3e0..20c965988 100644 --- a/db/migrate/20170318214217_add_header_remote_url_to_accounts.rb +++ b/db/migrate/20170318214217_add_header_remote_url_to_accounts.rb @@ -1,4 +1,4 @@ -class AddHeaderRemoteUrlToAccounts < ActiveRecord::Migration[5.0] +class AddHeaderRemoteURLToAccounts < ActiveRecord::Migration[5.0] def change add_column :accounts, :header_remote_url, :string, null: false, default: '' end diff --git a/db/migrate/20171130000000_add_embed_url_to_preview_cards.rb b/db/migrate/20171130000000_add_embed_url_to_preview_cards.rb index d19c0091b..8fcabef9f 100644 --- a/db/migrate/20171130000000_add_embed_url_to_preview_cards.rb +++ b/db/migrate/20171130000000_add_embed_url_to_preview_cards.rb @@ -1,6 +1,6 @@ require Rails.root.join('lib', 'mastodon', 'migration_helpers') -class AddEmbedUrlToPreviewCards < ActiveRecord::Migration[5.1] +class AddEmbedURLToPreviewCards < ActiveRecord::Migration[5.1] include Mastodon::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20180304013859_add_featured_collection_url_to_accounts.rb b/db/migrate/20180304013859_add_featured_collection_url_to_accounts.rb index e0b8ed5cc..1964b5121 100644 --- a/db/migrate/20180304013859_add_featured_collection_url_to_accounts.rb +++ b/db/migrate/20180304013859_add_featured_collection_url_to_accounts.rb @@ -1,4 +1,4 @@ -class AddFeaturedCollectionUrlToAccounts < ActiveRecord::Migration[5.1] +class AddFeaturedCollectionURLToAccounts < ActiveRecord::Migration[5.1] def change add_column :accounts, :featured_collection_url, :string end diff --git a/db/migrate/20200529214050_add_devices_url_to_accounts.rb b/db/migrate/20200529214050_add_devices_url_to_accounts.rb index 564877e5d..1323f8df7 100644 --- a/db/migrate/20200529214050_add_devices_url_to_accounts.rb +++ b/db/migrate/20200529214050_add_devices_url_to_accounts.rb @@ -1,4 +1,4 @@ -class AddDevicesUrlToAccounts < ActiveRecord::Migration[5.2] +class AddDevicesURLToAccounts < ActiveRecord::Migration[5.2] def change add_column :accounts, :devices_url, :string end diff --git a/lib/exceptions.rb b/lib/exceptions.rb new file mode 100644 index 000000000..7c8e77871 --- /dev/null +++ b/lib/exceptions.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Mastodon + class Error < StandardError; end + class NotPermittedError < Error; end + class ValidationError < Error; end + class HostValidationError < ValidationError; end + class LengthValidationError < ValidationError; end + class DimensionsValidationError < ValidationError; end + class StreamValidationError < ValidationError; end + class RaceConditionError < Error; end + class RateLimitExceededError < Error; end + + class UnexpectedResponseError < Error + def initialize(response = nil) + if response.respond_to? :uri + super("#{response.uri} returned code #{response.code}") + else + super + end + end + end +end diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb new file mode 100644 index 000000000..a2e1d9d01 --- /dev/null +++ b/lib/sanitize_ext/sanitize_config.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +class Sanitize + module Config + HTTP_PROTOCOLS = %w( + http + https + ).freeze + + LINK_PROTOCOLS = %w( + http + https + dat + dweb + ipfs + ipns + ssb + gopher + xmpp + magnet + gemini + ).freeze + + CLASS_WHITELIST_TRANSFORMER = lambda do |env| + node = env[:node] + class_list = node['class']&.split(/[\t\n\f\r ]/) + + return unless class_list + + class_list.keep_if do |e| + next true if /^(h|p|u|dt|e)-/.match?(e) # microformats classes + next true if /^(mention|hashtag)$/.match?(e) # semantic classes + next true if /^(ellipsis|invisible)$/.match?(e) # link formatting classes + end + + node['class'] = class_list.join(' ') + end + + UNSUPPORTED_HREF_TRANSFORMER = lambda do |env| + return unless env[:node_name] == 'a' + + current_node = env[:node] + + scheme = begin + if current_node['href'] =~ Sanitize::REGEX_PROTOCOL + Regexp.last_match(1).downcase + else + :relative + end + end + + current_node.replace(current_node.text) unless LINK_PROTOCOLS.include?(scheme) + end + + UNSUPPORTED_ELEMENTS_TRANSFORMER = lambda do |env| + return unless %w(h1 h2 h3 h4 h5 h6 blockquote pre ul ol li).include?(env[:node_name]) + + current_node = env[:node] + + case env[:node_name] + when 'li' + current_node.traverse do |node| + next unless %w(p ul ol li).include?(node.name) + + node.add_next_sibling('
') if node.next_sibling + node.replace(node.children) unless node.text? + end + else + current_node.name = 'p' + end + end + + MASTODON_STRICT ||= freeze_config( + elements: %w(p br span a), + + attributes: { + 'a' => %w(href rel class), + 'span' => %w(class), + }, + + add_attributes: { + 'a' => { + 'rel' => 'nofollow noopener noreferrer', + 'target' => '_blank', + }, + }, + + protocols: {}, + + transformers: [ + CLASS_WHITELIST_TRANSFORMER, + UNSUPPORTED_ELEMENTS_TRANSFORMER, + UNSUPPORTED_HREF_TRANSFORMER, + ] + ) + + MASTODON_OEMBED ||= freeze_config merge( + RELAXED, + elements: RELAXED[:elements] + %w(audio embed iframe source video), + + attributes: merge( + RELAXED[:attributes], + 'audio' => %w(controls), + 'embed' => %w(height src type width), + 'iframe' => %w(allowfullscreen frameborder height scrolling src width), + 'source' => %w(src type), + 'video' => %w(controls height loop width), + 'div' => [:data] + ), + + protocols: merge( + RELAXED[:protocols], + 'embed' => { 'src' => HTTP_PROTOCOLS }, + 'iframe' => { 'src' => HTTP_PROTOCOLS }, + 'source' => { 'src' => HTTP_PROTOCOLS } + ) + ) + end +end diff --git a/spec/lib/sanitize_config_spec.rb b/spec/lib/sanitize_config_spec.rb index d66302e64..747d81158 100644 --- a/spec/lib/sanitize_config_spec.rb +++ b/spec/lib/sanitize_config_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'rails_helper' -require Rails.root.join('app', 'lib', 'sanitize_config.rb') describe Sanitize::Config do describe '::MASTODON_STRICT' do diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb index e8d0e6494..a44878a44 100644 --- a/spec/validators/url_validator_spec.rb +++ b/spec/validators/url_validator_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe UrlValidator, type: :validator do +RSpec.describe URLValidator, type: :validator do describe '#validate_each' do before do allow(validator).to receive(:compliant?).with(value) { compliant } -- cgit From 9aaaa96d2ff5e27f065375a2544e86afa31a4e13 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 19 Mar 2021 02:43:13 +0100 Subject: Use more robust hook for loading timestamp_id function into database (#15919) --- config/application.rb | 1 + .../20170920024819_status_ids_to_timestamp_ids.rb | 4 +- lib/active_record/database_tasks_extensions.rb | 20 ++++++++ lib/tasks/db.rake | 56 ---------------------- 4 files changed, 23 insertions(+), 58 deletions(-) create mode 100644 lib/active_record/database_tasks_extensions.rb (limited to 'lib') diff --git a/config/application.rb b/config/application.rb index 0960247b3..3267fa71b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -28,6 +28,7 @@ require_relative '../lib/webpacker/manifest_extensions' require_relative '../lib/webpacker/helper_extensions' require_relative '../lib/action_dispatch/cookie_jar_extensions' require_relative '../lib/rails/engine_extensions' +require_relative '../lib/active_record/database_tasks_extensions' Dotenv::Railtie.load diff --git a/db/migrate/20170920024819_status_ids_to_timestamp_ids.rb b/db/migrate/20170920024819_status_ids_to_timestamp_ids.rb index c10aa2c4f..8679f8ece 100644 --- a/db/migrate/20170920024819_status_ids_to_timestamp_ids.rb +++ b/db/migrate/20170920024819_status_ids_to_timestamp_ids.rb @@ -1,7 +1,7 @@ class StatusIdsToTimestampIds < ActiveRecord::Migration[5.1] def up # Prepare the function we will use to generate IDs. - Rake::Task['db:define_timestamp_id'].execute + Mastodon::Snowflake.define_timestamp_id # Set up the statuses.id column to use our timestamp-based IDs. ActiveRecord::Base.connection.execute(<<~SQL) @@ -11,7 +11,7 @@ class StatusIdsToTimestampIds < ActiveRecord::Migration[5.1] SQL # Make sure we have a sequence to use. - Rake::Task['db:ensure_id_sequences_exist'].execute + Mastodon::Snowflake.ensure_id_sequences_exist end def down diff --git a/lib/active_record/database_tasks_extensions.rb b/lib/active_record/database_tasks_extensions.rb new file mode 100644 index 000000000..e274f476d --- /dev/null +++ b/lib/active_record/database_tasks_extensions.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require_relative '../mastodon/snowflake' + +module ActiveRecord + module Tasks + module DatabaseTasks + original_load_schema = instance_method(:load_schema) + + define_method(:load_schema) do |db_config, *args| + ActiveRecord::Base.establish_connection(db_config) + Mastodon::Snowflake.define_timestamp_id + + original_load_schema.bind(self).call(db_config, *args) + + Mastodon::Snowflake.ensure_id_sequences_exist + end + end + end +end diff --git a/lib/tasks/db.rake b/lib/tasks/db.rake index f6c9c7eec..552a02b3f 100644 --- a/lib/tasks/db.rake +++ b/lib/tasks/db.rake @@ -1,36 +1,5 @@ # frozen_string_literal: true -require_relative '../mastodon/snowflake' - -def each_schema_load_environment - # If we're in development, also run this for the test environment. - # This is a somewhat hacky way to do this, so here's why: - # 1. We have to define this before we load the schema, or we won't - # have a timestamp_id function when we get to it in the schema. - # 2. db:setup calls db:schema:load_if_ruby, which calls - # db:schema:load, which we define above as having a prerequisite - # of this task. - # 3. db:schema:load ends up running - # ActiveRecord::Tasks::DatabaseTasks.load_schema_current, which - # calls a private method `each_current_configuration`, which - # explicitly also does the loading for the `test` environment - # if the current environment is `development`, so we end up - # needing to do the same, and we can't even use the same method - # to do it. - - if Rails.env.development? - test_conf = ActiveRecord::Base.configurations['test'] - - if test_conf['database']&.present? - ActiveRecord::Base.establish_connection(:test) - yield - ActiveRecord::Base.establish_connection(Rails.env.to_sym) - end - end - - yield -end - namespace :db do namespace :migrate do desc 'Setup the db or migrate depending on state of db' @@ -61,29 +30,4 @@ namespace :db do end Rake::Task['db:migrate'].enhance(['db:post_migration_hook']) - - # Before we load the schema, define the timestamp_id function. - # Idiomatically, we might do this in a migration, but then it - # wouldn't end up in schema.rb, so we'd need to figure out a way to - # get it in before doing db:setup as well. This is simpler, and - # ensures it's always in place. - Rake::Task['db:schema:load'].enhance ['db:define_timestamp_id'] - - # After we load the schema, make sure we have sequences for each - # table using timestamp IDs. - Rake::Task['db:schema:load'].enhance do - Rake::Task['db:ensure_id_sequences_exist'].invoke - end - - task :define_timestamp_id do - each_schema_load_environment do - Mastodon::Snowflake.define_timestamp_id - end - end - - task :ensure_id_sequences_exist do - each_schema_load_environment do - Mastodon::Snowflake.ensure_id_sequences_exist - end - end end -- cgit From b3582298341e32528929c6f3292e36a6fa261ba5 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 19 Mar 2021 02:45:34 +0100 Subject: Further preparation for Rails 6 (#15916) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use ActiveRecord::Result#to_ary instead of deprecated to_hash They do the same thing, and to_hash has been removed from Rails 6.1 * Explicitly name polymorphic indexes to workaround a bug in Rails 6.1 cf. https://github.com/rails/rails/issues/41693 * Fix incorrect usage of “foreign_key” in migration script * Use `ActiveModel::Errors#delete` instead of deprecated clear method * Fix link headers tests on Rails 6.1 Rails 6.1 adds values to the Link header by default, thus it is not a LinkHeader object anymore. Fix the test to parse the Link header instead of assuming it is a LinkHeader. --- app/controllers/admin/domain_blocks_controller.rb | 2 +- db/migrate/20161006213403_rails_settings_migration.rb | 8 ++++---- db/migrate/20171119172437_create_admin_action_logs.rb | 2 +- db/migrate/20180528141303_fix_accounts_unique_index.rb | 2 +- db/migrate/20181024224956_migrate_account_conversations.rb | 4 ++-- db/migrate/20181207011115_downcase_custom_emoji_domains.rb | 2 +- db/migrate/20190726175042_add_case_insensitive_index_to_tags.rb | 2 +- lib/mastodon/migration_helpers.rb | 6 +++--- spec/requests/link_headers_spec.rb | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) (limited to 'lib') diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index ba927b04a..b140c454c 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -22,7 +22,7 @@ module Admin if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) @domain_block.save flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety - @domain_block.errors[:domain].clear + @domain_block.errors.delete(:domain) render :new else if existing_domain_block.present? diff --git a/db/migrate/20161006213403_rails_settings_migration.rb b/db/migrate/20161006213403_rails_settings_migration.rb index 42875d7cb..9d565cb5c 100644 --- a/db/migrate/20161006213403_rails_settings_migration.rb +++ b/db/migrate/20161006213403_rails_settings_migration.rb @@ -7,12 +7,12 @@ end class RailsSettingsMigration < MIGRATION_BASE_CLASS def self.up create_table :settings do |t| - t.string :var, :null => false + t.string :var, null: false t.text :value - t.references :target, :null => false, :polymorphic => true - t.timestamps :null => true + t.references :target, null: false, polymorphic: true, index: { name: 'index_settings_on_target_type_and_target_id' } + t.timestamps null: true end - add_index :settings, [ :target_type, :target_id, :var ], :unique => true + add_index :settings, [ :target_type, :target_id, :var ], unique: true end def self.down diff --git a/db/migrate/20171119172437_create_admin_action_logs.rb b/db/migrate/20171119172437_create_admin_action_logs.rb index 0c2b6c623..b690735d2 100644 --- a/db/migrate/20171119172437_create_admin_action_logs.rb +++ b/db/migrate/20171119172437_create_admin_action_logs.rb @@ -3,7 +3,7 @@ class CreateAdminActionLogs < ActiveRecord::Migration[5.1] create_table :admin_action_logs do |t| t.belongs_to :account, foreign_key: { on_delete: :cascade } t.string :action, null: false, default: '' - t.references :target, polymorphic: true + t.references :target, polymorphic: true, index: { name: 'index_admin_action_logs_on_target_type_and_target_id' } t.text :recorded_changes, null: false, default: '' t.timestamps diff --git a/db/migrate/20180528141303_fix_accounts_unique_index.rb b/db/migrate/20180528141303_fix_accounts_unique_index.rb index 5d7b3c463..02813f363 100644 --- a/db/migrate/20180528141303_fix_accounts_unique_index.rb +++ b/db/migrate/20180528141303_fix_accounts_unique_index.rb @@ -37,7 +37,7 @@ class FixAccountsUniqueIndex < ActiveRecord::Migration[5.2] end end - duplicates = Account.connection.select_all('SELECT string_agg(id::text, \',\') AS ids FROM accounts GROUP BY lower(username), lower(domain) HAVING count(*) > 1').to_hash + duplicates = Account.connection.select_all('SELECT string_agg(id::text, \',\') AS ids FROM accounts GROUP BY lower(username), lower(domain) HAVING count(*) > 1').to_ary duplicates.each do |row| deduplicate_account!(row['ids'].split(',')) diff --git a/db/migrate/20181024224956_migrate_account_conversations.rb b/db/migrate/20181024224956_migrate_account_conversations.rb index 12e0a70fa..9e6497d81 100644 --- a/db/migrate/20181024224956_migrate_account_conversations.rb +++ b/db/migrate/20181024224956_migrate_account_conversations.rb @@ -17,8 +17,8 @@ class MigrateAccountConversations < ActiveRecord::Migration[5.2] belongs_to :account, optional: true belongs_to :activity, polymorphic: true, optional: true - belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id', optional: true - belongs_to :mention, foreign_type: 'Mention', foreign_key: 'activity_id', optional: true + belongs_to :status, foreign_key: 'activity_id', optional: true + belongs_to :mention, foreign_key: 'activity_id', optional: true def target_status mention&.status diff --git a/db/migrate/20181207011115_downcase_custom_emoji_domains.rb b/db/migrate/20181207011115_downcase_custom_emoji_domains.rb index 65f1fc8d9..e27e0249d 100644 --- a/db/migrate/20181207011115_downcase_custom_emoji_domains.rb +++ b/db/migrate/20181207011115_downcase_custom_emoji_domains.rb @@ -2,7 +2,7 @@ class DowncaseCustomEmojiDomains < ActiveRecord::Migration[5.2] disable_ddl_transaction! def up - duplicates = CustomEmoji.connection.select_all('SELECT string_agg(id::text, \',\') AS ids FROM custom_emojis GROUP BY shortcode, lower(domain) HAVING count(*) > 1').to_hash + duplicates = CustomEmoji.connection.select_all('SELECT string_agg(id::text, \',\') AS ids FROM custom_emojis GROUP BY shortcode, lower(domain) HAVING count(*) > 1').to_ary duplicates.each do |row| CustomEmoji.where(id: row['ids'].split(',')[0...-1]).destroy_all diff --git a/db/migrate/20190726175042_add_case_insensitive_index_to_tags.rb b/db/migrate/20190726175042_add_case_insensitive_index_to_tags.rb index 057fc86ba..eb03d7ca7 100644 --- a/db/migrate/20190726175042_add_case_insensitive_index_to_tags.rb +++ b/db/migrate/20190726175042_add_case_insensitive_index_to_tags.rb @@ -2,7 +2,7 @@ class AddCaseInsensitiveIndexToTags < ActiveRecord::Migration[5.2] disable_ddl_transaction! def up - Tag.connection.select_all('SELECT string_agg(id::text, \',\') AS ids FROM tags GROUP BY lower(name) HAVING count(*) > 1').to_hash.each do |row| + Tag.connection.select_all('SELECT string_agg(id::text, \',\') AS ids FROM tags GROUP BY lower(name) HAVING count(*) > 1').to_ary.each do |row| canonical_tag_id = row['ids'].split(',').first redundant_tag_ids = row['ids'].split(',')[1..-1] diff --git a/lib/mastodon/migration_helpers.rb b/lib/mastodon/migration_helpers.rb index bf2314ecb..fcaa9259e 100644 --- a/lib/mastodon/migration_helpers.rb +++ b/lib/mastodon/migration_helpers.rb @@ -319,7 +319,7 @@ module Mastodon count_arel = table.project(Arel.star.count.as('count')) count_arel = yield table, count_arel if block_given? - total = exec_query(count_arel.to_sql).to_hash.first['count'].to_i + total = exec_query(count_arel.to_sql).to_ary.first['count'].to_i return if total == 0 end @@ -335,7 +335,7 @@ module Mastodon start_arel = table.project(table[:id]).order(table[:id].asc).take(1) start_arel = yield table, start_arel if block_given? - first_row = exec_query(start_arel.to_sql).to_hash.first + first_row = exec_query(start_arel.to_sql).to_ary.first # In case there are no rows but we didn't catch it in the estimated size: return unless first_row start_id = first_row['id'].to_i @@ -356,7 +356,7 @@ module Mastodon .skip(batch_size) stop_arel = yield table, stop_arel if block_given? - stop_row = exec_query(stop_arel.to_sql).to_hash.first + stop_row = exec_query(stop_arel.to_sql).to_ary.first update_arel = Arel::UpdateManager.new .table(table) diff --git a/spec/requests/link_headers_spec.rb b/spec/requests/link_headers_spec.rb index 712ee262b..c32e0f79a 100644 --- a/spec/requests/link_headers_spec.rb +++ b/spec/requests/link_headers_spec.rb @@ -25,7 +25,7 @@ describe 'Link headers' do end def link_header_with_type(type) - response.headers['Link'].links.find do |link| + LinkHeader.parse(response.headers['Link'].to_s).links.find do |link| link.attr_pairs.any? { |pair| pair == ['type', type] } end end -- cgit From 82caed594c3ad2cafe0c83b814879f30942fe57b Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 19 Mar 2021 11:07:56 +0100 Subject: Change deduplication order of tootctl maintenance fix-duplicates (#15923) Hopefully fixes #15922 Also update support up to latest database schema version --- lib/mastodon/maintenance_cli.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/mastodon/maintenance_cli.rb b/lib/mastodon/maintenance_cli.rb index 029d42a05..9f1eaf263 100644 --- a/lib/mastodon/maintenance_cli.rb +++ b/lib/mastodon/maintenance_cli.rb @@ -14,7 +14,7 @@ module Mastodon end MIN_SUPPORTED_VERSION = 2019_10_01_213028 - MAX_SUPPORTED_VERSION = 2020_12_18_054746 + MAX_SUPPORTED_VERSION = 2021_03_08_133107 # Stubs to enjoy ActiveRecord queries while not depending on a particular # version of the code/database @@ -142,7 +142,6 @@ module Mastodon @prompt.warn 'Please make sure to stop Mastodon and have a backup.' exit(1) unless @prompt.yes?('Continue?') - deduplicate_accounts! deduplicate_users! deduplicate_account_domain_blocks! deduplicate_account_identity_proofs! @@ -157,6 +156,7 @@ module Mastodon deduplicate_media_attachments! deduplicate_preview_cards! deduplicate_statuses! + deduplicate_accounts! deduplicate_tags! deduplicate_webauthn_credentials! -- cgit From c31c95ffe4fbf80981a0ee03484d72ee6d75d2ee Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 19 Mar 2021 13:14:40 +0100 Subject: Remove MySQL-specific code from Mastodon::MigrationHelpers (#15924) Mastodon::MigrationHelpers has been forked from Gitlab a long time ago, but Mastodon has never supported using a MySQL database. Removing MySQL support from Mastodon::MigrationHelpers makes it a little easier to maintain. In particular, it removes code that would need updating with Rails 6. --- lib/mastodon/migration_helpers.rb | 84 +++++++-------------------------------- 1 file changed, 14 insertions(+), 70 deletions(-) (limited to 'lib') diff --git a/lib/mastodon/migration_helpers.rb b/lib/mastodon/migration_helpers.rb index fcaa9259e..147642a1c 100644 --- a/lib/mastodon/migration_helpers.rb +++ b/lib/mastodon/migration_helpers.rb @@ -41,42 +41,18 @@ module Mastodon module MigrationHelpers - # Stub for Database.postgresql? from GitLab - def self.postgresql? - ActiveRecord::Base.configurations[Rails.env]['adapter'].casecmp('postgresql').zero? - end - - # Stub for Database.mysql? from GitLab - def self.mysql? - ActiveRecord::Base.configurations[Rails.env]['adapter'].casecmp('mysql2').zero? - end - # Model that can be used for querying permissions of a SQL user. class Grant < ActiveRecord::Base - self.table_name = - if Mastodon::MigrationHelpers.postgresql? - 'information_schema.role_table_grants' - else - 'mysql.user' - end + self.table_name = 'information_schema.role_table_grants' def self.scope_to_current_user - if Mastodon::MigrationHelpers.postgresql? - where('grantee = user') - else - where("CONCAT(User, '@', Host) = current_user()") - end + where('grantee = user') end # Returns true if the current user can create and execute triggers on the # given table. def self.create_and_execute_trigger?(table) - priv = - if Mastodon::MigrationHelpers.postgresql? - where(privilege_type: 'TRIGGER', table_name: table) - else - where(Trigger_priv: 'Y') - end + priv = where(privilege_type: 'TRIGGER', table_name: table) priv.scope_to_current_user.any? end @@ -141,10 +117,8 @@ module Mastodon 'in the body of your migration class' end - if MigrationHelpers.postgresql? - options = options.merge({ algorithm: :concurrently }) - disable_statement_timeout - end + options = options.merge({ algorithm: :concurrently }) + disable_statement_timeout add_index(table_name, column_name, options) end @@ -199,8 +173,6 @@ module Mastodon # Only available on Postgresql >= 9.2 def supports_drop_index_concurrently? - return false unless MigrationHelpers.postgresql? - version = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i version >= 90200 @@ -226,13 +198,7 @@ module Mastodon # While MySQL does allow disabling of foreign keys it has no equivalent # of PostgreSQL's "VALIDATE CONSTRAINT". As a result we'll just fall # back to the normal foreign key procedure. - if MigrationHelpers.mysql? - return add_foreign_key(source, target, - column: column, - on_delete: on_delete) - else - on_delete = 'SET NULL' if on_delete == :nullify - end + on_delete = 'SET NULL' if on_delete == :nullify disable_statement_timeout @@ -270,7 +236,7 @@ module Mastodon # the database. Disable the session's statement timeout to ensure # migrations don't get killed prematurely. (PostgreSQL only) def disable_statement_timeout - execute('SET statement_timeout TO 0') if MigrationHelpers.postgresql? + execute('SET statement_timeout TO 0') end # Updates the value of a column in batches. @@ -487,11 +453,7 @@ module Mastodon # If we were in the middle of update_column_in_batches, we should remove # the old column and start over, as we have no idea where we were. if column_for(table, new) - if MigrationHelpers.postgresql? - remove_rename_triggers_for_postgresql(table, trigger_name) - else - remove_rename_triggers_for_mysql(trigger_name) - end + remove_rename_triggers_for_postgresql(table, trigger_name) remove_column(table, new) end @@ -521,13 +483,8 @@ module Mastodon quoted_old = quote_column_name(old) quoted_new = quote_column_name(new) - if MigrationHelpers.postgresql? - install_rename_triggers_for_postgresql(trigger_name, quoted_table, - quoted_old, quoted_new) - else - install_rename_triggers_for_mysql(trigger_name, quoted_table, - quoted_old, quoted_new) - end + install_rename_triggers_for_postgresql(trigger_name, quoted_table, + quoted_old, quoted_new) update_column_in_batches(table, new, Arel::Table.new(table)[old]) @@ -685,11 +642,7 @@ module Mastodon check_trigger_permissions!(table) - if MigrationHelpers.postgresql? - remove_rename_triggers_for_postgresql(table, trigger_name) - else - remove_rename_triggers_for_mysql(trigger_name) - end + remove_rename_triggers_for_postgresql(table, trigger_name) remove_column(table, old) end @@ -844,18 +797,9 @@ module Mastodon quoted_pattern = Arel::Nodes::Quoted.new(pattern.to_s) quoted_replacement = Arel::Nodes::Quoted.new(replacement.to_s) - if MigrationHelpers.mysql? - locate = Arel::Nodes::NamedFunction - .new('locate', [quoted_pattern, column]) - insert_in_place = Arel::Nodes::NamedFunction - .new('insert', [column, locate, pattern.size, quoted_replacement]) - - Arel::Nodes::SqlLiteral.new(insert_in_place.to_sql) - else - replace = Arel::Nodes::NamedFunction - .new("regexp_replace", [column, quoted_pattern, quoted_replacement]) - Arel::Nodes::SqlLiteral.new(replace.to_sql) - end + replace = Arel::Nodes::NamedFunction + .new("regexp_replace", [column, quoted_pattern, quoted_replacement]) + Arel::Nodes::SqlLiteral.new(replace.to_sql) end def remove_foreign_key_without_error(*args) -- cgit From 741d0952b174740e70a09fe6db6862624dfe1e44 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 19 Mar 2021 13:14:57 +0100 Subject: Improve account counters handling (#15913) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improve account counters handling * Use ActiveRecord::Base::sanitize_sql to pass values instead of interpolating them Keep using string interpolation for `key` as it is safe and using “ActiveRecord::Base::sanitize_sql_hash_for_assignment” would require stitching bits of SQL in a way that is not more easily checked for safety. * Add migration hook to catch PostgreSQL versions earlier than 9.5 --- app/models/account_stat.rb | 42 ------------------- app/models/concerns/account_counters.rb | 60 ++++++++++++++++++++++++++- lib/tasks/db.rake | 8 +++- spec/models/account_stat_spec.rb | 57 ------------------------- spec/models/concerns/account_counters_spec.rb | 60 +++++++++++++++++++++++++++ 5 files changed, 125 insertions(+), 102 deletions(-) delete mode 100644 spec/models/account_stat_spec.rb create mode 100644 spec/models/concerns/account_counters_spec.rb (limited to 'lib') diff --git a/app/models/account_stat.rb b/app/models/account_stat.rb index e70b54d79..a826a9af3 100644 --- a/app/models/account_stat.rb +++ b/app/models/account_stat.rb @@ -18,46 +18,4 @@ class AccountStat < ApplicationRecord belongs_to :account, inverse_of: :account_stat update_index('accounts#account', :account) - - def increment_count!(key) - update(attributes_for_increment(key)) - rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotUnique - begin - reload_with_id - rescue ActiveRecord::RecordNotFound - return - end - - retry - end - - def decrement_count!(key) - update(attributes_for_decrement(key)) - rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotUnique - begin - reload_with_id - rescue ActiveRecord::RecordNotFound - return - end - - retry - end - - private - - def attributes_for_increment(key) - attrs = { key => public_send(key) + 1 } - attrs[:last_status_at] = Time.now.utc if key == :statuses_count - attrs - end - - def attributes_for_decrement(key) - attrs = { key => [public_send(key) - 1, 0].max } - attrs - end - - def reload_with_id - self.id = self.class.find_by!(account: account).id if new_record? - reload - end end diff --git a/app/models/concerns/account_counters.rb b/app/models/concerns/account_counters.rb index 6e25e1905..fd3f161ad 100644 --- a/app/models/concerns/account_counters.rb +++ b/app/models/concerns/account_counters.rb @@ -3,6 +3,8 @@ module AccountCounters extend ActiveSupport::Concern + ALLOWED_COUNTER_KEYS = %i(statuses_count following_count followers_count).freeze + included do has_one :account_stat, inverse_of: :account after_save :save_account_stat @@ -14,11 +16,65 @@ module AccountCounters :following_count=, :followers_count, :followers_count=, - :increment_count!, - :decrement_count!, :last_status_at, to: :account_stat + # @param [Symbol] key + def increment_count!(key) + update_count!(key, 1) + end + + # @param [Symbol] key + def decrement_count!(key) + update_count!(key, -1) + end + + # @param [Symbol] key + # @param [Integer] value + def update_count!(key, value) + raise ArgumentError, "Invalid key #{key}" unless ALLOWED_COUNTER_KEYS.include?(key) + raise ArgumentError, 'Do not call update_count! on dirty objects' if association(:account_stat).loaded? && account_stat&.changed? && account_stat.changed_attribute_names_to_save == %w(id) + + value = value.to_i + default_value = value.positive? ? value : 0 + + # We do an upsert using manually written SQL, as Rails' upsert method does + # not seem to support writing expressions in the UPDATE clause, but only + # re-insert the provided values instead. + # Even ARel seem to be missing proper handling of upserts. + sql = if value.positive? && key == :statuses_count + <<-SQL.squish + INSERT INTO account_stats(account_id, #{key}, created_at, updated_at, last_status_at) + VALUES (:account_id, :default_value, now(), now(), now()) + ON CONFLICT (account_id) DO UPDATE + SET #{key} = account_stats.#{key} + :value, + last_status_at = now(), + lock_version = account_stats.lock_version + 1, + updated_at = now() + RETURNING id; + SQL + else + <<-SQL.squish + INSERT INTO account_stats(account_id, #{key}, created_at, updated_at) + VALUES (:account_id, :default_value, now(), now()) + ON CONFLICT (account_id) DO UPDATE + SET #{key} = account_stats.#{key} + :value, + lock_version = account_stats.lock_version + 1, + updated_at = now() + RETURNING id; + SQL + end + + sql = AccountStat.sanitize_sql([sql, account_id: id, default_value: default_value, value: value]) + account_stat_id = AccountStat.connection.exec_query(sql)[0]['id'] + + # Reload account_stat if it was loaded, taking into account newly-created unsaved records + if association(:account_stat).loaded? + account_stat.id = account_stat_id if account_stat.new_record? + account_stat.reload + end + end + def account_stat super || build_account_stat end diff --git a/lib/tasks/db.rake b/lib/tasks/db.rake index 552a02b3f..7e6c1c8fc 100644 --- a/lib/tasks/db.rake +++ b/lib/tasks/db.rake @@ -19,7 +19,7 @@ namespace :db do task :post_migration_hook do at_exit do - unless %w(C POSIX).include?(ActiveRecord::Base.connection.execute('SELECT datcollate FROM pg_database WHERE datname = current_database();').first['datcollate']) + unless %w(C POSIX).include?(ActiveRecord::Base.connection.select_one('SELECT datcollate FROM pg_database WHERE datname = current_database();')['datcollate']) warn <<~WARNING Your database collation is susceptible to index corruption. (This warning does not indicate that index corruption has occured and can be ignored) @@ -29,5 +29,11 @@ namespace :db do end end + task :pre_migration_check do + version = ActiveRecord::Base.connection.select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i + abort 'ERROR: This version of Mastodon requires PostgreSQL 9.5 or newer. Please update PostgreSQL before updating Mastodon.' if version < 90_500 + end + + Rake::Task['db:migrate'].enhance(['db:pre_migration_check']) Rake::Task['db:migrate'].enhance(['db:post_migration_hook']) end diff --git a/spec/models/account_stat_spec.rb b/spec/models/account_stat_spec.rb deleted file mode 100644 index 8adc0d1d6..000000000 --- a/spec/models/account_stat_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'rails_helper' - -RSpec.describe AccountStat, type: :model do - describe '#increment_count!' do - it 'increments the count' do - account_stat = AccountStat.create(account: Fabricate(:account)) - expect(account_stat.followers_count).to eq 0 - account_stat.increment_count!(:followers_count) - expect(account_stat.followers_count).to eq 1 - end - - it 'increments the count in multi-threaded an environment' do - account_stat = AccountStat.create(account: Fabricate(:account), statuses_count: 0) - increment_by = 15 - wait_for_start = true - - threads = Array.new(increment_by) do - Thread.new do - true while wait_for_start - AccountStat.find(account_stat.id).increment_count!(:statuses_count) - end - end - - wait_for_start = false - threads.each(&:join) - - expect(account_stat.reload.statuses_count).to eq increment_by - end - end - - describe '#decrement_count!' do - it 'decrements the count' do - account_stat = AccountStat.create(account: Fabricate(:account), followers_count: 15) - expect(account_stat.followers_count).to eq 15 - account_stat.decrement_count!(:followers_count) - expect(account_stat.followers_count).to eq 14 - end - - it 'decrements the count in multi-threaded an environment' do - account_stat = AccountStat.create(account: Fabricate(:account), statuses_count: 15) - decrement_by = 10 - wait_for_start = true - - threads = Array.new(decrement_by) do - Thread.new do - true while wait_for_start - AccountStat.find(account_stat.id).decrement_count!(:statuses_count) - end - end - - wait_for_start = false - threads.each(&:join) - - expect(account_stat.reload.statuses_count).to eq 5 - end - end -end diff --git a/spec/models/concerns/account_counters_spec.rb b/spec/models/concerns/account_counters_spec.rb new file mode 100644 index 000000000..4350496e7 --- /dev/null +++ b/spec/models/concerns/account_counters_spec.rb @@ -0,0 +1,60 @@ +require 'rails_helper' + +describe AccountCounters do + let!(:account) { Fabricate(:account) } + + describe '#increment_count!' do + it 'increments the count' do + expect(account.followers_count).to eq 0 + account.increment_count!(:followers_count) + expect(account.followers_count).to eq 1 + end + + it 'increments the count in multi-threaded an environment' do + increment_by = 15 + wait_for_start = true + + threads = Array.new(increment_by) do + Thread.new do + true while wait_for_start + account.increment_count!(:statuses_count) + end + end + + wait_for_start = false + threads.each(&:join) + + expect(account.statuses_count).to eq increment_by + end + end + + describe '#decrement_count!' do + it 'decrements the count' do + account.followers_count = 15 + account.save! + expect(account.followers_count).to eq 15 + account.decrement_count!(:followers_count) + expect(account.followers_count).to eq 14 + end + + it 'decrements the count in multi-threaded an environment' do + decrement_by = 10 + wait_for_start = true + + account.statuses_count = 15 + account.save! + + threads = Array.new(decrement_by) do + Thread.new do + true while wait_for_start + account.decrement_count!(:statuses_count) + end + end + + wait_for_start = false + threads.each(&:join) + + expect(account.statuses_count).to eq 5 + end + end +end -- cgit