update sinatra to version 1.3
[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     # set a special test password
39     app.set :upload_password, Digest::SHA1.hexdigest(UPLOAD_PASSWORD)
40
41     app.set :environment, :test
42
43     app.set :depot_path, Dir.mktmpdir('coquelicot')
44   end
45
46   after do
47     FileUtils.remove_entry_secure Coquelicot.depot.path
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 "after a successful upload" do
58     before(:each) do
59       @url = upload
60     end
61
62     it "should not store the file in cleartext" do
63       files = Dir.glob("#{Coquelicot.depot.path}/*")
64       files.should have(1).items
65       File.new(files[0]).read().should_not include('should not store an uploaded file')
66     end
67
68     it "should generate a random URL to download the file" do
69       @url.should_not include(File.basename(__FILE__))
70     end
71
72     it "should store the file with a different name than the one in URL" do
73       url_name = @url.split('/')[-1]
74       files = Dir.glob("#{Coquelicot.depot.path}/*")
75       files.should have(1).items
76       url_name.should_not eql(File.basename(files[0]))
77     end
78
79     it "should encode the encryption key in URL as no password has been specified" do
80       url_name = @url.split('/')[-1]
81       url_name.split('-').should have(2).items
82     end
83
84     it "should download when using extra Base32 characters in URL" do
85       splitted = @url.split('/')
86       name = splitted[-1].upcase.gsub(/O/, '0').gsub(/L/, '1')
87       get "#{splitted[0..-2].join '/'}/#{name}"
88       last_response.should be_ok
89       last_response['Content-Type'].should eql('text/x-script.ruby')
90     end
91
92     context "when the file has been downloaded" do
93       before(:each) do
94         get @url
95       end
96
97       it "should be the same file as the uploaded" do
98         last_response.should be_ok
99         last_response['Content-Type'].should eql('text/x-script.ruby')
100         last_response.body.should eql(File.new(__FILE__).read)
101       end
102
103       it "should always has the same Last-Modified header" do
104         last_modified = last_response['Last-Modified']
105         last_modified.should_not be_nil
106         get @url
107         last_response['Last-Modified'].should eql(last_modified)
108       end
109     end
110   end
111
112   context "given an empty file" do
113     before do
114       @empty_file = Tempfile.new('empty')
115     end
116     it "should not be accepted when uploaded" do
117       url = upload :file => Rack::Test::UploadedFile.new(@empty_file.path, 'text/plain')
118       url.should be_nil
119       last_response.should_not be_redirect
120     end
121     after do
122       @empty_file.close true
123     end
124   end
125
126   it "should prevent upload without a password" do
127     url = upload :upload_token => JSON.dump({'upload_password' => ''})
128     url.should be_nil
129     last_response.status.should eql(403)
130   end
131
132   it "should prevent upload with a wrong password" do
133     url = upload :upload_token => JSON.dump({'upload_password' => 'bad'})
134     url.should be_nil
135     last_response.status.should eql(403)
136   end
137
138   it "should allow AJAX upload password verification" do
139     request "/authenticate", :method => "POST", :xhr => true,
140                              :params => { :upload_token => { 'upload_password' => UPLOAD_PASSWORD } }
141     last_response.should be_ok
142     request "/authenticate", :method => "POST", :xhr => true,
143                              :params => { :upload_token => '{}' }
144     last_response.status.should eql(403)
145     request "/authenticate", :method => "POST", :xhr => true,
146                              :params => { :upload_token => JSON.dump({'upload_password' => 'wrong'}) }
147     last_response.status.should eql(403)
148   end
149
150   context "when a 'one time download' has been retrieved" do
151     before(:each) do
152       @url = upload :one_time => true
153       get @url
154     end
155
156     it "should be the same as the uploaded file" do
157       last_response.should be_ok
158       last_response['Content-Type'].should eql('text/x-script.ruby')
159       last_response.body.should eql(File.new(__FILE__).read)
160     end
161
162     it "should not be downloadable any more" do
163       get @url
164       last_response.status.should eql(410)
165     end
166
167     it "should have zero'ed the file on the server" do
168       files = Dir.glob("#{Coquelicot.depot.path}/*")
169       files.should have(1).items
170       File.lstat(files[0]).size.should eql(0)
171     end
172   end
173
174   context "after a password protected upload" do
175     before(:each) do
176       @url = upload :file_key => 'somethingSecret'
177     end
178
179     it "should not return an URL with the encryption key" do
180       url_name = @url.split('/')[-1]
181       url_name.split('-').should have(1).items
182     end
183
184     it "should offer a password form before download" do
185       get @url
186       last_response.should be_ok
187       last_response['Content-Type'].should eql('text/html;charset=utf-8')
188       doc = Hpricot(last_response.body)
189       (doc/'input#file_key').should have(1).items
190     end
191
192     context "when given the correct password" do
193       it "should download the same file" do
194         post @url, :file_key => 'somethingSecret'
195         last_response.should be_ok
196         last_response['Content-Type'].should eql('text/x-script.ruby')
197         last_response.body.should eql(File.new(__FILE__).read)
198       end
199     end
200
201     it "should prevent download without a password" do
202       post @url
203       last_response.status.should eql(403)
204     end
205
206     it "should prevent download with a wrong password" do
207       post @url, :file_key => 'BAD'
208       last_response.status.should eql(403)
209     end
210   end
211
212   context "after an upload with a time limit" do
213     before(:each) do
214       @url = upload :expire => 60 # 1 hour
215     end
216
217     it "should prevent download after the time limit has expired" do
218       # let's be tomorrow
219       Timecop.travel(Date.today + 1) do
220         get @url
221         last_response.status.should eql(410)
222       end
223     end
224   end
225
226   it "should refuse an expiration time longer than the maximum" do
227     upload :expire => 60 * 24 * 31 * 12 # 1 year
228     last_response.status.should eql(403)
229   end
230
231   it "should cleanup expired files" do
232     url = upload :expire => 60, :file_key => 'test' # 1 hour
233     url_name = url.split('/')[-1]
234     Dir.glob("#{Coquelicot.depot.path}/*").should have(1).items
235     # let's be tomorrow
236     Timecop.travel(Date.today + 1) do
237       Coquelicot.depot.gc!
238       files = Dir.glob("#{Coquelicot.depot.path}/*")
239       files.should have(1).items
240       File.lstat(files[0]).size.should eql(0)
241       Coquelicot.depot.get_file(url_name).expired?.should be_true
242     end
243     # let's be after 'gone' period
244     Timecop.travel(Time.now + (Coquelicot.settings.gone_period * 60)) do
245       Coquelicot.depot.gc!
246       Dir.glob("#{Coquelicot.depot.path}/*").should have(0).items
247       Coquelicot.depot.get_file(url_name).should be_nil
248     end
249   end
250 end