Merge remote branch 'nadir/translations/de'
[coquelicot.git] / spec / coquelicot_spec.rb
1 # Coquelicot: "one-click" file sharing with a focus on users' privacy.
2 # Copyright © 2010-2012 potager.org <jardiniers@potager.org>
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as
6 # published by the Free Software Foundation, either version 3 of the
7 # License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 ENV['RACK_ENV'] = 'test'
18
19 require 'rubygems'
20 require 'bundler'
21 Bundler.setup
22
23 require 'rack/test'
24 require 'rspec'
25 require 'timecop'
26 require 'hpricot'
27 require 'tmpdir'
28
29 require 'coquelicot'
30
31 UPLOAD_PASSWORD = 'secret'
32
33 describe 'Coquelicot' do
34   include Rack::Test::Methods
35
36   def app
37     Coquelicot::Application
38   end
39
40   def upload(opts={})
41     opts = { :file => Rack::Test::UploadedFile.new(__FILE__, 'text/x-script.ruby'),
42              :upload_password => UPLOAD_PASSWORD
43            }.merge(opts)
44     post '/upload', opts
45     return nil unless last_response.redirect?
46     follow_redirect!
47     last_response.should be_ok
48     doc = Hpricot(last_response.body)
49     return (doc/'a').collect { |a| a.attributes['href'] }.
50              select { |h| h.start_with? "http://#{last_request.host}/" }[0]
51   end
52
53   before do
54     app.set :environment, :test
55   end
56
57   around(:each) do |example|
58     path = Dir.mktmpdir('coquelicot')
59     begin
60       app.set :depot_path, path
61       example.run
62     ensure
63       FileUtils.remove_entry_secure Coquelicot.depot.path
64     end
65   end
66
67   it "should offer an upload form" do
68     get '/'
69     last_response.should be_ok
70     doc = Hpricot(last_response.body)
71     (doc/"form#upload").should have(1).items
72   end
73
74   context "when I explicitely ask for french" do
75     it "should offer an upload form in french" do
76       get '/', :lang => 'fr'
77       last_response.should be_ok
78       doc = Hpricot(last_response.body)
79       (doc/"input.submit").attr('value').should == 'Partager !'
80     end
81   end
82
83   context "when using 'simpleauth' authentication mechanism" do
84     before(:each) do
85       app.set :authentication_method, :name => :simplepass,
86                                       :upload_password => Digest::SHA1.hexdigest(UPLOAD_PASSWORD)
87     end
88
89     context "after a successful upload" do
90       before(:each) do
91         @url = upload
92       end
93
94       it "should not store the file in cleartext" do
95         files = Dir.glob("#{Coquelicot.depot.path}/*")
96         files.should have(1).items
97         File.new(files[0]).read().should_not include('should not store an uploaded file')
98       end
99
100       it "should generate a random URL to download the file" do
101         @url.should_not include(File.basename(__FILE__))
102       end
103
104       it "should store the file with a different name than the one in URL" do
105         url_name = @url.split('/')[-1]
106         files = Dir.glob("#{Coquelicot.depot.path}/*")
107         files.should have(1).items
108         url_name.should_not eql(File.basename(files[0]))
109       end
110
111       it "should encode the encryption key in URL as no password has been specified" do
112         url_name = @url.split('/')[-1]
113         url_name.split('-').should have(2).items
114       end
115
116       it "should download when using extra Base32 characters in URL" do
117         splitted = @url.split('/')
118         name = splitted[-1].upcase.gsub(/O/, '0').gsub(/L/, '1')
119         get "#{splitted[0..-2].join '/'}/#{name}"
120         last_response.should be_ok
121         last_response['Content-Type'].should eql('text/x-script.ruby')
122       end
123
124       context "when the file has been downloaded" do
125         before(:each) do
126           get @url
127         end
128
129         it "should be the same file as the uploaded" do
130           last_response.should be_ok
131           last_response['Content-Type'].should eql('text/x-script.ruby')
132           last_response.body.should eql(File.new(__FILE__).read)
133         end
134
135         it "should always has the same Last-Modified header" do
136           last_modified = last_response['Last-Modified']
137           last_modified.should_not be_nil
138           get @url
139           last_response['Last-Modified'].should eql(last_modified)
140         end
141       end
142     end
143
144     context "given an empty file" do
145       before do
146         @empty_file = Tempfile.new('empty')
147       end
148       it "should not be accepted when uploaded" do
149         url = upload :file => Rack::Test::UploadedFile.new(@empty_file.path, 'text/plain')
150         url.should be_nil
151         last_response.should_not be_redirect
152       end
153       after do
154         @empty_file.close true
155       end
156     end
157
158     it "should prevent upload without a password" do
159       url = upload :upload_password => ''
160       url.should be_nil
161       last_response.status.should eql(403)
162     end
163
164     it "should prevent upload with a wrong password" do
165       url = upload :upload_password => 'bad'
166       url.should be_nil
167       last_response.status.should eql(403)
168     end
169
170     context "when using AJAX to verify upload password" do
171       context "when sending the right password" do
172         before do
173           request "/authenticate", :method => "POST", :xhr => true,
174                                    :params => { :upload_password => UPLOAD_PASSWORD }
175         end
176         subject { last_response }
177         it { should be_ok }
178       end
179       context "when sending no password" do
180         before do
181           request "/authenticate", :method => "POST", :xhr => true,
182                                    :params => { :upload_password => '' }
183         end
184         subject { last_response.status }
185         it { should == 403 }
186       end
187       context "when sending a JSON dump of the wrong password" do
188         before do
189           request "/authenticate", :method => "POST", :xhr => true,
190                                    :params => { :upload_password => 'wrong'}
191         end
192         subject { last_response.status }
193         it { should == 403 }
194       end
195     end
196
197     context "when a 'one time download' has been retrieved" do
198       before(:each) do
199         @url = upload :one_time => true
200         get @url
201       end
202
203       it "should be the same as the uploaded file" do
204         last_response.should be_ok
205         last_response['Content-Type'].should eql('text/x-script.ruby')
206         last_response.body.should eql(File.new(__FILE__).read)
207       end
208
209       it "should not be downloadable any more" do
210         get @url
211         last_response.status.should eql(410)
212       end
213
214       it "should have zero'ed the file on the server" do
215         files = Dir.glob("#{Coquelicot.depot.path}/*")
216         files.should have(1).items
217         File.lstat(files[0]).size.should eql(0)
218       end
219     end
220
221     context "after a password protected upload" do
222       before(:each) do
223         @url = upload :file_key => 'somethingSecret'
224       end
225
226       it "should not return an URL with the encryption key" do
227         url_name = @url.split('/')[-1]
228         url_name.split('-').should have(1).items
229       end
230
231       it "should offer a password form before download" do
232         get @url
233         last_response.should be_ok
234         last_response['Content-Type'].should eql('text/html;charset=utf-8')
235         doc = Hpricot(last_response.body)
236         (doc/'input#file_key').should have(1).items
237       end
238
239       context "when given the correct password" do
240         it "should download the same file" do
241           post @url, :file_key => 'somethingSecret'
242           last_response.should be_ok
243           last_response['Content-Type'].should eql('text/x-script.ruby')
244           last_response.body.should eql(File.new(__FILE__).read)
245         end
246       end
247
248       it "should prevent download without a password" do
249         post @url
250         last_response.status.should eql(403)
251       end
252
253       it "should prevent download with a wrong password" do
254         post @url, :file_key => 'BAD'
255         last_response.status.should eql(403)
256       end
257     end
258
259     context "after an upload with a time limit" do
260       before(:each) do
261         @url = upload :expire => 60 # 1 hour
262       end
263
264       it "should prevent download after the time limit has expired" do
265         # let's be the day after tomorrow
266         Timecop.travel(Date.today + 2) do
267           get @url
268           last_response.status.should eql(410)
269         end
270       end
271     end
272
273     it "should refuse an expiration time longer than the maximum" do
274       upload :expire => 60 * 24 * 31 * 12 # 1 year
275       last_response.status.should eql(403)
276     end
277
278     it "should cleanup expired files" do
279       url = upload :expire => 60, :file_key => 'test' # 1 hour
280       url_name = url.split('/')[-1]
281       Dir.glob("#{Coquelicot.depot.path}/*").should have(1).items
282       # let's be the day after tomorrow
283       Timecop.travel(Date.today + 2) do
284         Coquelicot.depot.gc!
285         files = Dir.glob("#{Coquelicot.depot.path}/*")
286         files.should have(1).items
287         File.lstat(files[0]).size.should eql(0)
288         Coquelicot.depot.get_file(url_name).expired?.should be_true
289       end
290       # let's be after 'gone' period
291       Timecop.travel(Time.now + (Coquelicot.settings.gone_period * 60)) do
292         Coquelicot.depot.gc!
293         Dir.glob("#{Coquelicot.depot.path}/*").should have(0).items
294         Coquelicot.depot.get_file(url_name).should be_nil
295       end
296     end
297   end
298
299   context "when using 'imap' authentication mechanism" do
300     before(:each) do
301       app.set :authentication_method, :name => 'imap',
302                                       :imap_server => 'example.org',
303                                       :imap_port => 993
304     end
305
306     it "should try to login to the IMAP server when using AJAX" do
307       imap = stub('Net::Imap').as_null_object
308       imap.should_receive(:login).with('user', 'password')
309       Net::IMAP.should_receive(:new).with('example.org', 993, true).and_return(imap)
310
311       request "/authenticate", :method => "POST", :xhr => true,
312                                :params => { :imap_user     => 'user',
313                                             :imap_password => 'password' }
314       last_response.should be_ok
315     end
316   end
317
318   context "when I set an URL in the 'url' setting" do
319     before(:each) do
320       app.set :url, 'http://example.com/url-test/'
321     end
322     it 'should be appear in <base/> href' do
323       request "/"
324       last_response.should be_ok
325       doc = Hpricot(last_response.body)
326       (doc/"base").attr('href').should == 'http://example.com/url-test/'
327     end
328   end
329 end