spec for different reference StoredFile with a new shiny helper
[coquelicot.git] / spec / coquelicot_spec.rb
1 # Coquelicot: "one-click" file sharing with a focus on users' privacy.
2 # Copyright ¬© 2010-2012 potager.org <jardiniers@potager.org>
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as
6 # published by the Free Software Foundation, either version 3 of the
7 # License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 require 'spec_helper'
18
19 require 'timecop'
20 require 'hpricot'
21 require 'tmpdir'
22
23 UPLOAD_PASSWORD = 'secret'
24
25 describe 'Coquelicot' do
26   include Rack::Test::Methods
27
28   include_context 'with Coquelicot::Application'
29
30   def upload(opts={})
31     opts = { :file => Rack::Test::UploadedFile.new(__FILE__, 'text/x-script.ruby'),
32              :upload_password => UPLOAD_PASSWORD
33            }.merge(opts)
34     post '/upload', opts
35     return nil unless last_response.redirect?
36     follow_redirect!
37     last_response.should be_ok
38     doc = Hpricot(last_response.body)
39     return (doc/'a').collect { |a| a.attributes['href'] }.
40              select { |h| h.start_with? "http://#{last_request.host}/" }[0]
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 "when I explicitely ask for french" do
51     it "should offer an upload form in french" do
52       get '/', :lang => 'fr'
53       last_response.should be_ok
54       doc = Hpricot(last_response.body)
55       (doc/"input.submit").attr('value').should == 'Partager¬†!'
56     end
57   end
58
59   context "when using 'simpleauth' authentication mechanism" do
60     before(:each) do
61       app.set :authentication_method, :name => :simplepass,
62                                       :upload_password => Digest::SHA1.hexdigest(UPLOAD_PASSWORD)
63     end
64
65     context "after a successful upload" do
66       before(:each) do
67         @url = upload
68       end
69
70       it "should not store the file in cleartext" do
71         files = Dir.glob("#{Coquelicot.depot.path}/*")
72         files.should have(1).items
73         File.new(files[0]).read().should_not include('should not store an uploaded file')
74       end
75
76       it "should generate a random URL to download the file" do
77         @url.should_not include(File.basename(__FILE__))
78       end
79
80       it "should store the file with a different name than the one in URL" do
81         url_name = @url.split('/')[-1]
82         files = Dir.glob("#{Coquelicot.depot.path}/*")
83         files.should have(1).items
84         url_name.should_not eql(File.basename(files[0]))
85       end
86
87       it "should encode the encryption key in URL as no password has been specified" do
88         url_name = @url.split('/')[-1]
89         url_name.split('-').should have(2).items
90       end
91
92       it "should say 'not found' is password in URL is wrong" do
93         get "#{@url}wrong"
94         last_response.status.should == 404
95       end
96
97       it "should download when using extra Base32 characters in URL" do
98         splitted = @url.split('/')
99         name = splitted[-1].upcase.gsub(/O/, '0').gsub(/L/, '1')
100         get "#{splitted[0..-2].join '/'}/#{name}"
101         last_response.should be_ok
102         last_response['Content-Type'].should eql('text/x-script.ruby')
103       end
104
105       context "when the file has been downloaded" do
106         before(:each) do
107           get @url
108         end
109
110         it "should be the same file as the uploaded" do
111           last_response.should be_ok
112           last_response['Content-Type'].should eql('text/x-script.ruby')
113           last_response.body.should eql(File.new(__FILE__).read)
114         end
115
116         it "should always has the same Last-Modified header" do
117           last_modified = last_response['Last-Modified']
118           last_modified.should_not be_nil
119           get @url
120           last_response['Last-Modified'].should eql(last_modified)
121         end
122       end
123     end
124
125     context "given an empty file" do
126       before do
127         @empty_file = Tempfile.new('empty')
128       end
129       it "should not be accepted when uploaded" do
130         url = upload :file => Rack::Test::UploadedFile.new(@empty_file.path, 'text/plain')
131         url.should be_nil
132         last_response.should_not be_redirect
133       end
134       after do
135         @empty_file.close true
136       end
137     end
138
139     it "should prevent upload without a password" do
140       url = upload :upload_password => ''
141       url.should be_nil
142       last_response.status.should eql(403)
143     end
144
145     it "should prevent upload with a wrong password" do
146       url = upload :upload_password => 'bad'
147       url.should be_nil
148       last_response.status.should eql(403)
149     end
150
151     context "when using AJAX to verify upload password" do
152       context "when sending the right password" do
153         before do
154           request "/authenticate", :method => "POST", :xhr => true,
155                                    :params => { :upload_password => UPLOAD_PASSWORD }
156         end
157         subject { last_response }
158         it { should be_ok }
159       end
160       context "when sending no password" do
161         before do
162           request "/authenticate", :method => "POST", :xhr => true,
163                                    :params => { :upload_password => '' }
164         end
165         subject { last_response.status }
166         it { should == 403 }
167       end
168       context "when sending a JSON dump of the wrong password" do
169         before do
170           request "/authenticate", :method => "POST", :xhr => true,
171                                    :params => { :upload_password => 'wrong'}
172         end
173         subject { last_response.status }
174         it { should == 403 }
175       end
176     end
177
178     context "when a 'one time download' has been retrieved" do
179       before(:each) do
180         @url = upload :one_time => true
181         get @url
182       end
183
184       it "should be the same as the uploaded file" do
185         last_response.should be_ok
186         last_response['Content-Type'].should eql('text/x-script.ruby')
187         last_response.body.should eql(File.new(__FILE__).read)
188       end
189
190       it "should not be downloadable any more" do
191         get @url
192         last_response.status.should eql(410)
193       end
194
195       it "should have zero'ed the file on the server" do
196         files = Dir.glob("#{Coquelicot.depot.path}/*")
197         files.should have(1).items
198         File.lstat(files[0]).size.should eql(0)
199       end
200     end
201
202     context "after a password protected upload" do
203       before(:each) do
204         @url = upload :file_key => 'somethingSecret'
205       end
206
207       it "should not return an URL with the encryption key" do
208         url_name = @url.split('/')[-1]
209         url_name.split('-').should have(1).items
210       end
211
212       it "should offer a password form before download" do
213         get @url
214         last_response.should be_ok
215         last_response['Content-Type'].should eql('text/html;charset=utf-8')
216         doc = Hpricot(last_response.body)
217         (doc/'input#file_key').should have(1).items
218       end
219
220       context "when given the correct password" do
221         it "should download the same file" do
222           post @url, :file_key => 'somethingSecret'
223           last_response.should be_ok
224           last_response['Content-Type'].should eql('text/x-script.ruby')
225           last_response.body.should eql(File.new(__FILE__).read)
226         end
227       end
228
229       it "should prevent download without a password" do
230         post @url
231         last_response.status.should eql(403)
232       end
233
234       it "should prevent download with a wrong password" do
235         post @url, :file_key => 'BAD'
236         last_response.status.should eql(403)
237       end
238     end
239
240     context "after an upload with a time limit" do
241       before(:each) do
242         @url = upload :expire => 60 # 1 hour
243       end
244
245       it "should prevent download after the time limit has expired" do
246         # let's be the day after tomorrow
247         Timecop.travel(Date.today + 2) do
248           get @url
249           last_response.status.should eql(410)
250         end
251       end
252     end
253
254     it "should refuse an expiration time longer than the maximum" do
255       upload :expire => 60 * 24 * 31 * 12 # 1 year
256       last_response.status.should eql(403)
257     end
258
259     it "should cleanup expired files" do
260       url = upload :expire => 60, :file_key => 'test' # 1 hour
261       url_name = url.split('/')[-1]
262       Dir.glob("#{Coquelicot.depot.path}/*").should have(1).items
263       # let's be the day after tomorrow
264       Timecop.travel(Date.today + 2) do
265         Coquelicot.depot.gc!
266         files = Dir.glob("#{Coquelicot.depot.path}/*")
267         files.should have(1).items
268         File.lstat(files[0]).size.should eql(0)
269         Coquelicot.depot.get_file(url_name).expired?.should be_true
270       end
271       # let's be after 'gone' period
272       Timecop.travel(Time.now + (Coquelicot.settings.gone_period * 60)) do
273         Coquelicot.depot.gc!
274         Dir.glob("#{Coquelicot.depot.path}/*").should have(0).items
275         Coquelicot.depot.get_file(url_name).should be_nil
276       end
277     end
278   end
279
280   context "when using 'imap' authentication mechanism" do
281     before(:each) do
282       app.set :authentication_method, :name => 'imap',
283                                       :imap_server => 'example.org',
284                                       :imap_port => 993
285     end
286
287     it "should try to login to the IMAP server when using AJAX" do
288       imap = stub('Net::Imap').as_null_object
289       imap.should_receive(:login).with('user', 'password')
290       Net::IMAP.should_receive(:new).with('example.org', 993, true).and_return(imap)
291
292       request "/authenticate", :method => "POST", :xhr => true,
293                                :params => { :imap_user     => 'user',
294                                             :imap_password => 'password' }
295       last_response.should be_ok
296     end
297   end
298 end