image_vise 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,167 @@
1
+ require_relative '../spec_helper'
2
+ require 'rack/test'
3
+
4
+ describe ImageVise::RenderEngine do
5
+ include Rack::Test::Methods
6
+
7
+ let(:app) { ImageVise::RenderEngine.new }
8
+
9
+ context 'when the subclass is configured to raise exceptions' do
10
+ after :each do
11
+ ImageVise.reset_allowed_hosts!
12
+ ImageVise.reset_secret_keys!
13
+ end
14
+
15
+ it 'raises an exception instead of returning an error response' do
16
+ class << app
17
+ def raise_exceptions?
18
+ true
19
+ end
20
+ end
21
+
22
+ p = ImageVise::Pipeline.new.crop(width: 10, height: 10, gravity: 'c')
23
+ image_request = ImageVise::ImageRequest.new(src_url: 'http://unknown.com/image.jpg', pipeline: p)
24
+ params = image_request.to_query_string_params('l33tness')
25
+ expect(app).to receive(:handle_generic_error).and_call_original
26
+ expect {
27
+ get '/', params
28
+ }.to raise_error(/No keys set/)
29
+ end
30
+ end
31
+
32
+ context 'when requesting an image' do
33
+ after :each do
34
+ ImageVise.reset_allowed_hosts!
35
+ ImageVise.reset_secret_keys!
36
+ end
37
+
38
+ context 'halts with 422' do
39
+ before :each do
40
+ parsed_url = Addressable::URI.parse(public_url)
41
+ ImageVise.add_allowed_host!(parsed_url.host)
42
+ end
43
+
44
+ it 'when the requested image cannot be opened by ImageMagick' do
45
+ uri = Addressable::URI.parse(public_url)
46
+ ImageVise.add_allowed_host!(uri.host)
47
+ ImageVise.add_secret_key!('l33tness')
48
+ uri.path = '/___nonexistent_image.jpg'
49
+
50
+ p = ImageVise::Pipeline.new.crop(width: 10, height: 10, gravity: 'c')
51
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
52
+ params = image_request.to_query_string_params('l33tness')
53
+
54
+ expect_any_instance_of(Patron::Session).to receive(:get_file) {|_self, url, path|
55
+ File.open(path, 'wb') {|f| f << 'totally not an image' }
56
+ double(status: 200)
57
+ }
58
+ expect(app).to receive(:handle_request_error).and_call_original
59
+
60
+ get '/', params
61
+ expect(last_response.status).to eq(422)
62
+ expect(last_response['Cache-Control']).to eq("private, max-age=0, no-cache")
63
+ expect(last_response.body).to include('Unsupported/unknown')
64
+ end
65
+ end
66
+
67
+ it 'responds with 403 when upstream returns it' do
68
+ uri = Addressable::URI.parse(public_url)
69
+ ImageVise.add_allowed_host!(uri.host)
70
+ ImageVise.add_secret_key!('l33tness')
71
+ uri.path = '/forbidden'
72
+
73
+ p = ImageVise::Pipeline.new.crop(width: 10, height: 10, gravity: 'c')
74
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
75
+ params = image_request.to_query_string_params('l33tness')
76
+
77
+ get '/', params
78
+ expect(last_response.status).to eq(403)
79
+ expect(last_response.headers['Content-Type']).to eq('application/json')
80
+ parsed = JSON.load(last_response.body)
81
+ expect(parsed['errors']).to include("Unfortunate upstream response: 403")
82
+ end
83
+
84
+ it 'replays upstream error response codes that are selected to be replayed to the requester' do
85
+ uri = Addressable::URI.parse(public_url)
86
+ ImageVise.add_allowed_host!(uri.host)
87
+ ImageVise.add_secret_key!('l33tness')
88
+
89
+ [404, 403, 503, 504, 500].each do | error_code |
90
+ allow_any_instance_of(Patron::Session).to receive(:get_file).and_return(double(status: error_code))
91
+
92
+ p = ImageVise::Pipeline.new.crop(width: 10, height: 10, gravity: 'c')
93
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
94
+ params = image_request.to_query_string_params('l33tness')
95
+
96
+ get '/', params
97
+ expect(last_response.status).to eq(error_code)
98
+ expect(last_response.headers).to have_key('Cache-Control')
99
+ expect(last_response.headers['Cache-Control']).to eq("private, max-age=0, no-cache")
100
+
101
+ expect(last_response.headers['Content-Type']).to eq('application/json')
102
+ parsed = JSON.load(last_response.body)
103
+ expect(parsed['errors']).to include("Unfortunate upstream response: #{error_code}")
104
+ end
105
+ end
106
+
107
+ it 'sets very far caching headers and an ETag, and returns a 304 if any ETag is set' do
108
+ uri = Addressable::URI.parse(public_url)
109
+ ImageVise.add_allowed_host!(uri.host)
110
+ ImageVise.add_secret_key!('l33tness')
111
+
112
+ p = ImageVise::Pipeline.new.fit_crop(width: 10, height: 35, gravity: 'c')
113
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
114
+ params = image_request.to_query_string_params('l33tness')
115
+
116
+ get '/', params
117
+
118
+ expect(last_response).to be_ok
119
+ expect(last_response['ETag']).not_to be_nil
120
+ expect(last_response['Cache-Control']).to eq('public')
121
+
122
+ get '/', params, {'HTTP_IF_NONE_MATCH' => last_response['ETag']}
123
+ expect(last_response.status).to eq(304)
124
+
125
+ # Should consider _any_ ETag a request to rerender something
126
+ # that already exists in an upstream cache
127
+ get '/', params, {'HTTP_IF_NONE_MATCH' => SecureRandom.hex(4)}
128
+ expect(last_response.status).to eq(304)
129
+ end
130
+
131
+ it 'when all goes well responds with an image that passes through all the processing steps' do
132
+ uri = Addressable::URI.parse(public_url)
133
+ ImageVise.add_allowed_host!(uri.host)
134
+ ImageVise.add_secret_key!('l33tness')
135
+
136
+ p = ImageVise::Pipeline.new.geom(geometry_string: '512x335').fit_crop(width: 10, height: 10, gravity: 'c')
137
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
138
+ params = image_request.to_query_string_params('l33tness')
139
+
140
+ get '/', params
141
+ expect(last_response.status).to eq(200)
142
+
143
+ expect(last_response.headers['Content-Type']).to eq('image/jpeg')
144
+ expect(last_response.headers).to have_key('Content-Length')
145
+ parsed_image = Magick::Image.from_blob(last_response.body)[0]
146
+ expect(parsed_image.columns).to eq(10)
147
+ end
148
+
149
+ it 'returns the processed JPEG image as a PNG if it had to get an alpha channel during processing' do
150
+ uri = Addressable::URI.parse(public_url)
151
+ ImageVise.add_allowed_host!(uri.host)
152
+ ImageVise.add_secret_key!('l33tness')
153
+
154
+ p = ImageVise::Pipeline.new.geom(geometry_string: '220x220').ellipse_stencil
155
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
156
+ params = image_request.to_query_string_params('l33tness')
157
+
158
+ get '/', params
159
+ expect(last_response.status).to eq(200)
160
+
161
+ expect(last_response.headers['Content-Type']).to eq('image/png')
162
+ expect(last_response.headers).to have_key('Content-Length')
163
+
164
+ examine_image_from_string(last_response.body)
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe ImageVise::Sharpen do
4
+ it 'refuses invalid parameters' do
5
+ expect { described_class.new(sigma: 0, radius: -1) }.to raise_error(ArgumentError)
6
+ end
7
+
8
+ it 'applies the crop with different gravities' do
9
+ [[1, 1], [4, 2], [0.75, 0.5]].each do |(r, s)|
10
+ image = Magick::Image.read(test_image_path)[0]
11
+ expect(ImageVise).to receive(:destroy).with(instance_of(Magick::Image)).and_call_original
12
+ sharpen = described_class.new(radius: r, sigma: s)
13
+ sharpen.apply!(image)
14
+ examine_image(image, "sharpen-rad_%02f-sigma_%02f" % [r, s])
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,89 @@
1
+ require_relative 'spec_helper'
2
+ require 'rack/test'
3
+
4
+ describe ImageVise do
5
+ include Rack::Test::Methods
6
+
7
+ def app
8
+ described_class.new
9
+ end
10
+
11
+ context 'ImageVise.allowed_hosts' do
12
+ xit 'returns the allowed hosts' do
13
+ expect(described_class.allowed_hosts).not_to be_empty
14
+ expect(described_class.allowed_hosts).to include('wetransfer-unittests.s3.amazonaws.com')
15
+ end
16
+
17
+ it 'allows add_allowed_host! and reset_allowed_hosts!' do
18
+ described_class.add_allowed_host!('www.imageboard.im')
19
+ expect(described_class.allowed_hosts).to include('www.imageboard.im')
20
+ described_class.reset_allowed_hosts!
21
+ expect(described_class.allowed_hosts).not_to include('www.imageboard.im')
22
+ end
23
+ end
24
+
25
+ context 'ImageVise.secret_keys' do
26
+ it 'raises when asked for a key and no keys has been set' do
27
+ expect {
28
+ described_class.secret_keys
29
+ }.to raise_error("No keys set, add a key using `ImageVise.add_secret_key!(key)'")
30
+ end
31
+
32
+ it 'allows add_secret_key!(key) and reset_secret_keys!' do
33
+ described_class.add_secret_key!('l33t')
34
+ expect(described_class.secret_keys).to include('l33t')
35
+ described_class.reset_secret_keys!
36
+ expect {
37
+ expect(described_class.secret_keys)
38
+ }.to raise_error
39
+ end
40
+ end
41
+
42
+ describe 'ImageVise.new.call' do
43
+ it 'instantiates a new app and performs call() on it' do
44
+ expect_any_instance_of(ImageVise::RenderEngine).to receive(:call).with(:mock_env) { :yes }
45
+ ImageVise.new.call(:mock_env)
46
+ end
47
+ end
48
+
49
+ describe 'ImageVise.call' do
50
+ it 'instantiates a new app and performs call() on it' do
51
+ expect_any_instance_of(ImageVise::RenderEngine).to receive(:call).with(:mock_env) { :yes }
52
+ ImageVise.call(:mock_env)
53
+ end
54
+ end
55
+
56
+ describe '.image_params' do
57
+ it 'generates a Hash with paremeters for processing the resized image' do
58
+ params = ImageVise.image_params(src_url: 'http://host.com/image.jpg', secret: 'l33t') do |pipe|
59
+ pipe.fit_crop width: 128, height: 256, gravity: 'c'
60
+ end
61
+ expect(params).to be_kind_of(Hash)
62
+ expect(params[:q]).not_to be_empty
63
+ expect(params[:sig]).not_to be_empty
64
+ end
65
+ end
66
+
67
+ describe 'methods dealing with the operator list' do
68
+ it 'have the basic operators already set up' do
69
+ oplist = ImageVise.defined_operator_names
70
+ expect(oplist).to include('sharpen')
71
+ expect(oplist).to include('crop')
72
+ end
73
+
74
+ it 'allows an operator to be added and retrieved' do
75
+ class CustomOp; end
76
+ ImageVise.add_operator 'custom_op', CustomOp
77
+ expect(ImageVise.operator_from(:custom_op)).to eq(CustomOp)
78
+ expect(ImageVise.operator_name_for(CustomOp.new)).to eq('custom_op')
79
+ expect(ImageVise.defined_operator_names).to include('custom_op')
80
+ end
81
+
82
+ it 'raises an exception when an operator key is requested that does not exist' do
83
+ class UnknownOp; end
84
+ expect {
85
+ ImageVise.operator_name_for(UnknownOp.new)
86
+ }.to raise_error(/not registered using ImageVise/)
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,74 @@
1
+ require 'bundler'
2
+ Bundler.require
3
+
4
+ require 'addressable/uri'
5
+ require 'strenv'
6
+ require 'tmpdir'
7
+ require_relative 'test_server'
8
+
9
+
10
+ TEST_RENDERS_DIR = Dir.mktmpdir
11
+
12
+ module Examine
13
+ def examine_image(magick_image, name_tag = 'test-img')
14
+ # When doing TDD, waiting for stuff to open is a drag - allow
15
+ # it to be squelched using 2 envvars. Also viewing images
16
+ # makes no sense on CI unless we bother with artifacts.
17
+ # The first one is what Gitlab-CI sets for us.
18
+ return if ENV.key?("CI_BUILD_ID")
19
+ return if ENV.key?("SKIP_INTERACTIVE")
20
+
21
+ Dir.mkdir(TEST_RENDERS_DIR) unless File.exist?(TEST_RENDERS_DIR)
22
+ path = File.join(TEST_RENDERS_DIR, name_tag + '.png')
23
+ magick_image.format = 'png'
24
+ magick_image.write(path)
25
+ `open #{path}`
26
+ end
27
+
28
+ def examine_image_from_string(string)
29
+ # When doing TDD, waiting for stuff to open is a drag - allow
30
+ # it to be squelched using 2 envvars. Also viewing images
31
+ # makes no sense on CI unless we bother with artifacts.
32
+ # The first one is what Gitlab-CI sets for us.
33
+ return if ENV.key?("CI_BUILD_ID")
34
+ return if ENV.key?("SKIP_INTERACTIVE")
35
+
36
+ Dir.mkdir(TEST_RENDERS_DIR) unless File.exist?(TEST_RENDERS_DIR)
37
+ random_name = 'test-image-%s' % SecureRandom.hex(3)
38
+ path = File.join(TEST_RENDERS_DIR, random_name)
39
+ File.open(path, 'wb'){|f| f << string }
40
+ `open #{path}`
41
+ end
42
+ end
43
+
44
+ require 'simplecov'
45
+ SimpleCov.start do
46
+ add_filter "/spec/"
47
+ end
48
+
49
+ require_relative '../lib/image_vise'
50
+
51
+ RSpec.configure do | config |
52
+ config.order = 'random'
53
+ config.include Examine
54
+ config.before :suite do
55
+ TestServer.start(nil, ssl=false, port=9001)
56
+ end
57
+
58
+ config.after :suite do
59
+ sleep 2
60
+ FileUtils.rm_rf(TEST_RENDERS_DIR)
61
+ end
62
+
63
+ def test_image_path
64
+ File.expand_path(__dir__ + '/waterside_magic_hour.jpg')
65
+ end
66
+
67
+ def public_url
68
+ 'http://localhost:9001/waterside_magic_hour.jpg'
69
+ end
70
+
71
+ config.around :each do |e|
72
+ STRICT_ENV.with_protected_env { e.run }
73
+ end
74
+ end
@@ -0,0 +1,61 @@
1
+ require 'webrick'
2
+ include WEBrick
3
+
4
+ class ForbiddenServlet < HTTPServlet::AbstractServlet
5
+ def do_GET(req,res)
6
+ res['Content-Type'] = "text/plain"
7
+ res.status = 403
8
+ end
9
+ end
10
+
11
+ class TestServer
12
+ def self.start( log_file = nil, ssl = false, port = 9001 )
13
+ new(log_file, ssl, port).start
14
+ end
15
+
16
+ def initialize( log_file = nil, ssl = false, port = 9001 )
17
+ log_file ||= StringIO.new
18
+ log = WEBrick::Log.new(log_file)
19
+
20
+ options = {
21
+ :Port => port,
22
+ :Logger => log,
23
+ :AccessLog => [
24
+ [ log, WEBrick::AccessLog::COMMON_LOG_FORMAT ],
25
+ [ log, WEBrick::AccessLog::REFERER_LOG_FORMAT ]
26
+ ],
27
+ :DocumentRoot => File.expand_path(__dir__),
28
+ }
29
+
30
+ if ssl
31
+ options[:SSLEnable] = true
32
+ options[:SSLCertificate] = OpenSSL::X509::Certificate.new(File.open("spec/certs/cacert.pem").read)
33
+ options[:SSLPrivateKey] = OpenSSL::PKey::RSA.new(File.open("spec/certs/privkey.pem").read)
34
+ options[:SSLCertName] = [ ["CN", WEBrick::Utils::getservername ] ]
35
+ end
36
+
37
+ @server = WEBrick::HTTPServer.new(options)
38
+ @server.mount("/forbidden", ForbiddenServlet)
39
+ end
40
+
41
+ def start
42
+ trap('INT') {
43
+ begin
44
+ @server.shutdown unless @server.nil?
45
+ rescue Object => e
46
+ $stderr.puts "Error #{__FILE__}:#{__LINE__}\n#{e.message}"
47
+ end
48
+ }
49
+
50
+ @thread = Thread.new { @server.start }
51
+ Thread.pass
52
+ self
53
+ end
54
+
55
+ def join
56
+ if defined? @thread and @thread
57
+ @thread.join
58
+ end
59
+ self
60
+ end
61
+ end
Binary file