diff --git a/.github/workflows/build-releases.yml b/.github/workflows/build-releases.yml index 3b82eef9d..b1b6744a8 100644 --- a/.github/workflows/build-releases.yml +++ b/.github/workflows/build-releases.yml @@ -22,7 +22,7 @@ jobs: # Only tag with latest when ran against the latest stable branch # This needs to be updated after each minor version release flavor: | - latest=${{ startsWith(github.ref, 'refs/tags/v4.2.') }} + latest=false tags: | type=pep440,pattern={{raw}} type=pep440,pattern=v{{major}}.{{minor}} diff --git a/CHANGELOG.md b/CHANGELOG.md index f3e364320..1a278646e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project will be documented in this file. +## [4.2.14] - 2024-02-03 + +### Added + +- Add `tootctl feeds vacuum` (#33065 by @ClearlyClaire) + +### Fixed + +- Fix inactive users' timelines being backfilled on follow and unsuspend (#33094 by @ClearlyClaire) +- Fix direct inbox delivery pushing posts into inactive followers' timelines (#33067 by @ClearlyClaire) +- Fix `TagFollow` records not being correctly handled in account operations (#33063 by @ClearlyClaire) +- Fix pushing hashtag-followed posts to feeds of inactive users (#33018 by @Gargron) +- Fix and improve batch attachment deletion handling when using OpenStack Swift (#32637 by @hugogameiro) +- Fix tl language native name (#32606 by @seav) + +### Security + +- Update dependencies + ## [4.2.13] - 2024-09-30 ### Security diff --git a/Gemfile.lock b/Gemfile.lock index 4c9345e9a..5c3cf1c56 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -28,47 +28,47 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8.4) - actionpack (= 7.0.8.4) - activesupport (= 7.0.8.4) + actioncable (7.0.8.6) + actionpack (= 7.0.8.6) + activesupport (= 7.0.8.6) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.4) - actionpack (= 7.0.8.4) - activejob (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionmailbox (7.0.8.6) + actionpack (= 7.0.8.6) + activejob (= 7.0.8.6) + activerecord (= 7.0.8.6) + activestorage (= 7.0.8.6) + activesupport (= 7.0.8.6) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8.4) - actionpack (= 7.0.8.4) - actionview (= 7.0.8.4) - activejob (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionmailer (7.0.8.6) + actionpack (= 7.0.8.6) + actionview (= 7.0.8.6) + activejob (= 7.0.8.6) + activesupport (= 7.0.8.6) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.8.4) - actionview (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionpack (7.0.8.6) + actionview (= 7.0.8.6) + activesupport (= 7.0.8.6) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8.4) - actionpack (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + actiontext (7.0.8.6) + actionpack (= 7.0.8.6) + activerecord (= 7.0.8.6) + activestorage (= 7.0.8.6) + activesupport (= 7.0.8.6) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.4) - activesupport (= 7.0.8.4) + actionview (7.0.8.6) + activesupport (= 7.0.8.6) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -78,22 +78,22 @@ GEM activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (7.0.8.4) - activesupport (= 7.0.8.4) + activejob (7.0.8.6) + activesupport (= 7.0.8.6) globalid (>= 0.3.6) - activemodel (7.0.8.4) - activesupport (= 7.0.8.4) - activerecord (7.0.8.4) - activemodel (= 7.0.8.4) - activesupport (= 7.0.8.4) - activestorage (7.0.8.4) - actionpack (= 7.0.8.4) - activejob (= 7.0.8.4) - activerecord (= 7.0.8.4) - activesupport (= 7.0.8.4) + activemodel (7.0.8.6) + activesupport (= 7.0.8.6) + activerecord (7.0.8.6) + activemodel (= 7.0.8.6) + activesupport (= 7.0.8.6) + activestorage (7.0.8.6) + actionpack (= 7.0.8.6) + activejob (= 7.0.8.6) + activerecord (= 7.0.8.6) + activesupport (= 7.0.8.6) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.8.4) + activesupport (7.0.8.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -350,7 +350,7 @@ GEM httplog (1.6.2) rack (>= 2.0) rainbow (>= 2.0.0) - i18n (1.14.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) i18n-tasks (1.0.12) activesupport (>= 4.0.2) @@ -446,7 +446,7 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2023.0808) mini_mime (1.1.5) - mini_portile2 (2.8.7) + mini_portile2 (2.8.8) minitest (5.19.0) msgpack (1.7.1) multi_json (1.15.0) @@ -468,7 +468,7 @@ GEM net-smtp (0.3.4) net-protocol net-ssh (7.1.0) - nio4r (2.7.3) + nio4r (2.7.4) nokogiri (1.16.7) mini_portile2 (~> 2.8.2) racc (~> 1.4) @@ -533,7 +533,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.9) + rack (2.2.10) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-cors (2.0.2) @@ -550,20 +550,20 @@ GEM rack rack-test (2.1.0) rack (>= 1.3) - rails (7.0.8.4) - actioncable (= 7.0.8.4) - actionmailbox (= 7.0.8.4) - actionmailer (= 7.0.8.4) - actionpack (= 7.0.8.4) - actiontext (= 7.0.8.4) - actionview (= 7.0.8.4) - activejob (= 7.0.8.4) - activemodel (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + rails (7.0.8.6) + actioncable (= 7.0.8.6) + actionmailbox (= 7.0.8.6) + actionmailer (= 7.0.8.6) + actionpack (= 7.0.8.6) + actiontext (= 7.0.8.6) + actionview (= 7.0.8.6) + activejob (= 7.0.8.6) + activemodel (= 7.0.8.6) + activerecord (= 7.0.8.6) + activestorage (= 7.0.8.6) + activesupport (= 7.0.8.6) bundler (>= 1.15.0) - railties (= 7.0.8.4) + railties (= 7.0.8.6) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -578,9 +578,9 @@ GEM rails-i18n (7.0.7) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.0.8.4) - actionpack (= 7.0.8.4) - activesupport (= 7.0.8.4) + railties (7.0.8.6) + actionpack (= 7.0.8.6) + activesupport (= 7.0.8.6) method_source rake (>= 12.2) thor (~> 1.0) @@ -604,7 +604,7 @@ GEM responders (3.1.0) actionpack (>= 5.2) railties (>= 5.2) - rexml (3.3.7) + rexml (3.3.9) rotp (6.3.0) rouge (4.1.2) rpam2 (4.0.2) @@ -741,9 +741,9 @@ GEM terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) test-prof (1.2.3) - thor (1.3.1) + thor (1.3.2) tilt (2.2.0) - timeout (0.4.1) + timeout (0.4.2) tpm-key_attestation (0.12.0) bindata (~> 2.4) openssl (> 2.0) @@ -807,7 +807,7 @@ GEM xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.16) + zeitwerk (2.6.18) PLATFORMS ruby diff --git a/SECURITY.md b/SECURITY.md index 81472b01b..43ab4454c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ If you believe you've identified a security vulnerability in Mastodon (a bug that allows something to happen that shouldn't be possible), you can either: -- open a [Github security issue on the Mastodon project](https://github.com/mastodon/mastodon/security/advisories/new) +- open a [GitHub security issue on the Mastodon project](https://github.com/mastodon/mastodon/security/advisories/new) - reach us at You should _not_ report such issues on public GitHub issues or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk. @@ -13,8 +13,9 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through ## Supported Versions -| Version | Supported | -| ------- | --------- | -| 4.2.x | Yes | -| 4.1.x | Yes | -| < 4.1 | No | +| Version | Supported | +| ------- | ---------------- | +| 4.3.x | Yes | +| 4.2.x | Yes | +| 4.1.x | Until 2025-04-08 | +| < 4.1 | No | diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index c42c4c23e..f460c8d42 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -161,7 +161,7 @@ module LanguagesHelper th: ['Thai', 'ไทย'].freeze, ti: ['Tigrinya', 'ትግርኛ'].freeze, tk: ['Turkmen', 'Türkmen'].freeze, - tl: ['Tagalog', 'Wikang Tagalog'].freeze, + tl: ['Tagalog', 'Tagalog'].freeze, tn: ['Tswana', 'Setswana'].freeze, to: ['Tonga', 'faka Tonga'].freeze, tr: ['Turkish', 'Türkçe'].freeze, diff --git a/app/lib/attachment_batch.rb b/app/lib/attachment_batch.rb index 13a9da828..c01abe596 100644 --- a/app/lib/attachment_batch.rb +++ b/app/lib/attachment_batch.rb @@ -76,10 +76,22 @@ class AttachmentBatch when :fog logger.debug { "Deleting #{attachment.path(style)}" } + retries = 0 begin attachment.send(:directory).files.new(key: attachment.path(style)).destroy - rescue Fog::Storage::OpenStack::NotFound - # Ignore failure to delete a file that has already been deleted + rescue Fog::OpenStack::Storage::NotFound + logger.debug "Will ignore because file is not found #{attachment.path(style)}" + rescue => e + retries += 1 + + if retries < MAX_RETRY + logger.debug "Retry #{retries}/#{MAX_RETRY} after #{e.message}" + sleep 2**retries + retry + else + logger.error "Batch deletion from fog failed after #{e.message}" + raise e + end end when :azure logger.debug { "Deleting #{attachment.path(style)}" } diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 32236f20d..f5aa3c9a2 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -18,7 +18,7 @@ class FeedManager # @yield [Account] # @return [void] def with_active_accounts(&block) - Account.joins(:user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).find_each(&block) + Account.joins(:user).merge(User.signed_in_recently).find_each(&block) end # Redis key of a feed @@ -58,6 +58,7 @@ class FeedManager # @param [Boolean] update # @return [Boolean] def push_to_home(account, status, update: false) + return false unless account.user&.signed_in_recently? return false unless add_to_feed(:home, account.id, status, aggregate_reblogs: account.user&.aggregates_reblogs?) trim(:home, account.id) @@ -83,7 +84,9 @@ class FeedManager # @param [Boolean] update # @return [Boolean] def push_to_list(list, status, update: false) - return false if filter_from_list?(status, list) || !add_to_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?) + return false if filter_from_list?(status, list) + return false unless list.account.user&.signed_in_recently? + return false unless add_to_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?) trim(:list, list.id) PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}", { 'update' => update }) if push_update_required?("timeline:list:#{list.id}") @@ -107,6 +110,8 @@ class FeedManager # @param [Account] into_account # @return [void] def merge_into_home(from_account, into_account) + return unless into_account.user&.signed_in_recently? + timeline_key = key(:home, into_account.id) aggregate = into_account.user&.aggregates_reblogs? query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4) @@ -133,6 +138,8 @@ class FeedManager # @param [List] list # @return [void] def merge_into_list(from_account, list) + return unless list.account.user&.signed_in_recently? + timeline_key = key(:list, list.id) aggregate = list.account.user&.aggregates_reblogs? query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4) diff --git a/app/lib/vacuum/feeds_vacuum.rb b/app/lib/vacuum/feeds_vacuum.rb index fb0b8a847..429215760 100644 --- a/app/lib/vacuum/feeds_vacuum.rb +++ b/app/lib/vacuum/feeds_vacuum.rb @@ -21,7 +21,7 @@ class Vacuum::FeedsVacuum end def inactive_users - User.confirmed.inactive + User.confirmed.not_signed_in_recently end def inactive_users_lists diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 74ba1695f..4c2fd8fa5 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -83,6 +83,9 @@ module AccountInteractions has_many :following, -> { order('follows.id desc') }, through: :active_relationships, source: :target_account has_many :followers, -> { order('follows.id desc') }, through: :passive_relationships, source: :account + # Hashtag follows + has_many :tag_follows, inverse_of: :account, dependent: :destroy + # Account notes has_many :account_notes, dependent: :destroy @@ -261,13 +264,13 @@ module AccountInteractions def followers_for_local_distribution followers.local .joins(:user) - .where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago) + .merge(User.signed_in_recently) end def lists_for_local_distribution scope = lists.joins(account: :user) scope.where.not(list_accounts: { follow_id: nil }).or(scope.where(account_id: id)) - .where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago) + .merge(User.signed_in_recently) end def remote_followers_hash(url) diff --git a/app/models/concerns/account_merging.rb b/app/models/concerns/account_merging.rb index 14e157a3d..1f0940f37 100644 --- a/app/models/concerns/account_merging.rb +++ b/app/models/concerns/account_merging.rb @@ -16,7 +16,7 @@ module AccountMerging Follow, FollowRequest, Block, Mute, AccountModerationNote, AccountPin, AccountStat, ListAccount, PollVote, Mention, AccountDeletionRequest, AccountNote, FollowRecommendationSuppression, - Appeal + Appeal, TagFollow ] owned_classes.each do |klass| diff --git a/app/models/tag_follow.rb b/app/models/tag_follow.rb index abe36cd17..528616c45 100644 --- a/app/models/tag_follow.rb +++ b/app/models/tag_follow.rb @@ -21,4 +21,6 @@ class TagFollow < ApplicationRecord accepts_nested_attributes_for :tag rate_limit by: :account, family: :follows + + scope :for_local_distribution, -> { joins(account: :user).merge(User.signed_in_recently) } end diff --git a/app/models/user.rb b/app/models/user.rb index 0c8d481c4..94d748453 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -110,14 +110,16 @@ class User < ApplicationRecord validates :confirm_password, absence: true, on: :create validate :validate_role_elevation + scope :account_not_suspended, -> { joins(:account).merge(Account.without_suspended) } scope :recent, -> { order(id: :desc) } scope :pending, -> { where(approved: false) } scope :approved, -> { where(approved: true) } scope :confirmed, -> { where.not(confirmed_at: nil) } scope :enabled, -> { where(disabled: false) } scope :disabled, -> { where(disabled: true) } - scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) } - scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended_at: nil }) } + scope :active, -> { confirmed.signed_in_recently.account_not_suspended } + scope :signed_in_recently, -> { where(current_sign_in_at: ACTIVE_DURATION.ago..) } + scope :not_signed_in_recently, -> { where(current_sign_in_at: ...ACTIVE_DURATION.ago) } scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) } scope :matches_ip, ->(value) { left_joins(:ips).where('user_ips.ip <<= ?', value).group('users.id') } scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) } @@ -160,6 +162,10 @@ class User < ApplicationRecord end end + def signed_in_recently? + current_sign_in_at.present? && current_sign_in_at >= ACTIVE_DURATION.ago + end + def confirmed? confirmed_at.present? end diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb index 190a72e5c..2e196333a 100644 --- a/app/services/delete_account_service.rb +++ b/app/services/delete_account_service.rb @@ -52,6 +52,7 @@ class DeleteAccountService < BaseService owned_lists scheduled_statuses status_pins + tag_follows ) ASSOCIATIONS_ON_DESTROY = %w( diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index f2a79c9fc..273133d48 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -94,7 +94,7 @@ class FanOutOnWriteService < BaseService end def deliver_to_hashtag_followers! - TagFollow.where(tag_id: @status.tags.map(&:id)).select(:id, :account_id).reorder(nil).find_in_batches do |follows| + TagFollow.for_local_distribution.where(tag_id: @status.tags.map(&:id)).select(:id, :account_id).reorder(nil).find_in_batches do |follows| FeedInsertWorker.push_bulk(follows) do |follow| [@status.id, follow.account_id, 'tags', { 'update' => update? }] end diff --git a/docker-compose.yml b/docker-compose.yml index 2645c9eeb..ad71f3725 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,7 @@ services: web: build: . - image: ghcr.io/mastodon/mastodon:v4.2.13 + image: ghcr.io/mastodon/mastodon:v4.2.14 restart: always env_file: .env.production command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" @@ -77,7 +77,7 @@ services: streaming: build: . - image: ghcr.io/mastodon/mastodon:v4.2.13 + image: ghcr.io/mastodon/mastodon:v4.2.14 restart: always env_file: .env.production command: node ./streaming @@ -95,7 +95,7 @@ services: sidekiq: build: . - image: ghcr.io/mastodon/mastodon:v4.2.13 + image: ghcr.io/mastodon/mastodon:v4.2.14 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/cli/feeds.rb b/lib/mastodon/cli/feeds.rb index 3467dd427..7ec1026b9 100644 --- a/lib/mastodon/cli/feeds.rb +++ b/lib/mastodon/cli/feeds.rb @@ -5,6 +5,7 @@ require_relative 'base' module Mastodon::CLI class Feeds < Base include Redisable + include DatabaseHelper option :all, type: :boolean, default: false option :concurrency, type: :numeric, default: 5, aliases: [:c] @@ -48,6 +49,38 @@ module Mastodon::CLI say('OK', :green) end + desc 'vacuum', 'Remove home feeds of inactive users from Redis' + long_desc <<-LONG_DESC + Running this task should not be needed in most cases, as Mastodon will + automatically clean up feeds from inactive accounts every day. + + However, this task is more aggressive in order to clean up feeds that + may have been missed because of bugs or database mishaps. + LONG_DESC + def vacuum + with_read_replica do + say('Deleting orphaned home feeds…') + redis.scan_each(match: 'feed:home:*').each_slice(1000) do |keys| + ids = keys.map { |key| key.split(':')[2] }.compact_blank + + known_ids = User.confirmed.signed_in_recently.where(account_id: ids).pluck(:account_id) + + keys_to_delete = keys.filter { |key| known_ids.exclude?(key.split(':')[2]&.to_i) } + redis.del(keys_to_delete) + end + + say('Deleting orphaned list feeds…') + redis.scan_each(match: 'feed:list:*').each_slice(1000) do |keys| + ids = keys.map { |key| key.split(':')[2] }.compact_blank + + known_ids = List.where(account_id: User.confirmed.signed_in_recently.select(:account_id)).where(id: ids).pluck(:id) + + keys_to_delete = keys.filter { |key| known_ids.exclude?(key.split(':')[2]&.to_i) } + redis.del(keys_to_delete) + end + end + end + private def active_user_accounts diff --git a/lib/mastodon/cli/maintenance.rb b/lib/mastodon/cli/maintenance.rb index c2a6802e1..14f75ece8 100644 --- a/lib/mastodon/cli/maintenance.rb +++ b/lib/mastodon/cli/maintenance.rb @@ -39,6 +39,7 @@ module Mastodon::CLI class Webhook < ApplicationRecord; end class BulkImport < ApplicationRecord; end class SoftwareUpdate < ApplicationRecord; end + class TagFollow < ApplicationRecord; end class PreviewCard < ApplicationRecord self.inheritance_column = false @@ -89,6 +90,7 @@ module Mastodon::CLI owned_classes << AccountIdentityProof if ActiveRecord::Base.connection.table_exists?(:account_identity_proofs) owned_classes << Appeal if ActiveRecord::Base.connection.table_exists?(:appeals) owned_classes << BulkImport if ActiveRecord::Base.connection.table_exists?(:bulk_imports) + owned_classes << TagFollow if ActiveRecord::Base.connection.table_exists?(:tag_follows) owned_classes.each do |klass| klass.where(account_id: other_account.id).find_each do |record| diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index d4d4a7381..cd98bcff8 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 13 + 14 end def default_prerelease diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f06150f02..6c557f8c9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -81,12 +81,36 @@ RSpec.describe User do end end - describe 'inactive' do - it 'returns a relation of inactive users' do - specified = Fabricate(:user, current_sign_in_at: 15.days.ago) - Fabricate(:user, current_sign_in_at: 6.days.ago) + describe 'signed_in_recently' do + it 'returns a relation of users who have signed in during the recent period' do + recent_sign_in_user = Fabricate(:user, current_sign_in_at: within_duration_window_days.ago) + Fabricate(:user, current_sign_in_at: exceed_duration_window_days.ago) - expect(described_class.inactive).to contain_exactly(specified) + expect(described_class.signed_in_recently) + .to contain_exactly(recent_sign_in_user) + end + end + + describe 'not_signed_in_recently' do + it 'returns a relation of users who have not signed in during the recent period' do + no_recent_sign_in_user = Fabricate(:user, current_sign_in_at: exceed_duration_window_days.ago) + Fabricate(:user, current_sign_in_at: within_duration_window_days.ago) + + expect(described_class.not_signed_in_recently) + .to contain_exactly(no_recent_sign_in_user) + end + end + + describe 'account_not_suspended' do + it 'returns with linked accounts that are not suspended' do + suspended_account = Fabricate(:account, suspended_at: 10.days.ago) + non_suspended_account = Fabricate(:account, suspended_at: nil) + suspended_user = Fabricate(:user, account: suspended_account) + non_suspended_user = Fabricate(:user, account: non_suspended_account) + + expect(described_class.account_not_suspended) + .to include(non_suspended_user) + .and not_include(suspended_user) end end @@ -111,6 +135,14 @@ RSpec.describe User do expect(described_class.matches_ip('2160:2160::/32')).to contain_exactly(user1) end end + + def exceed_duration_window_days + described_class::ACTIVE_DURATION + 2.days + end + + def within_duration_window_days + described_class::ACTIVE_DURATION - 2.days + end end describe 'blacklist' do diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 17067c58f..a36d919b9 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -196,6 +196,7 @@ RSpec::Sidekiq.configure do |config| end RSpec::Matchers.define_negated_matcher :not_change, :change +RSpec::Matchers.define_negated_matcher :not_include, :include def request_fixture(name) Rails.root.join('spec', 'fixtures', 'requests', name).read