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