impliment an upper-bound expiration time
[coquelicot.git] / test_coquelicot.rb
1 $:.unshift File.join(File.dirname(__FILE__), '../rack-test/lib')
2 $:.unshift File.join(File.dirname(__FILE__), '../timecop/lib')
3
4 require 'sinatra'
5 require 'coquelicot_app'
6 require 'spec'
7 require 'rack/test'
8 require 'timecop'
9 require 'hpricot'
10 require 'tmpdir'
11
12 UPLOAD_PASSWORD = 'secret'
13
14 set :environment, :test
15 set :upload_password, Digest::SHA1.hexdigest(UPLOAD_PASSWORD)
16
17 describe 'Coquelicot' do
18   include Rack::Test::Methods
19
20   def app
21     Sinatra::Application
22   end
23
24   def upload(opts={})
25     opts = { :file => Rack::Test::UploadedFile.new(__FILE__, 'text/x-script.ruby'),
26              :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     Coquelicot.setup :depot_path => Dir.mktmpdir('coquelicot') #"#{Time.now.to_f}"
39   end
40
41   after do
42     FileUtils.remove_entry_secure Coquelicot.depot.path
43   end
44
45   it "should offer an upload form" do
46     get '/'
47     last_response.should be_ok
48     doc = Hpricot(last_response.body)
49     (doc/"form#upload").should have(1).items
50   end
51
52   it "should allow retrieval of an uploaded file" do
53     url = upload
54     get url
55     last_response.should be_ok
56     last_response['Content-Type'].should eql('text/x-script.ruby')
57     last_response.body.should eql(File.new(__FILE__).read)
58   end
59
60   it "should correctly set Last-Modified header when downloading" do
61     url = upload
62     get url
63     last_modified = last_response['Last-Modified']
64     last_modified.should_not be_nil
65     get url
66     last_response['Last-Modified'].should eql(last_modified)
67   end
68
69   it "should prevent upload without a password" do
70     url = upload :upload_password => ''
71     url.should be_nil
72     last_response.status.should eql(403)
73   end
74
75   it "should prevent upload with a wrong password" do
76     url = upload :upload_password => "bad"
77     url.should be_nil
78     last_response.status.should eql(403)
79   end
80
81   it "should not store an uploaded file in cleartext" do
82     upload
83     files = Dir.glob("#{Coquelicot.depot.path}/*")
84     files.should have(1).items
85     File.new(files[0]).read().should_not include('should not store an uploaded file')
86   end
87
88   it "should generate a random URL to retrieve a file" do
89     url = upload
90     url.should_not include(File.basename(__FILE__))
91   end
92
93   it "should store files with a different name than then one in URL" do
94     url = upload
95     url_name = url.split('/')[-1]
96     files = Dir.glob("#{Coquelicot.depot.path}/*")
97     files.should have(1).items
98     url_name.should_not eql(File.basename(files[0]))
99   end
100
101   it "should encode the encryption key in URL when no password has been specified" do
102     url = upload
103     url_name = url.split('/')[-1]
104     url_name.split('-').should have(2).items
105   end
106
107   it "should not encode the encryption key in URL when a password has been specified" do
108     url = upload :file_key => 'somethingSecret'
109     url_name = url.split('/')[-1]
110     url_name.split('-').should have(1).items
111   end
112
113   it "should only allow one time download to be retrieved once" do
114     url = upload :one_time => true
115     get url
116     last_response.should be_ok
117     last_response['Content-Type'].should eql('text/x-script.ruby')
118     last_response.body.should eql(File.new(__FILE__).read)
119     get url
120     last_response.status.should eql(410)
121   end
122
123   it "should have files zero'ed after 'one time' download" do
124     url = upload :one_time => true
125     get url
126     files = Dir.glob("#{Coquelicot.depot.path}/*")
127     files.should have(1).items
128     File.lstat(files[0]).size.should eql(0)
129   end
130
131   it "should allow retrieval of a password protected file" do
132     url = upload :file_key => 'somethingSecret'
133     get url
134     last_response.should be_ok
135     doc = Hpricot(last_response.body)
136     (doc/'input#file_key').should have(1).items
137     url = (doc/'form')[0].attributes['action']
138     post url, :file_key => 'somethingSecret'
139     last_response.should be_ok
140     last_response['Content-Type'].should eql('text/x-script.ruby')
141     last_response.body.should eql(File.new(__FILE__).read)
142   end
143
144   it "should not allow retrieval of a password protected file without the password" do
145     url = upload :file_key => 'somethingSecret'
146     get url
147     last_response.should be_ok
148     last_response['Content-Type'].should_not eql('text/x-script.ruby')
149     post url
150     last_response.status.should eql(403)
151   end
152
153   it "should not allow retrieval of a password protected file with a wrong password" do
154     url = upload :file_key => 'somethingSecret'
155     post url, :file_key => 'BAD'
156     last_response.status.should eql(403)
157   end
158
159   it "should not allow retrieval after the time limit has expired" do
160     url = upload :expire => 60 # 1 hour
161     # let's be tomorrow
162     Timecop.travel(Date.today + 1) do
163       get url
164       last_response.status.should eql(410)
165     end
166   end
167
168   it "should not allow an expiration time longer than the maximum" do
169     upload :expire => 60 * 24 * 31 * 12 # 1 year
170     last_response.status.should eql(403)
171   end
172
173   it "should cleanup expired files" do
174     url = upload :expire => 60, :file_key => 'test' # 1 hour
175     url_name = url.split('/')[-1]
176     Dir.glob("#{Coquelicot.depot.path}/*").should have(1).items
177     # let's be tomorrow
178     Timecop.travel(Date.today + 1) do
179       Coquelicot.depot.gc!
180       files = Dir.glob("#{Coquelicot.depot.path}/*")
181       files.should have(1).items
182       File.lstat(files[0]).size.should eql(0)
183       Coquelicot.depot.get_file(url_name).expired?.should be_true
184     end
185     # let's be after 'gone' period
186     Timecop.travel(Time.now + (Coquelicot.settings.gone_period * 60)) do
187       Coquelicot.depot.gc!
188       Dir.glob("#{Coquelicot.depot.path}/*").should have(0).items
189       Coquelicot.depot.get_file(url_name).should be_nil
190     end
191   end
192
193   it "should map extra base32 characters to filenames" do
194     url = upload :expire => 60 # 1 hour
195     splitted = url.split('/')
196     name = splitted[-1].upcase.gsub(/O/, '0').gsub(/L/, '1')
197     get "#{splitted[0..-2].join '/'}/#{name}"
198     last_response.should be_ok
199     last_response['Content-Type'].should eql('text/x-script.ruby')
200   end
201 end