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