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