Commit 9e7decdc authored by Sander Snel's avatar Sander Snel

Merge remote-tracking branch 'upstream/master'

parents 0a5c57d2 e6d111f3
......@@ -3,6 +3,36 @@ Changelog
All notable changes to this project will be documented in this file.
## [3.0.1] - 2019-10-09
### Added
- Add `tootctl media usage` command ([Gargron](https://github.com/tootsuite/mastodon/pull/12115))
- Add admin setting to auto-approve trending hashtags ([Gargron](https://github.com/tootsuite/mastodon/pull/12122))
### Changed
- Change `tootctl media refresh` to skip already downloaded attachments ([Gargron](https://github.com/tootsuite/mastodon/pull/12118))
### Removed
- Remove auto-silence behaviour from spam check ([Gargron](https://github.com/tootsuite/mastodon/pull/12117))
- Remove HTML `lang` attribute from individual statuses in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/12124))
- Remove fallback to long description on sidebar and meta description ([Gargron](https://github.com/tootsuite/mastodon/pull/12119))
### Fixed
- Fix attachment not being re-downloaded even if file is not stored ([Gargron](https://github.com/tootsuite/mastodon/pull/12125))
- Fix old migration trying to use new column due to default status scope ([Gargron](https://github.com/tootsuite/mastodon/pull/12095))
- Fix column back button missing for not found accounts ([trwnh](https://github.com/tootsuite/mastodon/pull/12094))
- Fix issues with tootctl's parallelization and progress reporting ([Gargron](https://github.com/tootsuite/mastodon/pull/12093), [Gargron](https://github.com/tootsuite/mastodon/pull/12097))
- Fix existing user records with now-renamed `pt` locale ([Gargron](https://github.com/tootsuite/mastodon/pull/12092))
- Fix hashtag timeline REST API accepting too many hashtags ([Gargron](https://github.com/tootsuite/mastodon/pull/12091))
- Fix `GET /api/v1/instance` REST APIs being unavailable in secure mode ([Gargron](https://github.com/tootsuite/mastodon/pull/12089))
- Fix performance of home feed regeneration and merging ([Gargron](https://github.com/tootsuite/mastodon/pull/12084))
- Fix ffmpeg performance issues due to stdout buffer overflow ([hugogameiro](https://github.com/tootsuite/mastodon/pull/12088))
- Fix S3 adapter retrying failing uploads with exponential backoff ([Gargron](https://github.com/tootsuite/mastodon/pull/12085))
- Fix `tootctl accounts cull` advertising unused option flag ([Kjwon15](https://github.com/tootsuite/mastodon/pull/12074))
## [3.0.0] - 2019-10-03
### Added
......
......@@ -5,11 +5,17 @@ class Api::V1::StreamingController < Api::BaseController
def index
if Rails.configuration.x.streaming_api_base_url != request.host
uri = URI.parse(request.url)
uri.host = URI.parse(Rails.configuration.x.streaming_api_base_url).host
redirect_to uri.to_s, status: 301
redirect_to streaming_api_url, status: 301
else
raise ActiveRecord::RecordNotFound
not_found
end
end
private
def streaming_api_url
Addressable::URI.parse(request.url).tap do |uri|
uri.host = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url).host
end.to_s
end
end
......@@ -216,14 +216,14 @@ export default class StatusContent extends React.PureComponent {
return (
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
<span dangerouslySetInnerHTML={spoilerContent} lang={status.get('language')} />
<span dangerouslySetInnerHTML={spoilerContent} />
{' '}
<button tabIndex='0' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick}>{toggleText}</button>
</p>
{mentionsPlaceholder}
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} />
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
{!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
</div>
......@@ -231,7 +231,7 @@ export default class StatusContent extends React.PureComponent {
} else if (this.props.onClick) {
const output = [
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content'>
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} />
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} />
{!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
</div>,
......@@ -245,7 +245,7 @@ export default class StatusContent extends React.PureComponent {
} else {
return (
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle}>
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} />
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} />
{!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
</div>
......
......@@ -44,7 +44,6 @@ class SpamCheck
end
def flag!
auto_silence_account!
auto_report_status!
end
......@@ -134,17 +133,13 @@ class SpamCheck
text.gsub(/\s+/, ' ').strip
end
def auto_silence_account!
@account.silence!
end
def auto_report_status!
status_ids = Status.where(visibility: %i(public unlisted)).where(id: matching_status_ids).pluck(:id) + [@status.id] if @status.distributable?
ReportService.new.call(Account.representative, @account, status_ids: status_ids, comment: I18n.t('spam_check.spam_detected_and_silenced'))
end
def already_flagged?
@account.silenced?
@account.silenced? || @account.targeted_reports.unresolved.where(account_id: -99).exists?
end
def trusted?
......
......@@ -198,7 +198,7 @@ class Account < ApplicationRecord
end
def unsilence!
update!(silenced_at: nil, trust_level: trust_level == TRUST_LEVELS[:untrusted] ? TRUST_LEVELS[:trusted] : trust_level)
update!(silenced_at: nil)
end
def suspended?
......@@ -310,10 +310,9 @@ class Account < ApplicationRecord
def save_with_optional_media!
save!
rescue ActiveRecord::RecordInvalid
self.avatar = nil
self.header = nil
self[:avatar_remote_url] = ''
self[:header_remote_url] = ''
self.avatar = nil
self.header = nil
save!
end
......
......@@ -62,6 +62,8 @@ class Admin::AccountAction
def process_action!
case type
when 'none'
handle_resolve!
when 'disable'
handle_disable!
when 'silence'
......@@ -103,6 +105,16 @@ class Admin::AccountAction
end
end
def handle_resolve!
if with_report? && report.account_id == -99 && target_account.trust_level == Account::TRUST_LEVELS[:untrusted]
# This is an automated report and it is being dismissed, so it's
# a false positive, in which case update the account's trust level
# to prevent further spam checks
target_account.update(trust_level: Account::TRUST_LEVELS[:trusted])
end
end
def handle_disable!
authorize(target_account.user, :disable?)
log_action(:disable, target_account.user)
......
......@@ -18,7 +18,7 @@ module Remotable
return
end
return if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.blank? || self[attribute_name] == url
return if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.blank? || (self[attribute_name] == url && send("#{attachment_name}_file_name").present?)
begin
Request.new(:get, url).perform do |response|
......
......@@ -30,6 +30,7 @@ class Form::AdminSettings
mascot
spam_check_enabled
trends
trendable_by_default
show_domain_blocks
show_domain_blocks_rationale
noindex
......@@ -46,6 +47,7 @@ class Form::AdminSettings
profile_directory
spam_check_enabled
trends
trendable_by_default
noindex
).freeze
......
......@@ -76,7 +76,7 @@ class Tag < ApplicationRecord
alias listable? listable
def trendable
boolean_with_default('trendable', false)
boolean_with_default('trendable', Setting.trendable_by_default)
end
alias trendable? trendable
......
......@@ -52,13 +52,12 @@
.hero-widget__img
= image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.jpg'), alt: @instance_presenter.site_title
- if @instance_presenter.site_short_description.present?
.hero-widget__text
%p
= @instance_presenter.site_short_description.html_safe.presence
= link_to about_more_path do
= t('about.learn_more')
= fa_icon 'angle-double-right'
.hero-widget__text
%p
= @instance_presenter.site_short_description.html_safe.presence || t('about.about_mastodon_html')
= link_to about_more_path do
= t('about.learn_more')
= fa_icon 'angle-double-right'
.hero-widget__footer
.hero-widget__footer__column
......
......@@ -20,10 +20,10 @@
= f.input :site_contact_email, wrapper: :with_label, label: t('admin.settings.contact_information.email')
.fields-group
= f.input :site_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description.title'), hint: t('admin.settings.site_description.desc_html'), input_html: { rows: 4 }
= f.input :site_short_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_short_description.title'), hint: t('admin.settings.site_short_description.desc_html'), input_html: { rows: 2 }
.fields-group
= f.input :site_short_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_short_description.title'), hint: t('admin.settings.site_short_description.desc_html'), input_html: { rows: 2 }
= f.input :site_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description.title'), hint: t('admin.settings.site_description.desc_html'), input_html: { rows: 2 }
.fields-row
.fields-row__column.fields-row__column-6.fields-group
......@@ -71,6 +71,9 @@
.fields-group
= f.input :trends, as: :boolean, wrapper: :with_label, label: t('admin.settings.trends.title'), hint: t('admin.settings.trends.desc_html')
.fields-group
= f.input :trendable_by_default, as: :boolean, wrapper: :with_label, label: t('admin.settings.trendable_by_default.title'), hint: t('admin.settings.trendable_by_default.desc_html')
.fields-group
= f.input :noindex, as: :boolean, wrapper: :with_label, label: t('admin.settings.default_noindex.title'), hint: t('admin.settings.default_noindex.desc_html')
......@@ -89,8 +92,8 @@
= f.input :show_domain_blocks_rationale, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks_rationale.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
.fields-group
= f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, label: t('admin.settings.registrations.closed_message.title'), hint: t('admin.settings.registrations.closed_message.desc_html'), input_html: { rows: 8 }
= f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } unless whitelist_mode?
= f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, label: t('admin.settings.registrations.closed_message.title'), hint: t('admin.settings.registrations.closed_message.desc_html'), input_html: { rows: 8 }
= f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 }
= f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }, label: t('admin.settings.custom_css.title'), hint: t('admin.settings.custom_css.desc_html')
......
......@@ -3,7 +3,7 @@
= image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.jpg'), alt: @instance_presenter.site_title
.hero-widget__text
%p= @instance_presenter.site_short_description.html_safe.presence || @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
%p= @instance_presenter.site_short_description.html_safe.presence || t('about.about_mastodon_html')
- if Setting.trends && !(user_signed_in? && !current_user.setting_trends)
- trends = TrendingTags.get(3)
......
- thumbnail = @instance_presenter.thumbnail
- description ||= strip_tags(@instance_presenter.site_short_description.presence || @instance_presenter.site_description.presence || t('about.about_mastodon_html'))
- description ||= strip_tags(@instance_presenter.site_short_description.presence || t('about.about_mastodon_html'))
%meta{ name: 'description', content: description }/
......
......@@ -20,7 +20,7 @@
%p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }<
%span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}&nbsp;
%button.status__content__spoiler-link= t('statuses.show_more')
.e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }
.e-content{ style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }
= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
- if status.preloadable_poll
= react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
......
......@@ -24,7 +24,7 @@
%p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }<
%span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}&nbsp;
%button.status__content__spoiler-link= t('statuses.show_more')
.e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }
.e-content{ style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }
= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
- if status.preloadable_poll
= react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
......
# frozen_string_literal: true
Paperclip.options[:read_timeout] = 60
Paperclip.interpolates :filename do |attachment, style|
return attachment.original_filename if style == :original
[basename(attachment, style), extension(attachment, style)].delete_if(&:blank?).join('.')
if style == :original
attachment.original_filename
else
[basename(attachment, style), extension(attachment, style)].delete_if(&:blank?).join('.')
end
end
Paperclip::Attachment.default_options.merge!(
......@@ -24,17 +25,21 @@ if ENV['S3_ENABLED'] == 'true'
storage: :s3,
s3_protocol: s3_protocol,
s3_host_name: s3_hostname,
s3_headers: {
'X-Amz-Multipart-Threshold' => ENV.fetch('S3_MULTIPART_THRESHOLD') { 15.megabytes }.to_i,
'Cache-Control' => 'public, max-age=315576000, immutable',
},
s3_permissions: ENV.fetch('S3_PERMISSION') { 'public-read' },
s3_region: s3_region,
s3_credentials: {
bucket: ENV['S3_BUCKET'],
access_key_id: ENV['AWS_ACCESS_KEY_ID'],
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
},
s3_options: {
signature_version: ENV.fetch('S3_SIGNATURE_VERSION') { 'v4' },
http_open_timeout: 5,
......@@ -49,6 +54,7 @@ if ENV['S3_ENABLED'] == 'true'
endpoint: ENV['S3_ENDPOINT'],
force_path_style: true
)
Paperclip::Attachment.default_options[:url] = ':s3_path_url'
end
......@@ -74,6 +80,7 @@ elsif ENV['SWIFT_ENABLED'] == 'true'
openstack_region: ENV['SWIFT_REGION'],
openstack_cache_ttl: ENV.fetch('SWIFT_CACHE_TTL') { 60 },
},
fog_directory: ENV['SWIFT_CONTAINER'],
fog_host: ENV['SWIFT_OBJECT_URL'],
fog_public: true
......@@ -82,7 +89,7 @@ else
Paperclip::Attachment.default_options.merge!(
storage: :filesystem,
use_timestamp: true,
path: (ENV['PAPERCLIP_ROOT_PATH'] || ':rails_root/public/system') + '/:class/:attachment/:id_partition/:style/:filename',
url: (ENV['PAPERCLIP_ROOT_URL'] || '/system') + '/:class/:attachment/:id_partition/:style/:filename',
path: ENV.fetch('PAPERCLIP_ROOT_PATH', ':rails_root/public/system') + '/:class/:attachment/:id_partition/:style/:filename',
url: ENV.fetch('PAPERCLIP_ROOT_URL', '/system') + '/:class/:attachment/:id_partition/:style/:filename',
)
end
......@@ -2,7 +2,7 @@
en:
about:
about_hashtag_html: These are public toots tagged with <strong>#%{hashtag}</strong>. You can interact with them if you have an account anywhere in the fediverse.
about_mastodon_html: quey is a social network based on open web protocols and free, open-source software. It is decentralized like e-mail.
about_mastodon_html: 'The social network of the future: No ads, no corporate surveillance, ethical design, and decentralization! Own your data with quey!'
about_this: About
active_count_after: active
active_footnote: Monthly Active Users (MAU)
......@@ -17,8 +17,7 @@ en:
contact_unavailable: N/A
discover_users: Discover users
documentation: Documentation
federation_hint_html: With an account on %{instance} you'll be able to follow people on any quey server and beyond.
generic_description: "%{domain} is one server in the network"
federation_hint_html: With an account on %{instance} you'll be able to follow people on any Mastodon server and beyond.
get_apps: Try a mobile app
hosted_on: quey hosted on %{domain}
instance_actor_flash: |
......@@ -479,8 +478,8 @@ en:
open: Anyone can sign up
title: Registrations mode
show_known_fediverse_at_about_page:
desc_html: When toggled, it will show toots from all the known fediverse on preview. Otherwise it will only show local toots.
title: Show known fediverse on timeline preview
desc_html: When disabled, restricts the public timeline linked from the landing page to showing only local content
title: Include federated content on unauthenticated public timeline page
show_staff_badge:
desc_html: Show a staff badge on a user page
title: Show staff badge
......@@ -498,15 +497,18 @@ en:
title: Custom terms of service
site_title: Server name
spam_check_enabled:
desc_html: quey can auto-silence and auto-report accounts that send repeated unsolicited messages. There may be false positives.
desc_html: quey can auto-report accounts that send repeated unsolicited messages. There may be false positives.
title: Anti-spam automation
thumbnail:
desc_html: Used for previews via OpenGraph and API. 1200x630px recommended
title: Server thumbnail
timeline_preview:
desc_html: Display public timeline on landing page
title: Timeline preview
desc_html: Display link to public timeline on landing page and allow API access to the public timeline without authentication
title: Allow unauthenticated access to public timeline
title: Site settings
trendable_by_default:
desc_html: Affects hashtags that have not been previously disallowed
title: Allow hashtags to trend without prior review
trends:
desc_html: Publicly display previously reviewed hashtags that are currently trending
title: Trending hashtags
......
......@@ -35,6 +35,7 @@ defaults: &defaults
use_blurhash: true
use_pending_items: false
trends: true
trendable_by_default: false
notification_emails:
follow: false
reblog: false
......
......@@ -50,6 +50,7 @@ module Mastodon
option :concurrency, type: :numeric, default: 5, aliases: [:c]
option :verbose, type: :boolean, default: false, aliases: [:v]
option :dry_run, type: :boolean, default: false
option :force, type: :boolean, default: false
desc 'refresh', 'Fetch remote media files'
long_desc <<-DESC
Re-downloads media attachments from other servers. You must specify the
......@@ -62,6 +63,9 @@ module Mastodon
using username@domain handle of the account.
Use the --domain option to download attachments from a specific domain.
By default, attachments that are believed to be already downloaded will
not be re-downloaded. To force re-download of every URL, use --force.
DESC
def refresh
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
......@@ -85,7 +89,7 @@ module Mastodon
end
processed, aggregate = parallelize_with_progress(scope) do |media_attachment|
next if media_attachment.remote_url.blank?
next if media_attachment.remote_url.blank? || (!options[:force] && media_attachment.file_file_name.present?)
unless options[:dry_run]
media_attachment.reset_file!
......@@ -97,5 +101,17 @@ module Mastodon
say("Downloaded #{processed} media attachments (approx. #{number_to_human_size(aggregate)})#{dry_run}", :green, true)
end
desc 'usage', 'Calculate disk space consumed by Mastodon'
def usage
say("Attachments:\t#{number_to_human_size(MediaAttachment.sum(:file_file_size))} (#{number_to_human_size(MediaAttachment.where(account: Account.local).sum(:file_file_size))} local)")
say("Custom emoji:\t#{number_to_human_size(CustomEmoji.sum(:image_file_size))} (#{number_to_human_size(CustomEmoji.local.sum(:image_file_size))} local)")
say("Preview cards:\t#{number_to_human_size(PreviewCard.sum(:image_file_size))}")
say("Avatars:\t#{number_to_human_size(Account.sum(:avatar_file_size))} (#{number_to_human_size(Account.local.sum(:avatar_file_size))} local)")
say("Headers:\t#{number_to_human_size(Account.sum(:header_file_size))} (#{number_to_human_size(Account.local.sum(:header_file_size))} local)")
say("Backups:\t#{number_to_human_size(Backup.sum(:dump_file_size))}")
say("Imports:\t#{number_to_human_size(Import.sum(:data_file_size))}")
say("Settings:\t#{number_to_human_size(SiteUpload.sum(:file_file_size))}")
end
end
end
......@@ -13,7 +13,7 @@ module Mastodon
end
def patch
0
1
end
def flags
......
......@@ -181,10 +181,6 @@ RSpec.describe SpamCheck do
described_class.new(status2).flag!
end
it 'silences the account' do
expect(sender.silenced?).to be true
end
it 'creates a report about the account' do
expect(sender.targeted_reports.unresolved.count).to eq 1
end
......
......@@ -126,8 +126,8 @@ RSpec.describe Account, type: :model do
end
it 'sets default avatar, header, avatar_remote_url, and header_remote_url' do
expect(account.avatar_remote_url).to eq ''
expect(account.header_remote_url).to eq ''
expect(account.avatar_remote_url).to eq 'https://remote.test/invalid_avatar'
expect(account.header_remote_url).to eq expectation.header_remote_url
expect(account.avatar_file_name).to eq nil
expect(account.header_file_name).to eq nil
end
......
......@@ -18,6 +18,8 @@ RSpec.describe Remotable do
def hoge=(arg); end
def hoge_file_name; end
def hoge_file_name=(arg); end
def has_attribute?(arg); end
......@@ -109,12 +111,21 @@ RSpec.describe Remotable do
end
context 'foo[attribute_name] == url' do
it 'makes no request' do
it 'makes no request if file is saved' do
allow(foo).to receive(:[]).with(attribute_name).and_return(url)
allow(foo).to receive(:hoge_file_name).and_return('foo.jpg')
foo.hoge_remote_url = url
expect(request).not_to have_been_requested
end
it 'makes request if file is not saved' do
allow(foo).to receive(:[]).with(attribute_name).and_return(url)
allow(foo).to receive(:hoge_file_name).and_return(nil)
foo.hoge_remote_url = url
expect(request).to have_been_requested
end
end
context "scheme is https, parsed_url.host isn't empty, and foo[attribute_name] != url" do
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment