Merge security fixes of v3.5.17 (#1341)

_todo_

---------

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Co-authored-by: Essem <smswessem@gmail.com>
Co-authored-by: Jakob Gillich <jakob@gillich.me>
Co-authored-by: David Aaron <1858430+suddjian@users.noreply.github.com>
Co-authored-by: Matt Jankowski <matt@jankowski.online>
Co-authored-by: Jonathan de Jong <jonathandejong02@gmail.com>
This commit is contained in:
Jasmin 2024-02-01 16:37:20 +01:00 committed by GitHub
parent 1eaaff303c
commit 3fd984f95c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 240 additions and 75 deletions

View File

@ -5,8 +5,42 @@ All notable changes to this project will be documented in this file.
## End of life notice ## End of life notice
**The 3.5.x branch will not receive any update after 2023-12-31.** **The 3.5.x branch has reached its end of life and will not receive any further update.**
This means that no security fix will be made available for this branch after this date, and you will need to update to a more recent version (such as the 4.1.x branch) to receive security fixes. This means that no security fix will be made available for this branch after this date, and you will need to update to a more recent version (such as the 4.2.x branch) to receive security fixes.
## [3.5.17] - 2024-02-01
### Security
- Fix TODO
## [3.5.16] - 2023-12-04
### Changed
- Change GIF max matrix size error to explicitly mention GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27927))
- Change `Follow` activities delivery to bypass availability check ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/27586))
- Change Content-Security-Policy to be tighter on media paths ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26889))
### Fixed
- Fix incoming status creation date not being restricted to standard ISO8601 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27655), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28081))
- Fix posts from force-sensitized accounts being able to trend ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27620))
- Fix processing LDSigned activities from actors with unknown public keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27474))
## [3.5.15] - 2023-10-10
### Changed
- Change user archive export allowed period from 7 days to 6 days ([suddjian](https://github.com/mastodon/mastodon/pull/27200))
### Fixed
- Fix mentions being matched in some URL query strings ([mjankowski](https://github.com/mastodon/mastodon/pull/25656))
- Fix importer returning negative row estimates ([jgillich](https://github.com/mastodon/mastodon/pull/27258))
- Fix filtering audit log for entries about disabling 2FA ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27186))
- Fix tIME chunk not being properly removed from PNG uploads ([TheEssem](https://github.com/mastodon/mastodon/pull/27111))
- 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))
## [3.5.14] - 2023-09-19 ## [3.5.14] - 2023-09-19

View File

@ -10,11 +10,10 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
## Supported Versions ## Supported Versions
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ---------------- |
| 3.5.x | Until 2023-12-31 | | 4.2.x | Yes |
| 3.4.x | No | | 4.1.x | Yes |
| 3.3.x | No | | 4.0.x | No |
| < 3.3 | No | | 3.5.x | No |
| < 3.5 | No |
[bug-bounty]: https://app.intigriti.com/programs/mastodon/mastodonio/detail

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class AccountsIndex < Chewy::Index class AccountsIndex < Chewy::Index
include DatetimeClampingConcern
settings index: { refresh_interval: '30s' }, analysis: { settings index: { refresh_interval: '30s' }, analysis: {
analyzer: { analyzer: {
content: { content: {
@ -38,6 +40,6 @@ class AccountsIndex < Chewy::Index
field :following_count, type: 'long', value: ->(account) { account.following_count } field :following_count, type: 'long', value: ->(account) { account.following_count }
field :followers_count, type: 'long', value: ->(account) { account.followers_count } field :followers_count, type: 'long', value: ->(account) { account.followers_count }
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at } field :last_status_at, type: 'date', value: ->(account) { clamp_date(account.last_status_at || account.created_at) }
end end
end end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module DatetimeClampingConcern
extend ActiveSupport::Concern
MIN_ISO8601_DATETIME = '0000-01-01T00:00:00Z'.to_datetime.freeze
MAX_ISO8601_DATETIME = '9999-12-31T23:59:59Z'.to_datetime.freeze
class_methods do
def clamp_date(datetime)
datetime.clamp(MIN_ISO8601_DATETIME, MAX_ISO8601_DATETIME)
end
end
end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class TagsIndex < Chewy::Index class TagsIndex < Chewy::Index
include DatetimeClampingConcern
settings index: { refresh_interval: '30s' }, analysis: { settings index: { refresh_interval: '30s' }, analysis: {
analyzer: { analyzer: {
content: { content: {
@ -36,6 +38,6 @@ class TagsIndex < Chewy::Index
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? } field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
field :usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts } field :usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts }
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at } field :last_status_at, type: 'date', value: ->(tag) { clamp_date(tag.last_status_at || tag.created_at) }
end end
end end

View File

@ -18,8 +18,8 @@ class ActivityPub::LinkedDataSignature
return unless type == 'RsaSignature2017' return unless type == 'RsaSignature2017'
creator = ActivityPub::TagManager.instance.uri_to_resource(creator_uri, Account) creator = ActivityPub::TagManager.instance.uri_to_resource(creator_uri, Account)
creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) if creator&.public_key.blank?
return if creator.nil? return if creator.nil?
@ -27,9 +27,9 @@ class ActivityPub::LinkedDataSignature
document_hash = hash(@json.without('signature')) document_hash = hash(@json.without('signature'))
to_be_verified = options_hash + document_hash to_be_verified = options_hash + document_hash
if creator.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), to_be_verified) creator if creator.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), to_be_verified)
creator rescue OpenSSL::PKey::RSAError
end false
end end
def sign!(creator, sign_with: nil) def sign!(creator, sign_with: nil)

View File

@ -53,7 +53,8 @@ class ActivityPub::Parser::StatusParser
end end
def created_at def created_at
@object['published']&.to_datetime datetime = @object['published']&.to_datetime
datetime if datetime.present? && (0..9999).cover?(datetime.year)
rescue ArgumentError rescue ArgumentError
nil nil
end end

View File

@ -34,7 +34,9 @@ class Importer::BaseImporter
# Estimate the amount of documents that would be indexed. Not exact! # Estimate the amount of documents that would be indexed. Not exact!
# @returns [Integer] # @returns [Integer]
def estimate! def estimate!
ActiveRecord::Base.connection_pool.with_connection { |connection| connection.select_one("SELECT reltuples AS estimate FROM pg_class WHERE relname = '#{index.adapter.target.table_name}'")['estimate'].to_i } reltuples = ActiveRecord::Base.connection_pool.with_connection { |connection| connection.select_one("SELECT reltuples FROM pg_class WHERE relname = '#{index.adapter.target.table_name}'")['reltuples'].to_i }
# If the table has never yet been vacuumed or analyzed, reltuples contains -1
[reltuples, 0].max
end end
# Import data from the database into the index # Import data from the database into the index

View File

@ -61,9 +61,9 @@ class Account < ApplicationRecord
trust_level trust_level
) )
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i USERNAME_RE = /[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?/i
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[[:word:]]+)?)/i MENTION_RE = %r{(?<![=/[:word:]])@((#{USERNAME_RE})(?:@[[:word:].-]+[[:word:]]+)?)}i
URL_PREFIX_RE = /\Ahttp(s?):\/\/[^\/]+/ URL_PREFIX_RE = %r{\Ahttp(s?)://[^/]+}
include Attachmentable include Attachmentable
include AccountAssociations include AccountAssociations
@ -113,8 +113,8 @@ class Account < ApplicationRecord
scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) } scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) }
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) } scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) } scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) }
scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) } scope :by_recent_status, -> { includes(:account_stat).merge(AccountStat.order('last_status_at DESC NULLS LAST')).references(:account_stat) }
scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) } scope :by_recent_sign_in, -> { order(Arel.sql('users.current_sign_in_at DESC NULLS LAST')) }
scope :popular, -> { order('account_stats.followers_count desc') } scope :popular, -> { order('account_stats.followers_count desc') }
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) } scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) } scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }

View File

@ -31,7 +31,7 @@ class Admin::ActionLogFilter
destroy_instance: { target_type: 'Instance', action: 'destroy' }.freeze, destroy_instance: { target_type: 'Instance', action: 'destroy' }.freeze,
destroy_unavailable_domain: { target_type: 'UnavailableDomain', action: 'destroy' }.freeze, destroy_unavailable_domain: { target_type: 'UnavailableDomain', action: 'destroy' }.freeze,
destroy_status: { target_type: 'Status', action: 'destroy' }.freeze, destroy_status: { target_type: 'Status', action: 'destroy' }.freeze,
disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze, disable_2fa_user: { target_type: 'User', action: 'disable_2fa' }.freeze,
disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze, disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze,
disable_user: { target_type: 'User', action: 'disable' }.freeze, disable_user: { target_type: 'User', action: 'disable' }.freeze,
enable_custom_emoji: { target_type: 'CustomEmoji', action: 'enable' }.freeze, enable_custom_emoji: { target_type: 'CustomEmoji', action: 'enable' }.freeze,

View File

@ -52,9 +52,13 @@ module Attachmentable
return if attachment.blank? || !/image.*/.match?(attachment.content_type) || attachment.queued_for_write[:original].blank? return if attachment.blank? || !/image.*/.match?(attachment.content_type) || attachment.queued_for_write[:original].blank?
width, height = FastImage.size(attachment.queued_for_write[:original].path) width, height = FastImage.size(attachment.queued_for_write[:original].path)
matrix_limit = attachment.content_type == 'image/gif' ? GIF_MATRIX_LIMIT : MAX_MATRIX_LIMIT return unless width.present? && height.present?
raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported" if width.present? && height.present? && (width * height > matrix_limit) if attachment.content_type == 'image/gif' && width * height > GIF_MATRIX_LIMIT
raise Mastodon::DimensionsValidationError, "#{width}x#{height} GIF files are not supported"
elsif width * height > MAX_MATRIX_LIMIT
raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported"
end
end end
def appropriate_extension(attachment) def appropriate_extension(attachment)

View File

@ -156,7 +156,7 @@ class MediaAttachment < ApplicationRecord
}.freeze }.freeze
GLOBAL_CONVERT_OPTIONS = { GLOBAL_CONVERT_OPTIONS = {
all: '-quality 90 -strip +set modify-date +set create-date', all: '-quality 90 -strip +set date:modify +set date:create +set date:timestamp',
}.freeze }.freeze
belongs_to :account, inverse_of: :media_attachments, optional: true belongs_to :account, inverse_of: :media_attachments, optional: true

View File

@ -60,13 +60,13 @@ class RelationshipFilter
def relationship_scope(value) def relationship_scope(value)
case value case value
when 'following' when 'following'
account.following.eager_load(:account_stat).reorder(nil) account.following.includes(:account_stat).reorder(nil)
when 'followed_by' when 'followed_by'
account.followers.eager_load(:account_stat).reorder(nil) account.followers.includes(:account_stat).reorder(nil)
when 'mutual' when 'mutual'
account.followers.eager_load(:account_stat).reorder(nil).merge(Account.where(id: account.following)) account.followers.includes(:account_stat).reorder(nil).merge(Account.where(id: account.following))
when 'invited' when 'invited'
Account.joins(user: :invite).merge(Invite.where(user: account.user)).eager_load(:account_stat).reorder(nil) Account.joins(user: :invite).merge(Invite.where(user: account.user)).includes(:account_stat).reorder(nil)
else else
raise "Unknown relationship: #{value}" raise "Unknown relationship: #{value}"
end end
@ -112,7 +112,7 @@ class RelationshipFilter
def activity_scope(value) def activity_scope(value)
case value case value
when 'dormant' when 'dormant'
AccountStat.where(last_status_at: nil).or(AccountStat.where(AccountStat.arel_table[:last_status_at].lt(1.month.ago))) Account.joins(:account_stat).where(account_stat: { last_status_at: [nil, ...1.month.ago] })
else else
raise "Unknown activity: #{value}" raise "Unknown activity: #{value}"
end end

View File

@ -75,7 +75,7 @@ class Trends::Statuses < Trends::Base
private private
def eligible?(status) def eligible?(status)
status.public_visibility? && status.account.discoverable? && !status.account.silenced? && status.spoiler_text.blank? && !status.sensitive? && !status.reply? status.public_visibility? && status.account.discoverable? && !status.account.silenced? && !status.account.sensitized? && status.spoiler_text.blank? && !status.sensitive? && !status.reply?
end end
def calculate_scores(statuses, at_time) def calculate_scores(statuses, at_time)

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class BackupPolicy < ApplicationPolicy class BackupPolicy < ApplicationPolicy
MIN_AGE = 1.week MIN_AGE = 6.days
def create? def create?
user_signed_in? && current_user.backups.where('created_at >= ?', MIN_AGE.ago).count.zero? user_signed_in? && current_user.backups.where('created_at >= ?', MIN_AGE.ago).count.zero?

View File

@ -70,7 +70,7 @@ class FollowService < BaseService
if @target_account.local? if @target_account.local?
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request') LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request')
elsif @target_account.activitypub? elsif @target_account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url) ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url, { 'bypass_availability' => true })
end end
follow_request follow_request

View File

@ -13,9 +13,10 @@ class ActivityPub::DeliveryWorker
HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
def perform(json, source_account_id, inbox_url, options = {}) def perform(json, source_account_id, inbox_url, options = {})
return unless DeliveryFailureTracker.available?(inbox_url)
@options = options.with_indifferent_access @options = options.with_indifferent_access
return unless @options[:bypass_availability] || DeliveryFailureTracker.available?(inbox_url)
@json = json @json = json
@source_account = Account.find(source_account_id) @source_account = Account.find(source_account_id)
@inbox_url = inbox_url @inbox_url = inbox_url

View File

@ -3,7 +3,11 @@
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
def host_to_url(str) def host_to_url(str)
"http#{Rails.configuration.x.use_https ? 's' : ''}://#{str.split('/').first}" if str.present? return if str.blank?
uri = Addressable::URI.parse("http#{Rails.configuration.x.use_https ? 's' : ''}://#{str}")
uri.path += '/' unless uri.path.blank? || uri.path.end_with?('/')
uri.to_s
end end
base_host = Rails.configuration.x.web_domain base_host = Rails.configuration.x.web_domain

View File

@ -44,7 +44,7 @@ services:
web: web:
build: . build: .
image: ghcr.io/mastodon/mastodon:v3.5.14 image: ghcr.io/mastodon/mastodon:v3.5.17
restart: always restart: always
env_file: .env.production env_file: .env.production
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
@ -65,7 +65,7 @@ services:
streaming: streaming:
build: . build: .
image: ghcr.io/mastodon/mastodon:v3.5.14 image: ghcr.io/mastodon/mastodon:v3.5.17
restart: always restart: always
env_file: .env.production env_file: .env.production
command: node ./streaming command: node ./streaming
@ -83,7 +83,7 @@ services:
sidekiq: sidekiq:
build: . build: .
image: ghcr.io/mastodon/mastodon:v3.5.14 image: ghcr.io/mastodon/mastodon:v3.5.17
restart: always restart: always
env_file: .env.production env_file: .env.production
command: bundle exec sidekiq command: bundle exec sidekiq

View File

@ -13,7 +13,7 @@ module Mastodon
end end
def patch def patch
14 17
end end
def flags def flags

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
Fabricator(:account_stat) do Fabricator(:account_stat) do
account nil account { Fabricate.build(:account) }
statuses_count "" statuses_count '123'
following_count "" following_count '456'
followers_count "" followers_count '789'
end end

View File

@ -29,7 +29,47 @@ RSpec.describe ActivityPub::Activity::Create do
subject.perform subject.perform
end end
context 'object has been edited' do context 'when object publication date is below ISO8601 range' do
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
published: '-0977-11-03T08:31:22Z',
}
end
it 'creates status with a valid creation date', :aggregate_failures do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.text).to eq 'Lorem ipsum'
expect(status.created_at).to be_within(30).of(Time.now.utc)
end
end
context 'when object publication date is above ISO8601 range' do
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
published: '10000-11-03T08:31:22Z',
}
end
it 'creates status with a valid creation date', :aggregate_failures do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.text).to eq 'Lorem ipsum'
expect(status.created_at).to be_within(30).of(Time.now.utc)
end
end
context 'when object has been edited' do
let(:object_json) do let(:object_json) do
{ {
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
@ -40,18 +80,16 @@ RSpec.describe ActivityPub::Activity::Create do
} }
end end
it 'creates status' do it 'creates status with appropriate creation and edition dates', :aggregate_failures do
status = sender.statuses.first status = sender.statuses.first
expect(status).to_not be_nil expect(status).to_not be_nil
expect(status.text).to eq 'Lorem ipsum' expect(status.text).to eq 'Lorem ipsum'
end
it 'marks status as edited' do expect(status.created_at).to eq '2022-01-22T15:00:00Z'.to_datetime
status = sender.statuses.first
expect(status).to_not be_nil expect(status.edited?).to be true
expect(status.edited?).to eq true expect(status.edited_at).to eq '2022-01-22T16:00:00Z'.to_datetime
end end
end end

View File

@ -36,6 +36,40 @@ RSpec.describe ActivityPub::LinkedDataSignature do
end end
end end
context 'when local account record is missing a public key' do
let(:raw_signature) do
{
'creator' => 'http://example.com/alice',
'created' => '2017-09-23T20:21:34Z',
}
end
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
let(:service_stub) { instance_double(ActivityPub::FetchRemoteKeyService) }
before do
# Ensure signature is computed with the old key
signature
# Unset key
old_key = sender.public_key
sender.update!(private_key: '', public_key: '')
allow(ActivityPub::FetchRemoteKeyService).to receive(:new).and_return(service_stub)
allow(service_stub).to receive(:call).with('http://example.com/alice', id: false) do
sender.update!(public_key: old_key)
sender
end
end
it 'fetches key and returns creator' do
expect(subject.verify_account!).to eq sender
expect(service_stub).to have_received(:call).with('http://example.com/alice', id: false).once
end
end
context 'when signature is missing' do context 'when signature is missing' do
let(:signature) { nil } let(:signature) { nil }

View File

@ -689,7 +689,7 @@ RSpec.describe Account, type: :model do
expect(subject.match('Check this out https://medium.com/@alice/some-article#.abcdef123')).to be_nil expect(subject.match('Check this out https://medium.com/@alice/some-article#.abcdef123')).to be_nil
end end
xit 'does not match URL querystring' do it 'does not match URL query string' do
expect(subject.match('https://example.com/?x=@alice')).to be_nil expect(subject.match('https://example.com/?x=@alice')).to be_nil
end end
end end

View File

@ -6,32 +6,60 @@ describe RelationshipFilter do
let(:account) { Fabricate(:account) } let(:account) { Fabricate(:account) }
describe '#results' do describe '#results' do
context 'when default params are used' do let(:account_of_7_months) { Fabricate(:account_stat, statuses_count: 1, last_status_at: 7.months.ago).account }
let(:subject) do let(:account_of_1_day) { Fabricate(:account_stat, statuses_count: 1, last_status_at: 1.day.ago).account }
RelationshipFilter.new(account, 'order' => 'active').results let(:account_of_3_days) { Fabricate(:account_stat, statuses_count: 1, last_status_at: 3.days.ago).account }
let(:silent_account) { Fabricate(:account_stat, statuses_count: 0, last_status_at: nil).account }
before do
account.follow!(account_of_7_months)
account.follow!(account_of_1_day)
account.follow!(account_of_3_days)
account.follow!(silent_account)
end
context 'when ordering by last activity' do
context 'when not filtering' do
subject do
described_class.new(account, 'order' => 'active').results
end
it 'returns followings ordered by last activity' do
expect(subject).to eq [account_of_1_day, account_of_3_days, account_of_7_months, silent_account]
end
end end
before do context 'when filtering for dormant accounts' do
add_following_account_with(last_status_at: 7.days.ago) subject do
add_following_account_with(last_status_at: 1.day.ago) described_class.new(account, 'order' => 'active', 'activity' => 'dormant').results
add_following_account_with(last_status_at: 3.days.ago) end
it 'returns dormant followings ordered by last activity' do
expect(subject).to eq [account_of_7_months, silent_account]
end
end
end
context 'when ordering by account creation' do
context 'when not filtering' do
subject do
described_class.new(account, 'order' => 'recent').results
end
it 'returns followings ordered by last account creation' do
expect(subject).to eq [silent_account, account_of_3_days, account_of_1_day, account_of_7_months]
end
end end
it 'returns followings ordered by last activity' do context 'when filtering for dormant accounts' do
expected_result = account.following.eager_load(:account_stat).reorder(nil).by_recent_status subject do
described_class.new(account, 'order' => 'recent', 'activity' => 'dormant').results
end
expect(subject).to eq expected_result it 'returns dormant followings ordered by last activity' do
expect(subject).to eq [silent_account, account_of_7_months]
end
end end
end end
end end
def add_following_account_with(last_status_at:)
following_account = Fabricate(:account)
Fabricate(:account_stat, account: following_account,
last_status_at: last_status_at,
statuses_count: 1,
following_count: 0,
followers_count: 0)
Fabricate(:follow, account: account, target_account: following_account).account
end
end end