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