discourse/spec/models/upload_spec.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

837 lines
29 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
RSpec.describe Upload do
let(:upload) { build(:upload) }
let(:user_id) { 1 }
2013-04-07 23:52:46 +08:00
let(:image_filename) { "logo.png" }
let(:image) { file_from_fixtures(image_filename) }
let(:image_svg_filename) { "image.svg" }
let(:image_svg) { file_from_fixtures(image_svg_filename) }
let(:huge_image_filename) { "huge.jpg" }
let(:huge_image) { file_from_fixtures(huge_image_filename) }
2014-04-15 04:55:57 +08:00
let(:attachment_path) { __FILE__ }
let(:attachment) { File.new(attachment_path) }
describe ".with_no_non_post_relations" do
it "does not find non-post related uploads" do
post_upload = Fabricate(:upload)
post =
Fabricate(
:post,
raw: "<img src='#{post_upload.url}'>",
user: Fabricate(:user, refresh_auto_groups: true),
)
post.link_post_uploads
badge_upload = Fabricate(:upload)
Fabricate(:badge, image_upload: badge_upload)
avatar_upload = Fabricate(:upload)
Fabricate(:user, uploaded_avatar: avatar_upload)
site_setting_upload = Fabricate(:upload)
SiteSetting.create!(
name: "logo",
data_type: SiteSettings::TypeSupervisor.types[:upload],
value: site_setting_upload.id,
)
upload_ids = Upload.by_users.with_no_non_post_relations.pluck(:id)
expect(upload_ids).to eq([post_upload.id])
end
end
describe ".create_thumbnail!" do
it "does not create a thumbnail when disabled" do
SiteSetting.create_thumbnails = false
OptimizedImage.expects(:create_for).never
2013-09-27 16:55:50 +08:00
upload.create_thumbnail!(100, 100)
end
it "creates a thumbnail" do
upload = Fabricate(:upload)
thumbnail = Fabricate(:optimized_image, upload: upload)
SiteSetting.expects(:create_thumbnails?).returns(true)
OptimizedImage.expects(:create_for).returns(thumbnail)
2013-09-27 16:55:50 +08:00
upload.create_thumbnail!(100, 100)
upload.reload
expect(upload.optimized_images.count).to eq(1)
end
end
it "supports <style> element in SVG" do
SiteSetting.authorized_extensions = "svg"
upload = UploadCreator.new(image_svg, image_svg_filename).create_for(user_id)
expect(upload.valid?).to eq(true)
path = Discourse.store.path_for(upload)
expect(File.read(path)).to match(/<style>/)
end
it "can reconstruct dimensions on demand" do
SiteSetting.max_image_megapixels = 85
upload = UploadCreator.new(huge_image, "image.png").create_for(user_id)
upload.update_columns(width: nil, height: nil, thumbnail_width: nil, thumbnail_height: nil)
upload = Upload.find(upload.id)
expect(upload.width).to eq(8900)
expect(upload.height).to eq(8900)
upload.reload
expect(upload.read_attribute(:width)).to eq(8900)
upload.update_columns(width: nil, height: nil, thumbnail_width: nil, thumbnail_height: nil)
expect(upload.thumbnail_width).to eq(500)
expect(upload.thumbnail_height).to eq(500)
end
it "dimension calculation returns nil on missing image" do
SiteSetting.max_image_megapixels = 85
upload = UploadCreator.new(huge_image, "image.png").create_for(user_id)
upload.update_columns(width: nil, height: nil, thumbnail_width: nil, thumbnail_height: nil)
missing_url = "wrong_folder#{upload.url}"
upload.update_columns(url: missing_url)
expect(upload.thumbnail_height).to eq(nil)
expect(upload.thumbnail_width).to eq(nil)
end
it "returns error when image resolution is to big" do
2020-09-15 10:39:46 +08:00
SiteSetting.max_image_megapixels = 10
upload = UploadCreator.new(huge_image, "image.png").create_for(user_id)
expect(upload.persisted?).to eq(false)
expect(upload.errors.messages[:base].first).to eq(
I18n.t("upload.images.larger_than_x_megapixels", max_image_megapixels: 10),
2020-09-15 10:39:46 +08:00
)
end
it "extracts file extension" do
created_upload = UploadCreator.new(image, image_filename).create_for(user_id)
expect(created_upload.extension).to eq("png")
end
it "should create an invalid upload when the filename is blank" do
SiteSetting.authorized_extensions = "*"
2018-08-20 10:41:46 +08:00
created_upload = UploadCreator.new(attachment, nil).create_for(user_id)
expect(created_upload.valid?).to eq(false)
end
describe ".extract_url" do
let(:url) { "https://example.com/uploads/default/original/1X/d1c2d40ab994e8410c.png" }
it "should return the right part of url" do
expect(Upload.extract_url(url).to_s).to eq("/original/1X/d1c2d40ab994e8410c.png")
end
end
describe ".get_from_url" do
let(:sha1) { "10f73034616a796dfd70177dc54b6def44c4ba6f" }
let(:upload) { Fabricate(:upload, sha1: sha1) }
it "works when the file has been uploaded" do
expect(Upload.get_from_url(upload.url)).to eq(upload)
end
describe "for an extensionless url" do
before do
upload.update!(url: upload.url.sub(".png", ""))
upload.reload
end
it "should return the right upload" do
expect(Upload.get_from_url(upload.url)).to eq(upload)
end
end
it "should return the right upload as long as the upload's URL matches" do
upload.update!(url: "/uploads/default/12345/971308e535305c51.png")
expect(Upload.get_from_url(upload.url)).to eq(upload)
expect(Upload.get_from_url("/uploads/default/123131/971308e535305c51.png")).to eq(nil)
end
describe "for a url a tree" do
before do
upload.update!(
url:
Discourse.store.get_path_for("original", 16_001, upload.sha1, ".#{upload.extension}"),
)
end
it "should return the right upload" do
expect(Upload.get_from_url(upload.url)).to eq(upload)
end
end
it "works when using a cdn" do
2016-10-18 13:39:16 +08:00
begin
original_asset_host = Rails.configuration.action_controller.asset_host
Rails.configuration.action_controller.asset_host = "http://my.cdn.com"
expect(Upload.get_from_url(URI.join("http://my.cdn.com", upload.url).to_s)).to eq(upload)
2016-10-18 13:39:16 +08:00
ensure
Rails.configuration.action_controller.asset_host = original_asset_host
end
end
it "should return the right upload when using the full URL" do
expect(
Upload.get_from_url(URI.join("http://discourse.some.com:3000/", upload.url).to_s),
).to eq(upload)
end
2016-10-20 18:34:42 +08:00
it "doesn't blow up with an invalid URI" do
expect { Upload.get_from_url("http://ip:port/index.html") }.not_to raise_error
expect { Upload.get_from_url("mailto:admin%40example.com") }.not_to raise_error
2018-05-18 20:31:36 +08:00
expect { Upload.get_from_url("mailto:example") }.not_to raise_error
2016-10-20 18:34:42 +08:00
end
describe "s3 store" do
let(:upload) { Fabricate(:upload_s3) }
let(:path) { upload.url.sub(SiteSetting.Upload.s3_base_url, "") }
before { setup_s3 }
it "can download an s3 upload" do
stub_request(:get, upload.url).to_return(status: 200, body: "hello", headers: {})
expect(upload.content).to eq("hello")
end
it "should return the right upload when using base url (not CDN) for s3" do
upload
expect(Upload.get_from_url(upload.url)).to eq(upload)
end
describe "when using a cdn" do
let(:s3_cdn_url) { "https://mycdn.slowly.net" }
before { SiteSetting.s3_cdn_url = s3_cdn_url }
it "should return the right upload" do
upload
expect(Upload.get_from_url(URI.join(s3_cdn_url, path).to_s)).to eq(upload)
end
describe "when upload bucket contains subfolder" do
before { SiteSetting.s3_upload_bucket = "s3-upload-bucket/path/path2" }
it "should return the right upload" do
upload
expect(Upload.get_from_url(URI.join(s3_cdn_url, path).to_s)).to eq(upload)
end
end
end
it "should return the right upload when using one CDN for both s3 and assets" do
begin
original_asset_host = Rails.configuration.action_controller.asset_host
cdn_url = "http://my.cdn.com"
Rails.configuration.action_controller.asset_host = cdn_url
SiteSetting.s3_cdn_url = cdn_url
upload
expect(Upload.get_from_url(URI.join(cdn_url, path).to_s)).to eq(upload)
ensure
Rails.configuration.action_controller.asset_host = original_asset_host
end
end
end
end
describe ".get_from_urls" do
let(:upload) { Fabricate(:upload, sha1: "10f73034616a796dfd70177dc54b6def44c4ba6f") }
let(:upload2) { Fabricate(:upload, sha1: "2a7081e615f9075befd87a9a6d273935c0262cd5") }
it "works with multiple uploads" do
expect(Upload.get_from_urls([upload.url, upload2.url])).to contain_exactly(upload, upload2)
end
it "works for an extensionless URL" do
url = upload.url.sub(".png", "")
upload.update!(url: url)
expect(Upload.get_from_urls([url])).to contain_exactly(upload)
end
it "works with uploads with mismatched URLs" do
upload.update!(url: "/uploads/default/12345/971308e535305c51.png")
expect(Upload.get_from_urls([upload.url])).to contain_exactly(upload)
expect(Upload.get_from_urls(["/uploads/default/123131/971308e535305c51.png"])).to be_empty
end
it "works with an upload with a URL containing a deep tree" do
upload.update!(
url: Discourse.store.get_path_for("original", 16_001, upload.sha1, ".#{upload.extension}"),
)
expect(Upload.get_from_urls([upload.url])).to contain_exactly(upload)
end
it "works when using a CDN" do
begin
original_asset_host = Rails.configuration.action_controller.asset_host
Rails.configuration.action_controller.asset_host = "http://my.cdn.com"
expect(
Upload.get_from_urls([URI.join("http://my.cdn.com", upload.url).to_s]),
).to contain_exactly(upload)
ensure
Rails.configuration.action_controller.asset_host = original_asset_host
end
end
it "works with full URLs" do
expect(
Upload.get_from_urls([URI.join("http://discourse.some.com:3000/", upload.url).to_s]),
).to contain_exactly(upload)
end
it "handles invalid URIs" do
urls = %w[http://ip:port/index.html mailto:admin%40example.com mailto:example]
expect { Upload.get_from_urls(urls) }.not_to raise_error
end
end
describe ".generate_digest" do
it "should return the right digest" do
expect(Upload.generate_digest(image.path)).to eq("bc975735dfc6409c1c2aa5ebf2239949bcbdbd65")
end
end
describe ".short_url" do
it "should generate a correct short url" do
upload = Upload.new(sha1: "bda2c513e1da04f7b4e99230851ea2aafeb8cc4e", extension: "png")
expect(upload.short_url).to eq("upload://r3AYqESanERjladb4vBB7VsMBm6.png")
upload.extension = nil
expect(upload.short_url).to eq("upload://r3AYqESanERjladb4vBB7VsMBm6")
end
end
describe ".sha1_from_short_url" do
it "should be able to look up sha1" do
sha1 = "bda2c513e1da04f7b4e99230851ea2aafeb8cc4e"
expect(Upload.sha1_from_short_url("upload://r3AYqESanERjladb4vBB7VsMBm6.png")).to eq(sha1)
expect(Upload.sha1_from_short_url("upload://r3AYqESanERjladb4vBB7VsMBm6")).to eq(sha1)
expect(Upload.sha1_from_short_url("r3AYqESanERjladb4vBB7VsMBm6")).to eq(sha1)
end
it "should be able to look up sha1 even with leading zeros" do
sha1 = "0000c513e1da04f7b4e99230851ea2aafeb8cc4e"
expect(Upload.sha1_from_short_url("upload://1Eg9p8rrCURq4T3a6iJUk0ri6.png")).to eq(sha1)
end
end
describe ".sha1_from_long_url" do
it "should be able to get the sha1 from a regular upload URL" do
expect(
Upload.sha1_from_long_url(
"https://cdn.test.com/test/original/4X/7/6/5/1b6453892473a467d07372d45eb05abc2031647a.png",
),
).to eq("1b6453892473a467d07372d45eb05abc2031647a")
end
it "should be able to get the sha1 from a secure upload URL" do
expect(
Upload.sha1_from_long_url(
"#{Discourse.base_url}\/secure-uploads/original/1X/1b6453892473a467d07372d45eb05abc2031647a.png",
),
).to eq("1b6453892473a467d07372d45eb05abc2031647a")
end
it "doesn't get a sha1 for a URL that does not match our scheme" do
expect(
Upload.sha1_from_long_url(
"#{Discourse.base_url}\/blah/1b6453892473a467d07372d45eb05abc2031647a.png",
),
).to eq(nil)
end
end
2019-06-04 14:10:46 +08:00
describe "#base62_sha1" do
it "should return the right value" do
upload.update!(sha1: "0000c513e1da04f7b4e99230851ea2aafeb8cc4e")
expect(upload.base62_sha1).to eq("1Eg9p8rrCURq4T3a6iJUk0ri6")
end
end
describe ".sha1_from_short_path" do
it "should be able to lookup sha1" do
path = "/uploads/short-url/3UjQ4jHoyeoQndk5y3qHzm3QVTQ.png"
sha1 = "1b6453892473a467d07372d45eb05abc2031647a"
expect(Upload.sha1_from_short_path(path)).to eq(sha1)
expect(Upload.sha1_from_short_path(path.sub(".png", ""))).to eq(sha1)
end
end
2018-11-14 15:03:02 +08:00
describe "#to_s" do
it "should return the right value" do
expect(upload.to_s).to eq(upload.url)
end
end
describe ".migrate_to_new_scheme" do
it "should not migrate system uploads" do
SiteSetting.migrate_to_new_scheme = true
expect { Upload.migrate_to_new_scheme }.to_not change { Upload.pluck(:url) }
end
end
describe ".update_secure_status" do
it "respects the override parameter if provided" do
upload.update!(secure: true)
upload.update_secure_status(override: true)
expect(upload.secure).to eq(true)
upload.update_secure_status(override: false)
expect(upload.secure).to eq(false)
end
it "marks a local upload as not secure with default settings" do
upload.update!(secure: true)
expect { upload.update_secure_status }.to change { upload.secure }
expect(upload.secure).to eq(false)
end
context "with local attachment" do
before { SiteSetting.authorized_extensions = "pdf" }
let(:upload) do
Fabricate(:upload, original_filename: "small.pdf", extension: "pdf", secure: true)
end
it "marks a local attachment as secure if secure uploads enabled" do
upload.update!(secure: false, access_control_post: Fabricate(:private_message_post))
enable_secure_uploads
expect { upload.update_secure_status }.to change { upload.secure }
expect(upload.secure).to eq(true)
end
it "marks a local attachment as not secure if secure uploads enabled" do
expect { upload.update_secure_status }.to change { upload.secure }
expect(upload.secure).to eq(false)
end
end
it "does not change secure status of a non-attachment when prevent_anons_from_downloading_files is enabled by itself" do
SiteSetting.prevent_anons_from_downloading_files = true
SiteSetting.authorized_extensions = "mp4"
upload.update!(original_filename: "small.mp4", extension: "mp4")
expect { upload.update_secure_status }.not_to change { upload.secure }
expect(upload.secure).to eq(false)
end
context "with secure uploads enabled" do
before { enable_secure_uploads }
it "does not mark an image upload as not secure when there is no access control post id, to avoid unintentional exposure" do
upload.update!(secure: true)
FEATURE: Secure media allowing duplicated uploads with category-level privacy and post-based access rules (#8664) ### General Changes and Duplication * We now consider a post `with_secure_media?` if it is in a read-restricted category. * When uploading we now set an upload's secure status straight away. * When uploading if `SiteSetting.secure_media` is enabled, we do not check to see if the upload already exists using the `sha1` digest of the upload. The `sha1` column of the upload is filled with a `SecureRandom.hex(20)` value which is the same length as `Upload::SHA1_LENGTH`. The `original_sha1` column is filled with the _real_ sha1 digest of the file. * Whether an upload `should_be_secure?` is now determined by whether the `access_control_post` is `with_secure_media?` (if there is no access control post then we leave the secure status as is). * When serializing the upload, we now cook the URL if the upload is secure. This is so it shows up correctly in the composer preview, because we set secure status on upload. ### Viewing Secure Media * The secure-media-upload URL will take the post that the upload is attached to into account via `Guardian.can_see?` for access permissions * If there is no `access_control_post` then we just deliver the media. This should be a rare occurrance and shouldn't cause issues as the `access_control_post` is set when `link_post_uploads` is called via `CookedPostProcessor` ### Removed We no longer do any of these because we do not reuse uploads by sha1 if secure media is enabled. * We no longer have a way to prevent cross-posting of a secure upload from a private context to a public context. * We no longer have to set `secure: false` for uploads when uploading for a theme component.
2020-01-16 11:50:27 +08:00
upload.update_secure_status
expect(upload.secure).to eq(true)
end
it "marks the upload as not secure if its access control post is a public post" do
FileStore::S3Store.any_instance.expects(:update_upload_ACL).with(upload)
FEATURE: Secure media allowing duplicated uploads with category-level privacy and post-based access rules (#8664) ### General Changes and Duplication * We now consider a post `with_secure_media?` if it is in a read-restricted category. * When uploading we now set an upload's secure status straight away. * When uploading if `SiteSetting.secure_media` is enabled, we do not check to see if the upload already exists using the `sha1` digest of the upload. The `sha1` column of the upload is filled with a `SecureRandom.hex(20)` value which is the same length as `Upload::SHA1_LENGTH`. The `original_sha1` column is filled with the _real_ sha1 digest of the file. * Whether an upload `should_be_secure?` is now determined by whether the `access_control_post` is `with_secure_media?` (if there is no access control post then we leave the secure status as is). * When serializing the upload, we now cook the URL if the upload is secure. This is so it shows up correctly in the composer preview, because we set secure status on upload. ### Viewing Secure Media * The secure-media-upload URL will take the post that the upload is attached to into account via `Guardian.can_see?` for access permissions * If there is no `access_control_post` then we just deliver the media. This should be a rare occurrance and shouldn't cause issues as the `access_control_post` is set when `link_post_uploads` is called via `CookedPostProcessor` ### Removed We no longer do any of these because we do not reuse uploads by sha1 if secure media is enabled. * We no longer have a way to prevent cross-posting of a secure upload from a private context to a public context. * We no longer have to set `secure: false` for uploads when uploading for a theme component.
2020-01-16 11:50:27 +08:00
upload.update!(secure: true, access_control_post: Fabricate(:post))
upload.update_secure_status
expect(upload.secure).to eq(false)
end
it "leaves the upload as secure if its access control post is a PM post" do
FEATURE: Secure media allowing duplicated uploads with category-level privacy and post-based access rules (#8664) ### General Changes and Duplication * We now consider a post `with_secure_media?` if it is in a read-restricted category. * When uploading we now set an upload's secure status straight away. * When uploading if `SiteSetting.secure_media` is enabled, we do not check to see if the upload already exists using the `sha1` digest of the upload. The `sha1` column of the upload is filled with a `SecureRandom.hex(20)` value which is the same length as `Upload::SHA1_LENGTH`. The `original_sha1` column is filled with the _real_ sha1 digest of the file. * Whether an upload `should_be_secure?` is now determined by whether the `access_control_post` is `with_secure_media?` (if there is no access control post then we leave the secure status as is). * When serializing the upload, we now cook the URL if the upload is secure. This is so it shows up correctly in the composer preview, because we set secure status on upload. ### Viewing Secure Media * The secure-media-upload URL will take the post that the upload is attached to into account via `Guardian.can_see?` for access permissions * If there is no `access_control_post` then we just deliver the media. This should be a rare occurrance and shouldn't cause issues as the `access_control_post` is set when `link_post_uploads` is called via `CookedPostProcessor` ### Removed We no longer do any of these because we do not reuse uploads by sha1 if secure media is enabled. * We no longer have a way to prevent cross-posting of a secure upload from a private context to a public context. * We no longer have to set `secure: false` for uploads when uploading for a theme component.
2020-01-16 11:50:27 +08:00
upload.update!(secure: true, access_control_post: Fabricate(:private_message_post))
upload.update_secure_status
expect(upload.secure).to eq(true)
end
it "does not attempt to change the ACL if the secure status has not changed" do
FileStore::S3Store.any_instance.expects(:update_upload_ACL).with(upload).never
upload.update!(secure: true, access_control_post: Fabricate(:private_message_post))
upload.update_secure_status
end
it "does not attempt to change the ACL if s3_use_acls is disabled" do
SiteSetting.secure_uploads = false
SiteSetting.s3_use_acls = false
FileStore::S3Store.any_instance.expects(:update_upload_ACL).with(upload).never
upload.update!(secure: true, access_control_post: Fabricate(:post))
upload.update_secure_status
expect(upload.secure).to eq(false)
end
it "marks an image upload as secure if login_required is enabled" do
SiteSetting.login_required = true
upload.update!(secure: false)
expect { upload.update_secure_status }.to change { upload.secure }
expect(upload.reload.secure).to eq(true)
end
it "does not mark an upload used for a custom emoji as secure" do
SiteSetting.login_required = true
upload.update!(secure: false)
CustomEmoji.create(name: "meme", upload: upload)
upload.update_secure_status
expect(upload.reload.secure).to eq(false)
end
it "does not mark an upload whose origin matches a regular emoji as secure (sometimes emojis are downloaded in pull_hotlinked_images)" do
SiteSetting.login_required = true
falafel =
Emoji.all.find do |e|
e.url == "/images/emoji/twitter/falafel.png?v=#{Emoji::EMOJI_VERSION}"
end
upload.update!(secure: false, origin: "http://localhost:3000#{falafel.url}")
upload.update_secure_status
expect(upload.reload.secure).to eq(false)
end
it "does not mark any upload with origin containing images/emoji in the URL" do
SiteSetting.login_required = true
upload.update!(secure: false, origin: "http://localhost:3000/images/emoji/test.png")
upload.update_secure_status
expect(upload.reload.secure).to eq(false)
end
it "does not throw an error if the object storage provider does not support ACLs" do
FileStore::S3Store
.any_instance
.stubs(:update_upload_ACL)
.raises(
Aws::S3::Errors::NotImplemented.new(
"A header you provided implies functionality that is not implemented",
"",
),
)
upload.update!(secure: true, access_control_post: Fabricate(:private_message_post))
expect { upload.update_secure_status }.not_to raise_error
end
it "succeeds even if the extension of the upload is not authorized" do
upload.update!(secure: false, access_control_post: Fabricate(:private_message_post))
SiteSetting.login_required = true
SiteSetting.authorized_extensions = ""
upload.update_secure_status
upload.reload
expect(upload.secure).to eq(true)
end
it "respects the authorized extensions when creating a new upload, no matter its secure status" do
SiteSetting.login_required = true
SiteSetting.authorized_extensions = ""
expect do
Fabricate(
:upload,
access_control_post: Fabricate(:private_message_post),
security_last_changed_at: Time.zone.now,
security_last_changed_reason: "test",
secure: true,
)
end.to raise_error(ActiveRecord::RecordInvalid)
end
context "when secure_uploads_pm_only is true" do
before { SiteSetting.secure_uploads_pm_only = true }
it "does not mark an image upload as secure if login_required is enabled" do
SiteSetting.login_required = true
upload.update!(secure: false)
expect { upload.update_secure_status }.not_to change { upload.secure }
expect(upload.reload.secure).to eq(false)
end
it "marks the upload as not secure if its access control post is a public post" do
upload.update!(secure: true, access_control_post: Fabricate(:post))
upload.update_secure_status
expect(upload.secure).to eq(false)
end
it "leaves the upload as secure if its access control post is a PM post" do
upload.update!(secure: true, access_control_post: Fabricate(:private_message_post))
upload.update_secure_status
expect(upload.secure).to eq(true)
end
end
end
end
describe ".extract_upload_ids" do
let(:upload) { Fabricate(:upload) }
it "works with short URLs" do
ids = Upload.extract_upload_ids("This URL #{upload.short_url} is an upload")
expect(ids).to contain_exactly(upload.id)
end
it "works with SHA1s" do
ids = Upload.extract_upload_ids("This URL /#{upload.sha1} is an upload")
expect(ids).to contain_exactly(upload.id)
end
it "works with Base62 hashes" do
ids = Upload.extract_upload_ids("This URL /#{Upload.base62_sha1(upload.sha1)} is an upload")
expect(ids).to contain_exactly(upload.id)
end
it "works with shorter base62 hashes (when sha1 has leading 0s)" do
upload.update(sha1: "0000c513e1da04f7b4e99230851ea2aafeb8cc4e")
base62 = Upload.base62_sha1(upload.sha1).delete_prefix("0")
ids = Upload.extract_upload_ids("This URL /#{base62} is an upload")
expect(ids).to contain_exactly(upload.id)
end
end
def enable_secure_uploads
setup_s3
SiteSetting.secure_uploads = true
stub_upload(upload)
end
describe ".destroy" do
it "can correctly clear information when destroying an upload" do
upload = Fabricate(:upload)
user = Fabricate(:user)
user.user_profile.update!(
card_background_upload_id: upload.id,
profile_background_upload_id: upload.id,
)
upload.destroy
user.user_profile.reload
expect(user.user_profile.card_background_upload_id).to eq(nil)
expect(user.user_profile.profile_background_upload_id).to eq(nil)
end
end
describe ".secure_uploads_url_from_upload_url" do
before do
# must be done so signed_url_for_path exists
enable_secure_uploads
end
it "gets the secure uploads url from an S3 upload url" do
upload = Fabricate(:upload_s3, secure: true)
url = upload.url
secure_url = Upload.secure_uploads_url_from_upload_url(url)
expect(secure_url).not_to include(SiteSetting.Upload.absolute_base_url)
end
end
describe ".secure_uploads_url?" do
it "works for a secure uploads url with or without schema + host" do
url =
"//localhost:3000/secure-uploads/original/2X/f/f62055931bb702c7fd8f552fb901f977e0289a18.png"
expect(Upload.secure_uploads_url?(url)).to eq(true)
url = "/secure-uploads/original/2X/f/f62055931bb702c7fd8f552fb901f977e0289a18.png"
expect(Upload.secure_uploads_url?(url)).to eq(true)
url =
"http://localhost:3000/secure-uploads/original/2X/f/f62055931bb702c7fd8f552fb901f977e0289a18.png"
expect(Upload.secure_uploads_url?(url)).to eq(true)
end
it "does not get false positives on a topic url" do
url = "/t/secure-uploads-are-cool/42839"
expect(Upload.secure_uploads_url?(url)).to eq(false)
end
it "returns true only for secure uploads URL for actual media (images/video/audio)" do
url = "/secure-uploads/original/2X/f/f62055931bb702c7fd8f552fb901f977e0289a18.mp4"
expect(Upload.secure_uploads_url?(url)).to eq(true)
url = "/secure-uploads/original/2X/f/f62055931bb702c7fd8f552fb901f977e0289a18.png"
expect(Upload.secure_uploads_url?(url)).to eq(true)
url = "/secure-uploads/original/2X/f/f62055931bb702c7fd8f552fb901f977e0289a18.mp3"
expect(Upload.secure_uploads_url?(url)).to eq(true)
url = "/secure-uploads/original/2X/f/f62055931bb702c7fd8f552fb901f977e0289a18.pdf"
expect(Upload.secure_uploads_url?(url)).to eq(false)
end
it "does not work for regular upload urls" do
url = "/uploads/default/test_0/original/1X/e1864389d8252958586c76d747b069e9f68827e3.png"
expect(Upload.secure_uploads_url?(url)).to eq(false)
end
it "does not raise for invalid URLs" do
url = "http://URL:%20https://google.com"
expect(Upload.secure_uploads_url?(url)).to eq(false)
end
end
describe "#dominant_color" do
let(:white_image) { Fabricate(:image_upload, color: "white") }
let(:red_image) { Fabricate(:image_upload, color: "red") }
let(:high_color_image) { Fabricate(:image_upload, color: "#000A00F00", color_depth: 16) }
let(:not_an_image) do
upload = Fabricate(:upload)
file = Tempfile.new(%w[invalid .txt])
file << "Not really an image"
file.rewind
upload.update(url: Discourse.store.store_upload(file, upload), extension: "txt")
upload
end
let(:invalid_image) do
upload = Fabricate(:upload)
file = Tempfile.new(%w[invalid .png])
file << "Not really an image"
file.rewind
upload.update(url: Discourse.store.store_upload(file, upload))
upload
end
it "correctly identifies and stores an image's dominant color" do
expect(white_image.dominant_color).to eq(nil)
expect(white_image.dominant_color(calculate_if_missing: true)).to eq("FFFFFF")
expect(white_image.dominant_color).to eq("FFFFFF")
expect(red_image.dominant_color).to eq(nil)
expect(red_image.dominant_color(calculate_if_missing: true)).to eq("FF0000")
expect(red_image.dominant_color).to eq("FF0000")
expect(high_color_image.dominant_color).to eq(nil)
# original is: #000A00F00
# downsamples to: #009FEF
# A00 is closer to 9F than A0
# EF is closer to F00 than F0
expect(high_color_image.dominant_color(calculate_if_missing: true)).to eq("009FEF")
expect(high_color_image.dominant_color).to eq("009FEF")
end
it "can be backfilled" do
expect(white_image.dominant_color).to eq(nil)
expect(red_image.dominant_color).to eq(nil)
Upload.backfill_dominant_colors!(5)
white_image.reload
red_image.reload
expect(white_image.dominant_color).to eq("FFFFFF")
expect(red_image.dominant_color).to eq("FF0000")
end
it "is backfilled by the job" do
expect(white_image.dominant_color).to eq(nil)
expect(red_image.dominant_color).to eq(nil)
Jobs::BackfillDominantColors.new.execute({})
white_image.reload
red_image.reload
expect(white_image.dominant_color).to eq("FFFFFF")
expect(red_image.dominant_color).to eq("FF0000")
end
it "stores an empty string for non-image uploads" do
expect(not_an_image.dominant_color).to eq(nil)
expect(not_an_image.dominant_color(calculate_if_missing: true)).to eq("")
expect(not_an_image.dominant_color).to eq("")
end
it "correctly handles invalid image files" do
expect(invalid_image.dominant_color).to eq(nil)
expect(invalid_image.dominant_color(calculate_if_missing: true)).to eq("")
expect(invalid_image.dominant_color).to eq("")
end
it "correctly handles unparsable ImageMagick output" do
Discourse::Utils.stubs(:execute_command).returns("someinvalidoutput")
expect(invalid_image.dominant_color).to eq(nil)
expect { invalid_image.dominant_color(calculate_if_missing: true) }.to raise_error(
/Calculated dominant color but unable to parse output/,
)
expect(invalid_image.dominant_color).to eq(nil)
end
it "correctly handles error when file is too large to download" do
white_image.stubs(:local?).returns(false)
FileStore::LocalStore.any_instance.stubs(:download).returns(nil).once
expect(white_image.dominant_color).to eq(nil)
expect(white_image.dominant_color(calculate_if_missing: true)).to eq("")
expect(white_image.dominant_color).to eq("")
end
it "correctly handles error when file has HTTP error" do
white_image.stubs(:local?).returns(false)
FileStore::LocalStore
.any_instance
.stubs(:download)
.raises(OpenURI::HTTPError.new("Error", nil))
.once
expect(white_image.dominant_color).to eq(nil)
expect(white_image.dominant_color(calculate_if_missing: true)).to eq("")
expect(white_image.dominant_color).to eq("")
end
it "is validated for length" do
u = Fabricate(:upload)
# Acceptable values
u.update!(dominant_color: nil)
u.update!(dominant_color: "")
u.update!(dominant_color: "abcdef")
expect { u.update!(dominant_color: "toomanycharacters") }.to raise_error(
ActiveRecord::RecordInvalid,
)
expect { u.update!(dominant_color: "abcd") }.to raise_error(ActiveRecord::RecordInvalid)
end
end
describe ".mark_invalid_s3_uploads_as_missing" do
it "should update all upload records with a `verification_status` of `invalid_etag` to `s3_file_missing`" do
upload_1 =
Fabricate(:upload_s3, verification_status: Upload.verification_statuses[:invalid_etag])
upload_2 =
Fabricate(:upload_s3, verification_status: Upload.verification_statuses[:invalid_etag])
upload_3 = Fabricate(:upload_s3, verification_status: Upload.verification_statuses[:verified])
Upload.mark_invalid_s3_uploads_as_missing
expect(upload_1.reload.verification_status).to eq(
Upload.verification_statuses[:s3_file_missing_confirmed],
)
expect(upload_2.reload.verification_status).to eq(
Upload.verification_statuses[:s3_file_missing_confirmed],
)
expect(upload_3.reload.verification_status).to eq(Upload.verification_statuses[:verified])
end
end
2013-02-06 03:16:51 +08:00
end