5baed87d2d61a38948fbb450bf4dbc5e20012435
[coquelicot.git] / spec / coquelicot / app_spec.rb
1 # -*- coding: UTF-8 -*-
2 # Coquelicot: "one-click" file sharing with a focus on users' privacy.
3 # Copyright © 2010-2013 potager.org <jardiniers@potager.org>
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU Affero General Public License for more details.
14 #
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 require 'spec_helper'
19 require 'coquelicot/jyraphe_migrator'
20 require 'capybara/dsl'
21 require 'tempfile'
22
23 describe Coquelicot::Application do
24   include Rack::Test::Methods
25   include Capybara::DSL
26   Capybara.app = Coquelicot::Application
27
28   include_context 'with Coquelicot::Application'
29
30   def upload_password
31     'secret'
32   end
33
34   before(:each) do
35     app.set :authentication_method, :name => :simplepass,
36                                     :upload_password => Digest::SHA1.hexdigest(upload_password)
37   end
38
39   shared_context 'browser prefers french' do
40     around do |example|
41       begin
42         page.driver.header 'Accept-Language',  'fr-fr;q=1.0, en-gb;q=0.8, en;q=0.7'
43         example.run
44       ensure
45         page.driver.header 'Accept-Language', nil
46         reset_session!
47       end
48     end
49   end
50
51   describe 'get /' do
52     context 'using the default language' do
53       it 'should display the maximum file size' do
54         visit '/'
55         find(:xpath, '//label[@for="file"]').
56             should have_content("max. size: #{Coquelicot.settings.max_file_size.as_size}")
57       end
58       context 'when an "about text" is set"' do
59         before(:each) do
60           app.set :about_text, 'This is an about text'
61         end
62         it 'should display the "about text"' do
63           visit '/'
64           page.should have_content('This is an about text')
65         end
66       end
67       context 'when I explicitly request french' do
68         it 'should display a page in french' do
69           visit '/'
70           click_link 'fr'
71           page.should have_content('Partager')
72           reset_session!
73         end
74         # will fail without ordered Hash, see:
75         # <https://github.com/jnicklas/capybara/issues/670>
76         context 'when I upload an empty file', :if => RUBY_VERSION >= '1.9' do
77           around do |example|
78             file = Tempfile.new('coquelicot')
79             begin
80               visit '/'
81               click_link 'fr'
82               fill_in 'upload_password', :with => upload_password
83               attach_file 'file', file.path
84               click_button 'submit'
85               example.run
86             ensure
87               file.close!
88               reset_session!
89             end
90           end
91           it 'should display an error in french' do
92             page.should have_content('Le fichier est vide')
93           end
94         end
95       end
96     end
97     context 'when my browser prefers french' do
98       include_context 'browser prefers french'
99       context 'when I do nothing special' do
100         it 'should display a page in french' do
101           visit '/'
102           page.should have_content('Partager')
103         end
104         context 'when the max upload size is 1 KiB' do
105           around do |example|
106             begin
107               max_file_size = app.max_file_size
108               app.set :max_file_size, 1024
109               example.run
110             ensure
111               app.set :max_file_size, max_file_size
112             end
113           end
114           it 'should display "1 Kio" as the max upload size' do
115             visit '/'
116             page.should have_content('1 Kio')
117           end
118           # will fail without ordered Hash, see:
119           # <https://github.com/jnicklas/capybara/issues/670>
120           context 'when I upload something bigger', :if => RUBY_VERSION >= '1.9' do
121             before do
122               visit '/'
123               fill_in 'upload_password', :with => upload_password
124               attach_file 'file', __FILE__
125               click_button 'submit'
126             end
127             it 'should display an error in french' do
128               page.should have_content('plus gros que la taille maximale')
129             end
130           end
131         end
132         # will fail without ordered Hash, see:
133         # <https://github.com/jnicklas/capybara/issues/670>
134         context 'when I upload an empty file', :if => RUBY_VERSION >= '1.9' do
135           around do |example|
136             file = Tempfile.new('coquelicot')
137             begin
138               visit '/'
139               fill_in 'upload_password', :with => upload_password
140               attach_file 'file', file.path
141               click_button 'submit'
142               example.run
143             ensure
144               file.close!
145             end
146           end
147           it 'should display an error in french' do
148             page.should have_content('Le fichier est vide')
149           end
150         end
151       end
152       context 'when I explicitly request german' do
153         around(:each) do |example|
154           visit '/'
155           click_link 'de'
156           example.run
157           reset_session!
158         end
159         it 'should display a page in german' do
160           page.should have_content('Verteile')
161         end
162         # will fail without ordered Hash, see:
163         # <https://github.com/jnicklas/capybara/issues/670>
164         context 'after an upload', :if => RUBY_VERSION >= '1.9' do
165           before do
166             fill_in 'upload_password', :with => upload_password
167             attach_file 'file', __FILE__
168             click_button 'submit'
169           end
170           it 'should display a page in german' do
171             page.should have_content('Verteile eine weitere Datei')
172           end
173         end
174       end
175     end
176   end
177
178   describe 'get /README' do
179     before do
180       visit '/README'
181     end
182     it 'should display the README file' do
183       title = File.open(File.expand_path('../../../README', __FILE__)) { |f| f.readline.strip }
184       find('h1').should have_content(title)
185     end
186   end
187
188   describe 'get /about-your-data' do
189     it 'should display some info about data retention' do
190       visit '/about-your-data'
191       find('h1').should have_content('About your data…')
192     end
193     context 'when using SSL' do
194       it 'should notice the connection is encrypted' do
195         visit 'https://example.com/about-your-data'
196         page.should have_content('Exchanges between your computer and example.com are encrypted.')
197       end
198     end
199     context 'when not using SSL' do
200       it 'should notice the connection is encrypted' do
201         visit 'http://example.com/about-your-data'
202         page.should_not have_content('Exchanges between your computer and example.org are encrypted.')
203       end
204     end
205   end
206
207   describe 'post /authenticate' do
208     context 'when given a request with too much input' do
209       before do
210         # README is bigger than 5 kiB
211         path = File.expand_path('../../../README', __FILE__)
212         post '/authenticate', :file => Rack::Test::UploadedFile.new(path, 'text/plain')
213       end
214       it 'should get status 413 (Request entity too large)' do
215         last_response.status.should == 413
216       end
217     end
218   end
219 end
220
221 describe Coquelicot, '.run!' do
222   include_context 'with Coquelicot::Application'
223
224   context 'when given no option' do
225     it 'should display help and exit' do
226       stderr = capture(:stderr) do
227         expect { Coquelicot.run! %w{} }.to raise_error(SystemExit)
228       end
229       stderr.should =~ /Usage:/
230     end
231   end
232   context 'when using "-h"' do
233     it 'should display help and exit' do
234       stderr = capture(:stderr) do
235         expect { Coquelicot.run! %w{-h} }.to raise_error(SystemExit)
236       end
237       stderr.should =~ /Usage:/
238     end
239   end
240   context 'when using "-c <settings.yml>"' do
241     it 'should use the given setting file' do
242       settings_file = File.expand_path('../../../conf/settings-default.yml', __FILE__)
243       Coquelicot::Application.should_receive(:config_file).with(settings_file)
244       stderr = capture(:stderr) do
245         expect { Coquelicot.run! ['-c', settings_file] }.to raise_error(SystemExit)
246       end
247     end
248     context 'when the given settings file exists' do
249       around(:each) do |example|
250         settings = Tempfile.new('coquelicot')
251         begin
252           settings.write(YAML.dump({ 'depot_path' => '/nonexistent' }))
253           settings.close
254           @settings_path = settings.path
255           example.run
256         ensure
257           settings.unlink
258         end
259       end
260       it 'should use the depot path defined in the given settings' do
261         # We don't give a command, so exit is expected
262         stderr = capture(:stderr) do
263           expect { Coquelicot.run! ['-c', @settings_path] }.to raise_error(SystemExit)
264         end
265         Coquelicot.settings.depot_path.should == '/nonexistent'
266       end
267     end
268     context 'when the given settings file does not exist' do
269       it 'should display an error' do
270         stderr = capture(:stderr) do
271           expect { Coquelicot.run! %w{-c non-existent.yml} }.to raise_error(SystemExit)
272         end
273         stderr.should =~ /cannot access/
274       end
275     end
276   end
277   context 'when given an invalid option' do
278     it 'should display an error' do
279       stderr = capture(:stderr) do
280         expect { Coquelicot.run! %w{--invalid-option} }.to raise_error(SystemExit)
281       end
282       stderr.should =~ /not a valid option/
283     end
284   end
285   context 'when given "whatever"' do
286     it 'should display an error' do
287       stderr = capture(:stderr) do
288         expect { Coquelicot.run! %w{whatever} }.to raise_error(SystemExit)
289       end
290       stderr.should =~ /not a valid command/
291     end
292   end
293   shared_context 'command accepts options' do
294     context 'when given "--help" option' do
295       it 'should display help and exit' do
296         stderr = capture(:stderr) do
297           expect { Coquelicot.run!([command, '--help']) }.to raise_error(SystemExit)
298         end
299         stderr.should =~ /Usage:/
300       end
301     end
302     context 'when given an invalid option' do
303       it 'should display an error' do
304         stderr = capture(:stderr) do
305           expect { Coquelicot.run!([command, '--invalid-option']) }.to raise_error(SystemExit)
306         end
307         stderr.should =~ /not a valid option/
308       end
309     end
310   end
311   context 'when given "start"' do
312     let(:command) { 'start' }
313     include_context 'command accepts options'
314
315     before(:each) do
316       # :stdout_path and :stderr_path should not be set, otherwise RSpec will break!
317       app.set :log, nil
318     end
319     context 'with default options' do
320       it 'should daemonize' do
321         ::Unicorn::Launcher.should_receive(:daemonize!)
322         ::Rainbows::HttpServer.stub(:new).and_return(double('HttpServer').as_null_object)
323         Coquelicot.run! %w{start}
324       end
325       it 'should start the web server' do
326         ::Unicorn::Launcher.stub(:daemonize!)
327         server = double('HttpServer')
328         server.should_receive(:start).and_return(double('Thread').as_null_object)
329         ::Rainbows::HttpServer.stub(:new).and_return(server)
330         Coquelicot.run! %w{start}
331       end
332     end
333     context 'when given the --no-daemon option' do
334       it 'should not daemonize' do
335         ::Unicorn::Launcher.should_receive(:daemonize!).never
336         ::Rainbows::HttpServer.stub(:new).and_return(double('HttpServer').as_null_object)
337         Coquelicot.run! %w{start --no-daemon}
338       end
339       it 'should set the default configuration' do
340         app.set :pid, @depot_path
341         app.set :listen, ['127.0.0.1:42']
342         ::Rainbows::HttpServer.any_instance.stub(:start) do
343           server = ::Rainbows.server
344           server.config.set[:pid].should == @depot_path
345           server.config.set[:listeners].should == ['127.0.0.1:42']
346           double('Thread').as_null_object
347         end
348         Coquelicot.run! %w{start --no-daemon}
349       end
350       it 'should start the web server' do
351         server = double('HttpServer')
352         server.should_receive(:start).and_return(double('Thread').as_null_object)
353         ::Rainbows::HttpServer.stub(:new).and_return(server)
354         Coquelicot.run! %w{start --no-daemon}
355       end
356     end
357   end
358   context 'when given "stop"' do
359     let(:command) { 'stop' }
360     include_context 'command accepts options'
361
362     context 'when the pid file is correct' do
363       let(:pid) { 42 }
364       before(:each) do
365         File.open("#{@depot_path}/pid", 'w') do |f|
366           f.write(pid.to_s)
367         end
368         app.set :pid, "#{@depot_path}/pid"
369       end
370       it 'should stop the web server' do
371         Process.should_receive(:kill).with(:TERM, pid)
372         Coquelicot.run! %w{stop}
373       end
374     end
375     context 'when the pid file does not exist' do
376       it 'should error out' do
377         app.set :pid, '/nonexistent'
378         stderr = capture(:stderr) do
379           expect { Coquelicot.run! %w{stop} }.to raise_error(SystemExit)
380         end
381         stderr.should =~ /Unable to read/
382       end
383     end
384     context 'when the pid file contains garbage' do
385       before(:each) do
386         File.open("#{@depot_path}/pid", 'w') do |f|
387           f.write('The queerest of the queer')
388         end
389         app.set :pid, "#{@depot_path}/pid"
390       end
391       it 'should errour out' do
392         stderr = capture(:stderr) do
393           expect { Coquelicot.run! %w{stop} }.to raise_error(SystemExit)
394         end
395         stderr.should =~ /Bad PID file/
396       end
397     end
398   end
399   context 'when given "gc"' do
400     let(:command) { 'gc' }
401     include_context 'command accepts options'
402
403     it 'should call gc!' do
404       Coquelicot.depot.should_receive(:gc!).once
405       Coquelicot.run! %w{gc}
406     end
407   end
408   context 'when given "migrate-jyraphe"' do
409     let(:args) { %w{all args} }
410     it 'should call the migrator' do
411       Coquelicot::JyrapheMigrator.should_receive(:run!).with(args)
412       Coquelicot.run!(%w{migrate-jyraphe} + args)
413     end
414   end
415 end