66e73c3502c39769a2611a7f3ea2c44e391565db
[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_app'
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     it "should allow AJAX upload password verification" do
146       request "/authenticate", :method => "POST", :xhr => true,
147                                :params => { :upload_token => { 'upload_password' => UPLOAD_PASSWORD } }
148       last_response.should be_ok
149       request "/authenticate", :method => "POST", :xhr => true,
150                                :params => { :upload_token => '{}' }
151       last_response.status.should eql(403)
152       request "/authenticate", :method => "POST", :xhr => true,
153                                :params => { :upload_token => JSON.dump({'upload_password' => 'wrong'}) }
154       last_response.status.should eql(403)
155     end
156
157     context "when a 'one time download' has been retrieved" do
158       before(:each) do
159         @url = upload :one_time => true
160         get @url
161       end
162
163       it "should be the same as the uploaded file" do
164         last_response.should be_ok
165         last_response['Content-Type'].should eql('text/x-script.ruby')
166         last_response.body.should eql(File.new(__FILE__).read)
167       end
168
169       it "should not be downloadable any more" do
170         get @url
171         last_response.status.should eql(410)
172       end
173
174       it "should have zero'ed the file on the server" do
175         files = Dir.glob("#{Coquelicot.depot.path}/*")
176         files.should have(1).items
177         File.lstat(files[0]).size.should eql(0)
178       end
179     end
180
181     context "after a password protected upload" do
182       before(:each) do
183         @url = upload :file_key => 'somethingSecret'
184       end
185
186       it "should not return an URL with the encryption key" do
187         url_name = @url.split('/')[-1]
188         url_name.split('-').should have(1).items
189       end
190
191       it "should offer a password form before download" do
192         get @url
193         last_response.should be_ok
194         last_response['Content-Type'].should eql('text/html;charset=utf-8')
195         doc = Hpricot(last_response.body)
196         (doc/'input#file_key').should have(1).items
197       end
198
199       context "when given the correct password" do
200         it "should download the same file" do
201           post @url, :file_key => 'somethingSecret'
202           last_response.should be_ok
203           last_response['Content-Type'].should eql('text/x-script.ruby')
204           last_response.body.should eql(File.new(__FILE__).read)
205         end
206       end
207
208       it "should prevent download without a password" do
209         post @url
210         last_response.status.should eql(403)
211       end
212
213       it "should prevent download with a wrong password" do
214         post @url, :file_key => 'BAD'
215         last_response.status.should eql(403)
216       end
217     end
218
219     context "after an upload with a time limit" do
220       before(:each) do
221         @url = upload :expire => 60 # 1 hour
222       end
223
224       it "should prevent download after the time limit has expired" do
225         # let's be tomorrow
226         Timecop.travel(Date.today + 1) do
227           get @url
228           last_response.status.should eql(410)
229         end
230       end
231     end
232
233     it "should refuse an expiration time longer than the maximum" do
234       upload :expire => 60 * 24 * 31 * 12 # 1 year
235       last_response.status.should eql(403)
236     end
237
238     it "should cleanup expired files" do
239       url = upload :expire => 60, :file_key => 'test' # 1 hour
240       url_name = url.split('/')[-1]
241       Dir.glob("#{Coquelicot.depot.path}/*").should have(1).items
242       # let's be tomorrow
243       Timecop.travel(Date.today + 1) do
244         Coquelicot.depot.gc!
245         files = Dir.glob("#{Coquelicot.depot.path}/*")
246         files.should have(1).items
247         File.lstat(files[0]).size.should eql(0)
248         Coquelicot.depot.get_file(url_name).expired?.should be_true
249       end
250       # let's be after 'gone' period
251       Timecop.travel(Time.now + (Coquelicot.settings.gone_period * 60)) do
252         Coquelicot.depot.gc!
253         Dir.glob("#{Coquelicot.depot.path}/*").should have(0).items
254         Coquelicot.depot.get_file(url_name).should be_nil
255       end
256     end
257   end
258 end