aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/test/controller/session/cookie_store_test.rb
blob: 5adaeaf5c575c51b7de6601f5effd9831d905be8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
require 'abstract_unit'
require 'action_controller/cgi_process'
require 'action_controller/cgi_ext'

require 'stringio'


class CGI::Session::CookieStore
  def ensure_secret_secure_with_test_hax(secret)
    if secret == CookieStoreTest.default_session_options['secret']
      return true
    else
      ensure_secret_secure_without_test_hax(secret)
    end
  end
  alias_method_chain :ensure_secret_secure, :test_hax
end


# Expose for tests.
class CGI
  attr_reader :output_cookies, :output_hidden

  class Session
    attr_reader :dbman

    class CookieStore
      attr_reader :data, :original, :cookie_options
    end
  end
end

class CookieStoreTest < Test::Unit::TestCase
  def self.default_session_options
    { 'database_manager' => CGI::Session::CookieStore,
      'session_key' => '_myapp_session',
      'secret' => 'Keep it secret; keep it safe.',
      'no_cookies' => true,
      'no_hidden' => true }
  end

  def self.cookies
    { :empty => ['BAgw--0686dcaccc01040f4bd4f35fe160afe9bc04c330', {}],
      :a_one => ['BAh7BiIGYWkG--5689059497d7f122a7119f171aef81dcfd807fec', { 'a' => 1 }],
      :typical => ['BAh7ByIMdXNlcl9pZGkBeyIKZmxhc2h7BiILbm90aWNlIgxIZXkgbm93--9d20154623b9eeea05c62ab819be0e2483238759', { 'user_id' => 123, 'flash' => { 'notice' => 'Hey now' }}],
      :flashed => ['BAh7ByIMdXNlcl9pZGkBeyIKZmxhc2h7AA==--bf9785a666d3c4ac09f7fe3353496b437546cfbf', { 'user_id' => 123, 'flash' => {} }],
      :double_escaped => [CGI.escape('BAh7ByIMdXNlcl9pZGkBeyIKZmxhc2h7AA%3D%3D--bf9785a666d3c4ac09f7fe3353496b437546cfbf'), { 'user_id' => 123, 'flash' => {} }] }

  end

  def setup
    ENV.delete('HTTP_COOKIE')
  end

  def test_raises_argument_error_if_missing_session_key
    [nil, ''].each do |blank|
      assert_raise(ArgumentError, blank.inspect) { new_session 'session_key' => blank }
    end
  end

  def test_raises_argument_error_if_missing_secret
    [nil, ''].each do |blank|
      assert_raise(ArgumentError, blank.inspect) { new_session 'secret' => blank }
    end
  end

  def test_raises_argument_error_if_secret_is_probably_insecure
    ["password", "secret", "12345678901234567890123456789"].each do |blank|
      assert_raise(ArgumentError, blank.inspect) { new_session 'secret' => blank }
    end
  end

  def test_reconfigures_session_to_omit_id_cookie_and_hidden_field
    new_session do |session|
      assert_equal true, @options['no_hidden']
      assert_equal true, @options['no_cookies']
    end
  end

  def test_restore_unmarshals_missing_cookie_as_empty_hash
    new_session do |session|
      assert_nil session.dbman.data
      assert_nil session['test']
      assert_equal Hash.new, session.dbman.data
    end
  end

  def test_restore_unmarshals_good_cookies
    cookies(:empty, :a_one, :typical).each do |value, expected|
      set_cookie! value
      new_session do |session|
        assert_nil session['lazy loads the data hash']
        assert_equal expected, session.dbman.data
      end
    end
  end

  def test_restore_deletes_tampered_cookies
    set_cookie! 'a--b'
    new_session do |session|
      assert_raise(CGI::Session::CookieStore::TamperedWithCookie) { session['fail'] }
      assert_cookie_deleted session
    end
  end

  def test_restores_double_encoded_cookies
    set_cookie! cookie_value(:double_escaped)
    new_session do |session|
      session.dbman.restore
      assert_equal session["user_id"], 123
      assert_equal session["flash"], {}
    end
  end

  def test_close_doesnt_write_cookie_if_data_is_blank
    new_session do |session|
      assert_no_cookies session
      session.close
      assert_no_cookies session
    end
  end

  def test_close_doesnt_write_cookie_if_data_is_unchanged
    set_cookie! cookie_value(:typical)
    new_session do |session|
      assert_no_cookies session
      session['user_id'] = session['user_id']
      session.close
      assert_no_cookies session
    end
  end

  def test_close_raises_when_data_overflows
    set_cookie! cookie_value(:empty)
    new_session do |session|
      session['overflow'] = 'bye!' * 1024
      assert_raise(CGI::Session::CookieStore::CookieOverflow) { session.close }
      assert_no_cookies session
    end
  end

  def test_close_marshals_and_writes_cookie
    set_cookie! cookie_value(:typical)
    new_session do |session|
      assert_no_cookies session
      session['flash'] = {}
      assert_no_cookies session
      session.close
      assert_equal 1, session.cgi.output_cookies.size
      cookie = session.cgi.output_cookies.first
      assert_cookie cookie, cookie_value(:flashed)
    end
  end

  def test_delete_writes_expired_empty_cookie_and_sets_data_to_nil
    set_cookie! cookie_value(:typical)
    new_session do |session|
      assert_no_cookies session
      session.delete
      assert_cookie_deleted session

      # @data is set to nil so #close doesn't send another cookie.
      session.close
      assert_cookie_deleted session
    end
  end

  def test_new_session_doesnt_reuse_deleted_cookie_data
    set_cookie! cookie_value(:typical)

    new_session do |session|
      assert_not_nil session['user_id']
      session.delete

      # Start a new session using the same CGI instance.
      post_delete_session = CGI::Session.new(session.cgi, self.class.default_session_options)
      assert_nil post_delete_session['user_id']
    end
  end

  private
    def assert_no_cookies(session)
      assert_nil session.cgi.output_cookies, session.cgi.output_cookies.inspect
    end

    def assert_cookie_deleted(session, message = 'Expected session deletion cookie to be set')
      assert_equal 1, session.cgi.output_cookies.size
      cookie = session.cgi.output_cookies.first
      assert_cookie cookie, nil, 1.year.ago.to_date, "#{message}: #{cookie.name} => #{cookie.value}"
    end

    def assert_cookie(cookie, value = nil, expires = nil, message = nil)
      assert_equal '_myapp_session', cookie.name, message
      assert_equal [value].compact, cookie.value, message
      assert_equal expires, cookie.expires ? cookie.expires.to_date : cookie.expires, message
    end


    def cookies(*which)
      self.class.cookies.values_at(*which)
    end

    def cookie_value(which)
      self.class.cookies[which].first
    end

    def set_cookie!(value)
      ENV['HTTP_COOKIE'] = "_myapp_session=#{value}"
    end

    def new_session(options = {})
      with_cgi do |cgi|
        assert_nil cgi.output_hidden, "Output hidden params should be empty: #{cgi.output_hidden.inspect}"
        assert_nil cgi.output_cookies, "Output cookies should be empty: #{cgi.output_cookies.inspect}"

        @options = self.class.default_session_options.merge(options)
        session = CGI::Session.new(cgi, @options)

        assert_nil cgi.output_hidden, "Output hidden params should be empty: #{cgi.output_hidden.inspect}"
        assert_nil cgi.output_cookies, "Output cookies should be empty: #{cgi.output_cookies.inspect}"

        yield session if block_given?
        session
      end
    end

    def with_cgi
      ENV['REQUEST_METHOD'] = 'GET'
      ENV['HTTP_HOST'] = 'example.com'
      ENV['QUERY_STRING'] = ''

      cgi = CGI.new('query', StringIO.new(''))
      yield cgi if block_given?
      cgi
    end
end


class CookieStoreWithBlockAsSecretTest < CookieStoreTest
  def self.default_session_options
    CookieStoreTest.default_session_options.merge 'secret' => Proc.new { 'Keep it secret; keep it safe.' }
  end
end


class CookieStoreWithMD5DigestTest < CookieStoreTest
  def self.default_session_options
    CookieStoreTest.default_session_options.merge 'digest' => 'MD5'
  end

  def self.cookies
    { :empty => ['BAgw--0415cc0be9579b14afc22ee2d341aa21', {}],
      :a_one => ['BAh7BiIGYWkG--5a0ed962089cc6600ff44168a5d59bc8', { 'a' => 1 }],
      :typical => ['BAh7ByIMdXNlcl9pZGkBeyIKZmxhc2h7BiILbm90aWNlIgxIZXkgbm93--f426763f6ef435b3738b493600db8d64', { 'user_id' => 123, 'flash' => { 'notice' => 'Hey now' }}],
      :flashed => ['BAh7ByIMdXNlcl9pZGkBeyIKZmxhc2h7AA==--0af9156650dab044a53a91a4ddec2c51', { 'user_id' => 123, 'flash' => {} }],
      :double_escaped => [CGI.escape('BAh7ByIMdXNlcl9pZGkBeyIKZmxhc2h7AA%3D%3D--0af9156650dab044a53a91a4ddec2c51'), { 'user_id' => 123, 'flash' => {} }] }
  end
end