mirror of
https://github.com/discourse/discourse.git
synced 2024-11-30 15:24:05 +08:00
494379201d
Spec was checking implementation when it did not need to, temp file was blank so optimized image should be blank
388 lines
11 KiB
Ruby
388 lines
11 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
describe OptimizedImage do
|
|
let(:upload) { build(:upload) }
|
|
before { upload.id = 42 }
|
|
|
|
unless ENV["TRAVIS"]
|
|
describe '.crop' do
|
|
it 'should produce cropped images (requires ImageMagick 7)' do
|
|
tmp_path = "/tmp/cropped.png"
|
|
|
|
begin
|
|
OptimizedImage.crop(
|
|
"#{Rails.root}/spec/fixtures/images/logo.png",
|
|
tmp_path,
|
|
5,
|
|
5
|
|
)
|
|
|
|
# we don't want to deal with something new here every time image magick
|
|
# is upgraded or pngquant is upgraded, lets just test the basics ...
|
|
# cropped image should be less than 120 bytes
|
|
|
|
cropped_size = File.size(tmp_path)
|
|
|
|
expect(cropped_size).to be < 120
|
|
expect(cropped_size).to be > 50
|
|
|
|
ensure
|
|
File.delete(tmp_path) if File.exists?(tmp_path)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe ".resize_instructions" do
|
|
let(:image) { "#{Rails.root}/spec/fixtures/images/logo.png" }
|
|
|
|
it "doesn't return any color options by default" do
|
|
instructions = described_class.resize_instructions(image, image, "50x50")
|
|
expect(instructions).to_not include('-colors')
|
|
end
|
|
|
|
it "supports an optional color option" do
|
|
instructions = described_class.resize_instructions(image, image, "50x50", colors: 12)
|
|
expect(instructions).to include('-colors')
|
|
end
|
|
|
|
end
|
|
|
|
describe '.resize' do
|
|
it 'should work correctly when extension is bad' do
|
|
|
|
original_path = Dir::Tmpname.create(['origin', '.bin']) { nil }
|
|
|
|
begin
|
|
FileUtils.cp "#{Rails.root}/spec/fixtures/images/logo.png", original_path
|
|
|
|
# we use "filename" to get the correct extension here, it is more important
|
|
# then any other param
|
|
|
|
orig_size = File.size(original_path)
|
|
|
|
OptimizedImage.resize(
|
|
original_path,
|
|
original_path,
|
|
5,
|
|
5,
|
|
filename: "test.png"
|
|
)
|
|
|
|
new_size = File.size(original_path)
|
|
expect(orig_size).to be > new_size
|
|
expect(new_size).not_to eq(0)
|
|
|
|
ensure
|
|
File.delete(original_path) if File.exists?(original_path)
|
|
end
|
|
end
|
|
|
|
it 'should work correctly' do
|
|
|
|
file = File.open("#{Rails.root}/spec/fixtures/images/resized.png")
|
|
upload = UploadCreator.new(file, "test.bin").create_for(-1)
|
|
|
|
expect(upload.filesize).to eq(199)
|
|
|
|
expect(upload.width).to eq(5)
|
|
expect(upload.height).to eq(5)
|
|
|
|
upload.create_thumbnail!(10, 10)
|
|
thumb = upload.thumbnail(10, 10)
|
|
|
|
expect(thumb.width).to eq(10)
|
|
expect(thumb.height).to eq(10)
|
|
|
|
# very image magic specific so fudge here
|
|
expect(thumb.filesize).to be > 200
|
|
|
|
# this size is based off original upload
|
|
# it is the size we render, by default, in the post
|
|
expect(upload.thumbnail_width).to eq(5)
|
|
expect(upload.thumbnail_height).to eq(5)
|
|
|
|
# lets ensure we can rebuild the filesize
|
|
thumb.update_columns(filesize: nil)
|
|
thumb = OptimizedImage.find(thumb.id)
|
|
|
|
# attempts to auto correct
|
|
expect(thumb.filesize).to be > 200
|
|
end
|
|
|
|
describe 'when an svg with a href is masked as a png' do
|
|
it 'should not trigger the external request' do
|
|
tmp_path = "/tmp/resized.png"
|
|
|
|
begin
|
|
expect do
|
|
OptimizedImage.resize(
|
|
"#{Rails.root}/spec/fixtures/images/svg.png",
|
|
tmp_path,
|
|
5,
|
|
5,
|
|
raise_on_error: true
|
|
)
|
|
end.to raise_error(RuntimeError, /improper image header/)
|
|
ensure
|
|
File.delete(tmp_path) if File.exists?(tmp_path)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '.downsize' do
|
|
it 'should downsize logo (requires ImageMagick 7)' do
|
|
tmp_path = "/tmp/downsized.png"
|
|
|
|
begin
|
|
OptimizedImage.downsize(
|
|
"#{Rails.root}/spec/fixtures/images/logo.png",
|
|
tmp_path,
|
|
"100x100\>"
|
|
)
|
|
|
|
info = FastImage.new(tmp_path)
|
|
expect(info.size).to eq([100, 27])
|
|
expect(File.size(tmp_path)).to be < 2300
|
|
|
|
ensure
|
|
File.delete(tmp_path) if File.exists?(tmp_path)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe ".safe_path?" do
|
|
|
|
it "correctly detects unsafe paths" do
|
|
expect(OptimizedImage.safe_path?("/path/A-AA/22_00.JPG")).to eq(true)
|
|
expect(OptimizedImage.safe_path?("/path/AAA/2200.JPG")).to eq(true)
|
|
expect(OptimizedImage.safe_path?("/tmp/a.png")).to eq(true)
|
|
expect(OptimizedImage.safe_path?("../a.png")).to eq(false)
|
|
expect(OptimizedImage.safe_path?("/tmp/a.png\\test")).to eq(false)
|
|
expect(OptimizedImage.safe_path?("/tmp/a.png\\test")).to eq(false)
|
|
expect(OptimizedImage.safe_path?("/path/\u1000.png")).to eq(false)
|
|
expect(OptimizedImage.safe_path?("/path/x.png\n")).to eq(false)
|
|
expect(OptimizedImage.safe_path?("/path/x.png\ny.png")).to eq(false)
|
|
expect(OptimizedImage.safe_path?("/path/x.png y.png")).to eq(false)
|
|
expect(OptimizedImage.safe_path?(nil)).to eq(false)
|
|
end
|
|
|
|
end
|
|
|
|
describe "ensure_safe_paths!" do
|
|
it "raises nothing on safe paths" do
|
|
expect {
|
|
OptimizedImage.ensure_safe_paths!("/a.png", "/b.png")
|
|
}.not_to raise_error
|
|
end
|
|
|
|
it "raises InvalidAccess error on paths" do
|
|
expect {
|
|
OptimizedImage.ensure_safe_paths!("/a.png", "/b.png", "c.png")
|
|
}.to raise_error(Discourse::InvalidAccess)
|
|
end
|
|
end
|
|
|
|
describe ".local?" do
|
|
|
|
def local(url)
|
|
OptimizedImage.new(url: url).local?
|
|
end
|
|
|
|
it "correctly detects local vs remote" do
|
|
expect(local("//hello")).to eq(false)
|
|
expect(local("http://hello")).to eq(false)
|
|
expect(local("https://hello")).to eq(false)
|
|
expect(local("https://hello")).to eq(false)
|
|
expect(local("/hello")).to eq(true)
|
|
end
|
|
end
|
|
|
|
describe ".create_for" do
|
|
|
|
context "versioning" do
|
|
let(:filename) { 'logo.png' }
|
|
let(:file) { file_from_fixtures(filename) }
|
|
|
|
it "is able to update optimized images on version change" do
|
|
upload = UploadCreator.new(file, filename).create_for(Discourse.system_user.id)
|
|
optimized = OptimizedImage.create_for(upload, 10, 10)
|
|
|
|
expect(optimized.version).to eq(OptimizedImage::VERSION)
|
|
|
|
optimized_again = OptimizedImage.create_for(upload, 10, 10)
|
|
expect(optimized_again.id).to eq(optimized.id)
|
|
|
|
optimized.update_columns(version: nil)
|
|
old_id = optimized.id
|
|
|
|
optimized_new = OptimizedImage.create_for(upload, 10, 10)
|
|
|
|
expect(optimized_new.id).not_to eq(old_id)
|
|
|
|
# cleanup (which transaction rollback may miss)
|
|
optimized_new.destroy
|
|
upload.destroy
|
|
end
|
|
end
|
|
|
|
it "is able to 'optimize' an svg" do
|
|
# we don't really optimize anything, we simply copy
|
|
# but at least this confirms this actually works
|
|
|
|
SiteSetting.authorized_extensions = 'svg'
|
|
svg = file_from_fixtures('image.svg')
|
|
upload = UploadCreator.new(svg, 'image.svg').create_for(Discourse.system_user.id)
|
|
resized = upload.get_optimized_image(50, 50, {})
|
|
|
|
# we perform some basic svg mangling but expect the string Discourse to be there
|
|
expect(File.read(Discourse.store.path_for(resized))).to include("Discourse")
|
|
expect(File.read(Discourse.store.path_for(resized))).to eq(File.read(Discourse.store.path_for(upload)))
|
|
end
|
|
|
|
context "when using an internal store" do
|
|
|
|
let(:store) { FakeInternalStore.new }
|
|
before { Discourse.stubs(:store).returns(store) }
|
|
|
|
context "when an error happened while generating the thumbnail" do
|
|
|
|
it "returns nil" do
|
|
OptimizedImage.expects(:resize).returns(false)
|
|
expect(OptimizedImage.create_for(upload, 100, 200)).to eq(nil)
|
|
end
|
|
|
|
end
|
|
|
|
context "when the thumbnail is properly generated" do
|
|
|
|
before do
|
|
OptimizedImage.expects(:resize).returns(true)
|
|
end
|
|
|
|
it "does not download a copy of the original image" do
|
|
store.expects(:download).never
|
|
OptimizedImage.create_for(upload, 100, 200)
|
|
end
|
|
|
|
it "closes and removes the tempfile" do
|
|
Tempfile.any_instance.expects(:close!)
|
|
OptimizedImage.create_for(upload, 100, 200)
|
|
end
|
|
|
|
it "works" do
|
|
oi = OptimizedImage.create_for(upload, 100, 200)
|
|
expect(oi.sha1).to eq("da39a3ee5e6b4b0d3255bfef95601890afd80709")
|
|
expect(oi.extension).to eq(".png")
|
|
expect(oi.width).to eq(100)
|
|
expect(oi.height).to eq(200)
|
|
expect(oi.url).to eq("/internally/stored/optimized/image.png")
|
|
end
|
|
|
|
it "is able to change the format" do
|
|
oi = OptimizedImage.create_for(upload, 100, 200, format: 'gif')
|
|
expect(oi.url).to eq("/internally/stored/optimized/image.gif")
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
describe "external store" do
|
|
let(:s3_upload) { Fabricate(:upload_s3) }
|
|
|
|
before do
|
|
SiteSetting.enable_s3_uploads = true
|
|
SiteSetting.s3_upload_bucket = "s3-upload-bucket"
|
|
SiteSetting.s3_access_key_id = "some key"
|
|
SiteSetting.s3_secret_access_key = "some secret key"
|
|
SiteSetting.s3_region = "us-east-1"
|
|
|
|
tempfile = Tempfile.new(["discourse-external", ".png"])
|
|
|
|
%i{head get}.each do |method|
|
|
stub_request(method, "http://#{s3_upload.url}")
|
|
.to_return(
|
|
status: 200,
|
|
body: tempfile.read
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when we have a bad file returned" do
|
|
it "returns nil" do
|
|
# tempfile is empty
|
|
# this can not be resized
|
|
expect(OptimizedImage.create_for(s3_upload, 100, 200)).to eq(nil)
|
|
end
|
|
end
|
|
|
|
context "when the thumbnail is properly generated" do
|
|
before do
|
|
OptimizedImage.expects(:resize).returns(true)
|
|
end
|
|
|
|
it "downloads a copy of the original image" do
|
|
optimized_path = "/optimized/1X/#{s3_upload.sha1}_2_100x200.png"
|
|
|
|
stub_request(
|
|
:head,
|
|
"https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com/"
|
|
)
|
|
|
|
stub_request(
|
|
:put,
|
|
"https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com#{optimized_path}"
|
|
).to_return(
|
|
status: 200,
|
|
headers: { "ETag" => "someetag" }
|
|
)
|
|
|
|
oi = OptimizedImage.create_for(s3_upload, 100, 200)
|
|
|
|
expect(oi.sha1).to eq("da39a3ee5e6b4b0d3255bfef95601890afd80709")
|
|
expect(oi.extension).to eq(".png")
|
|
expect(oi.width).to eq(100)
|
|
expect(oi.height).to eq(200)
|
|
expect(oi.url).to eq("//#{SiteSetting.s3_upload_bucket}.s3.dualstack.us-east-1.amazonaws.com#{optimized_path}")
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
describe '#destroy' do
|
|
describe 'when upload_id is no longer valid' do
|
|
it 'should still destroy the record' do
|
|
image = Fabricate(:optimized_image)
|
|
image.upload.delete
|
|
image.reload.destroy
|
|
|
|
expect(OptimizedImage.exists?(id: image.id)).to eq(false)
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
class FakeInternalStore
|
|
|
|
def external?
|
|
false
|
|
end
|
|
|
|
def path_for(upload)
|
|
upload.url
|
|
end
|
|
|
|
def store_optimized_image(file, optimized_image, content_type = nil, secure: false)
|
|
"/internally/stored/optimized/image#{optimized_image.extension}"
|
|
end
|
|
|
|
end
|