Merge tag 'v4.1.11' into chinwag-4.1
This commit is contained in:
commit
fc58fc207f
22 changed files with 225 additions and 47 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -3,6 +3,23 @@ Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [4.1.11] - 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))
|
||||||
|
- Fix error and incorrect URLs in `/api/v1/accounts/:id/featured_tags` for remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27459))
|
||||||
|
- Fix report processing notice not mentioning the report number when performing a custom action ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27442))
|
||||||
|
- Fix some link anchors being recognized as hashtags ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27271), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27584))
|
||||||
|
|
||||||
## [4.1.10] - 2023-10-10
|
## [4.1.10] - 2023-10-10
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
13
SECURITY.md
13
SECURITY.md
|
@ -10,9 +10,10 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ------------------ |
|
| ------- | ---------------- |
|
||||||
| 4.1.x | Yes |
|
| 4.2.x | Yes |
|
||||||
| 4.0.x | Until 2023-10-31 |
|
| 4.1.x | Yes |
|
||||||
| 3.5.x | Until 2023-12-31 |
|
| 4.0.x | No |
|
||||||
| < 3.5 | No |
|
| 3.5.x | Until 2023-12-31 |
|
||||||
|
| < 3.5 | No |
|
||||||
|
|
|
@ -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
|
||||||
|
|
14
app/chewy/concerns/datetime_clamping_concern.rb
Normal file
14
app/chewy/concerns/datetime_clamping_concern.rb
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -21,7 +21,7 @@ module Admin
|
||||||
account_action.save!
|
account_action.save!
|
||||||
|
|
||||||
if account_action.with_report?
|
if account_action.with_report?
|
||||||
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: params[:report_id])
|
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: resource_params[:report_id])
|
||||||
else
|
else
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,8 +18,8 @@ class ActivityPub::LinkedDataSignature
|
||||||
|
|
||||||
return unless type == 'RsaSignature2017'
|
return unless type == 'RsaSignature2017'
|
||||||
|
|
||||||
creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
|
creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
|
||||||
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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -33,7 +33,7 @@ class Tag < ApplicationRecord
|
||||||
HASTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)'
|
HASTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)'
|
||||||
HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASTAG_LAST_SEQUENCE}"
|
HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASTAG_LAST_SEQUENCE}"
|
||||||
|
|
||||||
HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_PAT})/i
|
HASHTAG_RE = %r{(?<![=/)\w])#(#{HASHTAG_NAME_PAT})}i
|
||||||
HASHTAG_NAME_RE = /\A(#{HASHTAG_NAME_PAT})\z/i
|
HASHTAG_NAME_RE = /\A(#{HASHTAG_NAME_PAT})\z/i
|
||||||
HASHTAG_INVALID_CHARS_RE = /[^[:alnum:]#{HASHTAG_SEPARATORS}]/
|
HASHTAG_INVALID_CHARS_RE = /[^[:alnum:]#{HASHTAG_SEPARATORS}]/
|
||||||
|
|
||||||
|
|
|
@ -91,7 +91,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? && valid_locale?(status.language)
|
status.public_visibility? && status.account.discoverable? && !status.account.silenced? && !status.account.sensitized? && status.spoiler_text.blank? && !status.sensitive? && !status.reply? && valid_locale?(status.language)
|
||||||
end
|
end
|
||||||
|
|
||||||
def calculate_scores(statuses, at_time)
|
def calculate_scores(statuses, at_time)
|
||||||
|
|
|
@ -10,7 +10,9 @@ class REST::FeaturedTagSerializer < ActiveModel::Serializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def url
|
def url
|
||||||
short_account_tag_url(object.account, object.tag)
|
# The path is hardcoded because we have to deal with both local and
|
||||||
|
# remote users, which are different routes
|
||||||
|
account_with_domain_url(object.account, "tagged/#{object.tag.to_param}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def name
|
def name
|
||||||
|
|
|
@ -71,7 +71,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
|
||||||
|
|
|
@ -23,9 +23,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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -126,7 +126,7 @@ Rails.application.routes.draw do
|
||||||
get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status
|
get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/@:username_with_domain/(*any)', to: 'home#index', constraints: { username_with_domain: /([^\/])+?/ }, format: false
|
get '/@:username_with_domain/(*any)', to: 'home#index', constraints: { username_with_domain: %r{([^/])+?} }, as: :account_with_domain, format: false
|
||||||
get '/settings', to: redirect('/settings/profile')
|
get '/settings', to: redirect('/settings/profile')
|
||||||
|
|
||||||
namespace :settings do
|
namespace :settings do
|
||||||
|
|
|
@ -56,7 +56,7 @@ services:
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v4.1.10
|
image: ghcr.io/mastodon/mastodon:v4.1.11
|
||||||
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"
|
||||||
|
@ -77,7 +77,7 @@ services:
|
||||||
|
|
||||||
streaming:
|
streaming:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v4.1.10
|
image: ghcr.io/mastodon/mastodon:v4.1.11
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: node ./streaming
|
command: node ./streaming
|
||||||
|
@ -95,7 +95,7 @@ services:
|
||||||
|
|
||||||
sidekiq:
|
sidekiq:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v4.1.10
|
image: ghcr.io/mastodon/mastodon:v4.1.11
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec sidekiq
|
command: bundle exec sidekiq
|
||||||
|
|
|
@ -13,7 +13,7 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
def patch
|
def patch
|
||||||
10
|
11
|
||||||
end
|
end
|
||||||
|
|
||||||
def flags
|
def flags
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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_actor!).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 }
|
||||||
|
|
||||||
|
|
|
@ -31,44 +31,52 @@ RSpec.describe Tag do
|
||||||
expect(subject.match('https://en.wikipedia.org/wiki/Ghostbusters_(song)#Lawsuit')).to be_nil
|
expect(subject.match('https://en.wikipedia.org/wiki/Ghostbusters_(song)#Lawsuit')).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not match URLs with hashtag-like anchors after a numeral' do
|
||||||
|
expect(subject.match('https://gcc.gnu.org/bugzilla/show_bug.cgi?id=111895#c4')).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not match URLs with hashtag-like anchors after an empty query parameter' do
|
||||||
|
expect(subject.match('https://en.wikipedia.org/wiki/Ghostbusters_(song)?foo=#Lawsuit')).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
it 'matches #aesthetic' do
|
it 'matches #aesthetic' do
|
||||||
expect(subject.match('this is #aesthetic').to_s).to eq ' #aesthetic'
|
expect(subject.match('this is #aesthetic').to_s).to eq '#aesthetic'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'matches digits at the start' do
|
it 'matches digits at the start' do
|
||||||
expect(subject.match('hello #3d').to_s).to eq ' #3d'
|
expect(subject.match('hello #3d').to_s).to eq '#3d'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'matches digits in the middle' do
|
it 'matches digits in the middle' do
|
||||||
expect(subject.match('hello #l33ts35k').to_s).to eq ' #l33ts35k'
|
expect(subject.match('hello #l33ts35k').to_s).to eq '#l33ts35k'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'matches digits at the end' do
|
it 'matches digits at the end' do
|
||||||
expect(subject.match('hello #world2016').to_s).to eq ' #world2016'
|
expect(subject.match('hello #world2016').to_s).to eq '#world2016'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'matches underscores at the beginning' do
|
it 'matches underscores at the beginning' do
|
||||||
expect(subject.match('hello #_test').to_s).to eq ' #_test'
|
expect(subject.match('hello #_test').to_s).to eq '#_test'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'matches underscores at the end' do
|
it 'matches underscores at the end' do
|
||||||
expect(subject.match('hello #test_').to_s).to eq ' #test_'
|
expect(subject.match('hello #test_').to_s).to eq '#test_'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'matches underscores in the middle' do
|
it 'matches underscores in the middle' do
|
||||||
expect(subject.match('hello #one_two_three').to_s).to eq ' #one_two_three'
|
expect(subject.match('hello #one_two_three').to_s).to eq '#one_two_three'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'matches middle dots' do
|
it 'matches middle dots' do
|
||||||
expect(subject.match('hello #one·two·three').to_s).to eq ' #one·two·three'
|
expect(subject.match('hello #one·two·three').to_s).to eq '#one·two·three'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'matches ・unicode in ぼっち・ざ・ろっく correctly' do
|
it 'matches ・unicode in ぼっち・ざ・ろっく correctly' do
|
||||||
expect(subject.match('testing #ぼっち・ざ・ろっく').to_s).to eq ' #ぼっち・ざ・ろっく'
|
expect(subject.match('testing #ぼっち・ざ・ろっく').to_s).to eq '#ぼっち・ざ・ろっく'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'matches ZWNJ' do
|
it 'matches ZWNJ' do
|
||||||
expect(subject.match('just add #نرمافزار and').to_s).to eq ' #نرمافزار'
|
expect(subject.match('just add #نرمافزار and').to_s).to eq '#نرمافزار'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not match middle dots at the start' do
|
it 'does not match middle dots at the start' do
|
||||||
|
@ -76,7 +84,7 @@ RSpec.describe Tag do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not match middle dots at the end' do
|
it 'does not match middle dots at the end' do
|
||||||
expect(subject.match('hello #one·two·three·').to_s).to eq ' #one·two·three'
|
expect(subject.match('hello #one·two·three·').to_s).to eq '#one·two·three'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not match purely-numeric hashtags' do
|
it 'does not match purely-numeric hashtags' do
|
||||||
|
|
50
spec/requests/api/v1/accounts/featured_tags_spec.rb
Normal file
50
spec/requests/api/v1/accounts/featured_tags_spec.rb
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'account featured tags API' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
||||||
|
let(:scopes) { 'read:accounts' }
|
||||||
|
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
||||||
|
let(:account) { Fabricate(:account) }
|
||||||
|
|
||||||
|
describe 'GET /api/v1/accounts/:id/featured_tags' do
|
||||||
|
subject do
|
||||||
|
get "/api/v1/accounts/#{account.id}/featured_tags", headers: headers
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
account.featured_tags.create!(name: 'foo')
|
||||||
|
account.featured_tags.create!(name: 'bar')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the expected tags', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(body_as_json).to contain_exactly(a_hash_including({
|
||||||
|
name: 'bar',
|
||||||
|
url: "https://cb6e6126.ngrok.io/@#{account.username}/tagged/bar",
|
||||||
|
}), a_hash_including({
|
||||||
|
name: 'foo',
|
||||||
|
url: "https://cb6e6126.ngrok.io/@#{account.username}/tagged/foo",
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the account is remote' do
|
||||||
|
it 'returns the expected tags', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(body_as_json).to contain_exactly(a_hash_including({
|
||||||
|
name: 'bar',
|
||||||
|
url: "https://cb6e6126.ngrok.io/@#{account.pretty_acct}/tagged/bar",
|
||||||
|
}), a_hash_including({
|
||||||
|
name: 'foo',
|
||||||
|
url: "https://cb6e6126.ngrok.io/@#{account.pretty_acct}/tagged/foo",
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue