implement file format 2.0
[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 require 'spec_helper'
18
19 require 'timecop'
20 require 'hpricot'
21 require 'tmpdir'
22
23 UPLOAD_PASSWORD = 'secret'
24
25 # The specs in this file are written like what should have been Cucumber
26 # features and without much knowledge of best practices with RSpec. Most of
27 # them should be improved, rewritten and moved to `spec/coquelicot/app_spec.rb`.
28 #
29 # Once down, we could remove the dependency on Hpricot for the much better
30 # Capybara (which is used in `spec/coquelicot/app_spec.rb`).
31
32 describe 'Coquelicot' do
33   include Rack::Test::Methods
34
35   include_context 'with Coquelicot::Application'
36
37   def upload(opts={})
38     opts = { :file => Rack::Test::UploadedFile.new(__FILE__, 'text/x-script.ruby'),
39              :upload_password => UPLOAD_PASSWORD
40            }.merge(opts)
41     post '/upload', opts
42     return nil unless last_response.redirect?
43     follow_redirect!
44     last_response.should be_ok
45     doc = Hpricot(last_response.body)
46     return (doc/'a').collect { |a| a.attributes['href'] }.
47              select { |h| h.start_with? "http://#{last_request.host}/" }[0]
48   end
49
50   it "should offer an upload form" do
51     get '/'
52     last_response.should be_ok
53     doc = Hpricot(last_response.body)
54     (doc/"form#upload").should have(1).items
55   end
56
57   context "when I explicitely ask for french" do
58     it "should offer an upload form in french" do
59       get '/', :lang => 'fr'
60       last_response.should be_ok
61       doc = Hpricot(last_response.body)
62       (doc/"input.submit").attr('value').should == 'Partager¬†!'
63     end
64   end
65
66   context "when using 'simpleauth' authentication mechanism" do
67     before(:each) do
68       app.set :authentication_method, :name => :simplepass,
69                                       :upload_password => Digest::SHA1.hexdigest(UPLOAD_PASSWORD)
70     end
71
72     context "after a successful upload" do
73       before(:each) do
74         @url = upload
75       end
76
77       it "should not store the file in cleartext" do
78         files = Dir.glob("#{Coquelicot.depot.path}/*")
79         files.should have(2).items
80         File.new(files[0]).read().should_not include('should not store an uploaded file')
81       end
82
83       it "should generate a random URL to download the file" do
84         @url.should_not include(File.basename(__FILE__))
85       end
86
87       it "should store the file with a different name than the one in URL" do
88         url_name = @url.split('/')[-1]
89         files = Dir.glob("#{Coquelicot.depot.path}/*")
90         files.should have(2).items
91         url_name.should_not eql(File.basename(files[0]))
92       end
93
94       it "should encode the encryption key in URL as no password has been specified" do
95         url_name = @url.split('/')[-1]
96         url_name.split('-').should have(2).items
97       end
98
99       it "should say 'not found' is password in URL is wrong" do
100         get "#{@url}wrong"
101         last_response.status.should == 404
102       end
103
104       it "should download when using extra Base32 characters in URL" do
105         splitted = @url.split('/')
106         name = splitted[-1].upcase.gsub(/O/, '0').gsub(/L/, '1')
107         get "#{splitted[0..-2].join '/'}/#{name}"
108         last_response.should be_ok
109         last_response['Content-Type'].should eql('text/x-script.ruby')
110       end
111
112       context "when the file has been downloaded" do
113         before(:each) do
114           get @url
115         end
116
117         it "should be the same file as the uploaded" do
118           last_response.should be_ok
119           last_response['Content-Type'].should eql('text/x-script.ruby')
120           last_response.body.should eql(File.new(__FILE__).read)
121         end
122
123         it "should always has the same Last-Modified header" do
124           last_modified = last_response['Last-Modified']
125           last_modified.should_not be_nil
126           get @url
127           last_response['Last-Modified'].should eql(last_modified)
128         end
129       end
130     end
131
132     context "given an empty file" do
133       before do
134         @empty_file = Tempfile.new('empty')
135       end
136       it "should not be accepted when uploaded" do
137         url = upload :file => Rack::Test::UploadedFile.new(@empty_file.path, 'text/plain')
138         url.should be_nil
139         last_response.should_not be_redirect
140       end
141       after do
142         @empty_file.close true
143       end
144     end
145
146     it "should prevent upload without a password" do
147       url = upload :upload_password => ''
148       url.should be_nil
149       last_response.status.should eql(403)
150     end
151
152     it "should prevent upload with a wrong password" do
153       url = upload :upload_password => 'bad'
154       url.should be_nil
155       last_response.status.should eql(403)
156     end
157
158     context "when using AJAX to verify upload password" do
159       context "when sending the right password" do
160         before do
161           request "/authenticate", :method => "POST", :xhr => true,
162                                    :params => { :upload_password => UPLOAD_PASSWORD }
163         end
164         subject { last_response }
165         it { should be_ok }
166       end
167       context "when sending no password" do
168         before do
169           request "/authenticate", :method => "POST", :xhr => true,
170                                    :params => { :upload_password => '' }
171         end
172         subject { last_response.status }
173         it { should == 403 }
174       end
175       context "when sending a JSON dump of the wrong password" do
176         before do
177           request "/authenticate", :method => "POST", :xhr => true,
178                                    :params => { :upload_password => 'wrong'}
179         end
180         subject { last_response.status }
181         it { should == 403 }
182       end
183     end
184
185     context "when a 'one time download' has been retrieved" do
186       before(:each) do
187         @url = upload :one_time => true
188         get @url
189       end
190
191       it "should be the same as the uploaded file" do
192         last_response.should be_ok
193         last_response['Content-Type'].should eql('text/x-script.ruby')
194         last_response.body.should eql(File.new(__FILE__).read)
195       end
196
197       it "should not be downloadable any more" do
198         get @url
199         last_response.status.should eql(410)
200       end
201
202       it "should have zero'ed the file on the server" do
203         files = Dir.glob("#{Coquelicot.depot.path}/*")
204         files.should have(2).items
205         File.lstat(files[0]).size.should eql(0)
206       end
207     end
208
209     context "after a password protected upload" do
210       before(:each) do
211         @url = upload :file_key => 'somethingSecret'
212       end
213
214       it "should not return an URL with the encryption key" do
215         url_name = @url.split('/')[-1]
216         url_name.split('-').should have(1).items
217       end
218
219       it "should offer a password form before download" do
220         get @url
221         last_response.should be_ok
222         last_response['Content-Type'].should eql('text/html;charset=utf-8')
223         doc = Hpricot(last_response.body)
224         (doc/'input#file_key').should have(1).items
225       end
226
227       context "when given the correct password" do
228         it "should download the same file" do
229           post @url, :file_key => 'somethingSecret'
230           last_response.should be_ok
231           last_response['Content-Type'].should eql('text/x-script.ruby')
232           last_response.body.should eql(File.new(__FILE__).read)
233         end
234       end
235
236       it "should prevent download without a password" do
237         post @url
238         last_response.status.should eql(403)
239       end
240
241       it "should prevent download with a wrong password" do
242         post @url, :file_key => 'BAD'
243         last_response.status.should eql(403)
244       end
245     end
246
247     context "after an upload with a time limit" do
248       before(:each) do
249         @url = upload :expire => 60 # 1 hour
250       end
251
252       it "should prevent download after the time limit has expired" do
253         # let's be the day after tomorrow
254         Timecop.travel(Date.today + 2) do
255           get @url
256           last_response.status.should eql(410)
257         end
258       end
259     end
260
261     it "should refuse an expiration time longer than the maximum" do
262       upload :expire => 60 * 24 * 31 * 12 # 1 year
263       last_response.status.should eql(403)
264     end
265
266     it "should cleanup expired files" do
267       url = upload :expire => 60, :file_key => 'test' # 1 hour
268       url_name = url.split('/')[-1]
269       Dir.glob("#{Coquelicot.depot.path}/*").should have(2).items
270       # let's be the day after tomorrow
271       Timecop.travel(Date.today + 2) do
272         Coquelicot.depot.gc!
273         files = Dir.glob("#{Coquelicot.depot.path}/*")
274         files.should have(2).items
275         File.lstat(files[0]).size.should eql(0)
276         Coquelicot.depot.get_file(url_name).expired?.should be_true
277       end
278       # let's be after 'gone' period
279       Timecop.travel(Time.now + (Coquelicot.settings.gone_period * 60)) do
280         Coquelicot.depot.gc!
281         Dir.glob("#{Coquelicot.depot.path}/*").should have(0).items
282         Coquelicot.depot.get_file(url_name).should be_nil
283       end
284     end
285   end
286
287   context "when using 'imap' authentication mechanism" do
288     before(:each) do
289       app.set :authentication_method, :name => 'imap',
290                                       :imap_server => 'example.org',
291                                       :imap_port => 993
292     end
293
294     it "should try to login to the IMAP server when using AJAX" do
295       imap = stub('Net::Imap').as_null_object
296       imap.should_receive(:login).with('user', 'password')
297       Net::IMAP.should_receive(:new).with('example.org', 993, true).and_return(imap)
298
299       request "/authenticate", :method => "POST", :xhr => true,
300                                :params => { :imap_user     => 'user',
301                                             :imap_password => 'password' }
302       last_response.should be_ok
303     end
304   end
305 end