update TOOD entry in README about gemification
[coquelicot.git] / spec / coquelicot_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
20 require 'timecop'
21 require 'hpricot'
22 require 'tmpdir'
23 require 'active_support'
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_password
38     'secret'
39   end
40
41   def upload(opts={})
42     # We need the request to be in the right order
43     params = ActiveSupport::OrderedHash.new
44     params[:upload_password] = upload_password
45     params[:expire] = 5
46     params[:one_time] = ''
47     params[:file_key] = ''
48     params[:file] = Rack::Test::UploadedFile.new(__FILE__, 'text/x-script.ruby')
49     params.merge!(opts)
50     data = build_multipart(params)
51     post '/upload', {}, { :input           => data,
52                           'CONTENT_LENGTH' => data.length.to_s,
53                           'CONTENT_TYPE'   => "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}"
54                         }
55     return nil unless last_response.redirect?
56     follow_redirect!
57     last_response.should be_ok
58     doc = Hpricot(last_response.body)
59     return (doc/'.ready')[0].inner_text
60   end
61
62   def build_multipart(params)
63     params.map do |name, value|
64       if value.is_a? Rack::Test::UploadedFile
65         <<-PART
66 --#{Rack::Multipart::MULTIPART_BOUNDARY}\r
67 Content-Disposition: form-data; name="#{name}"; filename="#{Rack::Utils.escape(value.original_filename)}"\r
68 Content-Type: #{value.content_type}\r
69 Content-Length: #{::File.stat(value.path).size}\r
70 \r
71 #{slurp(value.path)}\r
72 PART
73       else
74         <<-PART
75 --#{Rack::Multipart::MULTIPART_BOUNDARY}\r
76 Content-Disposition: form-data; name="#{name}"\r
77 \r
78 #{value}\r
79 PART
80       end
81     end.join + "--#{Rack::Multipart::MULTIPART_BOUNDARY}--\r"
82   end
83
84   it "should offer an upload form" do
85     get '/'
86     last_response.should be_ok
87     doc = Hpricot(last_response.body)
88     (doc/"form#upload").should have(1).items
89   end
90
91   context "when I explicitely ask for french" do
92     it "should offer an upload form in french" do
93       get '/', :lang => 'fr'
94       last_response.should be_ok
95       doc = Hpricot(last_response.body)
96       (doc/"#submit").attr('value').should == 'Partager¬†!'
97     end
98   end
99
100   context "when using 'simpleauth' authentication mechanism" do
101     before(:each) do
102       app.set :authentication_method, :name => :simplepass,
103                                       :upload_password => Digest::SHA1.hexdigest(upload_password)
104     end
105
106     context "after a successful upload" do
107       before(:each) do
108         @url = upload
109       end
110
111       it "should not store the file in cleartext" do
112         files = Dir.glob("#{Coquelicot.depot.path}/*")
113         files.should have(2).items
114         File.new(files[0]).read().should_not include('should not store an uploaded file')
115       end
116
117       it "should generate a random URL to download the file" do
118         @url.should_not include(File.basename(__FILE__))
119       end
120
121       it "should store the file with a different name than the one in URL" do
122         url_name = @url.split('/')[-1]
123         files = Dir.glob("#{Coquelicot.depot.path}/*")
124         files.should have(2).items
125         url_name.should_not eql(File.basename(files[0]))
126       end
127
128       it "should encode the encryption key in URL as no password has been specified" do
129         url_name = @url.split('/')[-1]
130         url_name.split('-').should have(2).items
131       end
132
133       it "should say 'not found' is password in URL is wrong" do
134         get "#{@url}wrong"
135         last_response.status.should == 404
136       end
137
138       it "should download when using extra Base32 characters in URL" do
139         splitted = @url.split('/')
140         name = splitted[-1].upcase.gsub(/O/, '0').gsub(/L/, '1')
141         get "#{splitted[0..-2].join '/'}/#{name}"
142         last_response.should be_ok
143         last_response['Content-Type'].should eql('text/x-script.ruby')
144       end
145
146       context "when the file has been downloaded" do
147         before(:each) do
148           get @url
149         end
150
151         it "should be the same file as the uploaded" do
152           last_response.should be_ok
153           last_response['Content-Type'].should eql('text/x-script.ruby')
154           last_response.body.should == slurp(__FILE__)
155         end
156
157         it "should have sent the right Content-Length" do
158           last_response.should be_ok
159           last_response['Content-Length'].to_i.should == File.stat(__FILE__).size
160         end
161
162         it "should always has the same Last-Modified header" do
163           last_modified = last_response['Last-Modified']
164           last_modified.should_not be_nil
165           get @url
166           last_response['Last-Modified'].should eql(last_modified)
167         end
168       end
169     end
170
171     context "given an empty file" do
172       before do
173         @empty_file = Tempfile.new('empty')
174       end
175       it "should not be accepted when uploaded" do
176         url = upload :file => Rack::Test::UploadedFile.new(@empty_file.path, 'text/plain')
177         url.should be_nil
178         last_response.should_not be_redirect
179       end
180       after do
181         @empty_file.close true
182       end
183     end
184
185     it "should prevent upload without a password" do
186       url = upload :upload_password => ''
187       url.should be_nil
188       last_response.status.should eql(403)
189     end
190
191     it "should prevent upload with a wrong password" do
192       url = upload :upload_password => 'bad'
193       url.should be_nil
194       last_response.status.should eql(403)
195     end
196
197     context "when using AJAX to verify upload password" do
198       context "when sending the right password" do
199         before do
200           request "/authenticate", :method => "POST", :xhr => true,
201                                    :params => { :upload_password => upload_password }
202         end
203         subject { last_response }
204         it { should be_ok }
205       end
206       context "when sending no password" do
207         before do
208           request "/authenticate", :method => "POST", :xhr => true,
209                                    :params => { :upload_password => '' }
210         end
211         subject { last_response.status }
212         it { should == 403 }
213       end
214       context "when sending a JSON dump of the wrong password" do
215         before do
216           request "/authenticate", :method => "POST", :xhr => true,
217                                    :params => { :upload_password => 'wrong'}
218         end
219         subject { last_response.status }
220         it { should == 403 }
221       end
222     end
223
224     context "when a 'one time download' has been retrieved" do
225       before(:each) do
226         @url = upload :one_time => true
227         get @url
228       end
229
230       it "should be the same as the uploaded file" do
231         last_response.should be_ok
232         last_response['Content-Type'].should eql('text/x-script.ruby')
233         last_response.body.should == slurp(__FILE__)
234       end
235
236       it "should not be downloadable any more" do
237         get @url
238         last_response.status.should eql(410)
239       end
240
241       it "should have zero'ed the file on the server" do
242         files = Dir.glob("#{Coquelicot.depot.path}/*")
243         files.should have(2).items
244         File.lstat(files[0]).size.should eql(0)
245       end
246     end
247
248     context "after a password protected upload" do
249       before(:each) do
250         @url = upload :file_key => 'somethingSecret'
251       end
252
253       it "should not return an URL with the encryption key" do
254         url_name = @url.split('/')[-1]
255         url_name.split('-').should have(1).items
256       end
257
258       it "should offer a password form before download" do
259         get @url
260         last_response.should be_ok
261         last_response['Content-Type'].should eql('text/html;charset=utf-8')
262         doc = Hpricot(last_response.body)
263         (doc/'input#file_key').should have(1).items
264       end
265
266       context "when given the correct password" do
267         it "should download the same file" do
268           post @url, :file_key => 'somethingSecret'
269           last_response.should be_ok
270           last_response['Content-Type'].should eql('text/x-script.ruby')
271           last_response.body.should == slurp(__FILE__)
272         end
273       end
274
275       it "should prevent download without a password" do
276         post @url
277         last_response.status.should eql(403)
278       end
279
280       it "should prevent download with a wrong password" do
281         post @url, :file_key => 'BAD'
282         last_response.status.should eql(403)
283       end
284     end
285
286     context "after an upload with a time limit" do
287       before(:each) do
288         @url = upload :expire => 60 # 1 hour
289       end
290
291       it "should prevent download after the time limit has expired" do
292         # let's be the day after tomorrow
293         Timecop.travel(Date.today + 2) do
294           get @url
295           last_response.status.should eql(410)
296         end
297       end
298     end
299
300     it "should refuse an expiration time longer than the maximum" do
301       upload :expire => 60 * 24 * 31 * 12 # 1 year
302       last_response.status.should eql(403)
303     end
304
305     it "should cleanup expired files" do
306       url = upload :expire => 60, :file_key => 'test' # 1 hour
307       url_name = url.split('/')[-1]
308       Dir.glob("#{Coquelicot.depot.path}/*").should have(2).items
309       # let's be the day after tomorrow
310       Timecop.travel(Date.today + 2) do
311         Coquelicot.depot.gc!
312         files = Dir.glob("#{Coquelicot.depot.path}/*")
313         files.should have(2).items
314         File.lstat(files[0]).size.should eql(0)
315         Coquelicot.depot.get_file(url_name).expired?.should be_true
316       end
317       # let's be after 'gone' period
318       Timecop.travel(Time.now + (Coquelicot.settings.gone_period * 60)) do
319         Coquelicot.depot.gc!
320         Dir.glob("#{Coquelicot.depot.path}/*").should have(0).items
321         Coquelicot.depot.get_file(url_name).should be_nil
322       end
323     end
324   end
325
326   context "when using 'imap' authentication mechanism" do
327     before(:each) do
328       app.set :authentication_method, :name => 'imap',
329                                       :imap_server => 'example.org',
330                                       :imap_port => 993
331     end
332
333     it "should try to login to the IMAP server when using AJAX" do
334       imap = stub('Net::Imap').as_null_object
335       imap.should_receive(:login).with('user', 'password')
336       Net::IMAP.should_receive(:new).with('example.org', 993, true).and_return(imap)
337
338       request "/authenticate", :method => "POST", :xhr => true,
339                                :params => { :imap_user     => 'user',
340                                             :imap_password => 'password' }
341       last_response.should be_ok
342     end
343   end
344 end