encoding_test.rb 17.5 KB
Newer Older
1
# encoding: utf-8
2
require 'securerandom'
3
require 'abstract_unit'
4
require 'active_support/core_ext/string/inflections'
J
Jeremy Kemper 已提交
5
require 'active_support/json'
6
require 'active_support/time'
7
require 'time_zone_test_helpers'
8

9
class TestJSONEncoding < ActiveSupport::TestCase
10 11
  include TimeZoneTestHelpers

12 13 14 15
  class Foo
    def initialize(a, b)
      @a, @b = a, b
    end
16 17
  end

18 19
  class Hashlike
    def to_hash
20
      { :foo => "hello", :bar => "world" }
21 22 23
    end
  end

24
  class Custom
25 26 27 28
    def initialize(serialized)
      @serialized = serialized
    end

G
Godfrey Chan 已提交
29
    def as_json(options = nil)
30
      @serialized
31 32 33
    end
  end

34 35 36 37 38 39 40 41 42
  class CustomWithOptions
    attr_accessor :foo, :bar

    def as_json(options={})
      options[:only] = %w(foo bar)
      super(options)
    end
  end

43 44 45 46 47 48
  class OptionsTest
    def as_json(options = :default)
      options
    end
  end

49 50 51 52 53 54 55 56 57 58 59 60 61
  class HashWithAsJson < Hash
    attr_accessor :as_json_called

    def initialize(*)
      super
    end

    def as_json(options={})
      @as_json_called = true
      super
    end
  end

62 63 64 65
  TrueTests     = [[ true,  %(true)  ]]
  FalseTests    = [[ false, %(false) ]]
  NilTests      = [[ nil,   %(null)  ]]
  NumericTests  = [[ 1,     %(1)     ],
66
                   [ 2.5,   %(2.5)   ],
67 68 69
                   [ 0.0/0.0,   %(null) ],
                   [ 1.0/0.0,   %(null) ],
                   [ -1.0/0.0,  %(null) ],
70
                   [ BigDecimal('0.0')/BigDecimal('0.0'),  %(null) ],
71
                   [ BigDecimal('2.5'), %("#{BigDecimal('2.5').to_s}") ]]
72

73
  StringTests   = [[ 'this is the <string>',     %("this is the \\u003cstring\\u003e")],
74
                   [ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ],
75
                   [ 'http://test.host/posts/1', %("http://test.host/posts/1")],
76
                   [ "Control characters: \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\u2028\u2029",
77
                     %("Control characters: \\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d\\u001e\\u001f\\u2028\\u2029") ]]
78

79 80
  ArrayTests    = [[ ['a', 'b', 'c'],          %([\"a\",\"b\",\"c\"])          ],
                   [ [1, 'a', :b, nil, false], %([1,\"a\",\"b\",null,false]) ]]
81

82 83 84 85
  RangeTests    = [[ 1..2,     %("1..2")],
                   [ 1...2,    %("1...2")],
                   [ 1.5..2.5, %("1.5..2.5")]]

86 87 88 89
  SymbolTests   = [[ :a,     %("a")    ],
                   [ :this,  %("this") ],
                   [ :"a b", %("a b")  ]]

90
  ObjectTests   = [[ Foo.new(1, 2), %({\"a\":1,\"b\":2}) ]]
91
  HashlikeTests = [[ Hashlike.new, %({\"bar\":\"world\",\"foo\":\"hello\"}) ]]
92 93 94 95 96 97 98
  CustomTests   = [[ Custom.new("custom"), '"custom"' ],
                   [ Custom.new(nil), 'null' ],
                   [ Custom.new(:a), '"a"' ],
                   [ Custom.new([ :foo, "bar" ]), '["foo","bar"]' ],
                   [ Custom.new({ :foo => "hello", :bar => "world" }), '{"bar":"world","foo":"hello"}' ],
                   [ Custom.new(Hashlike.new), '{"bar":"world","foo":"hello"}' ],
                   [ Custom.new(Custom.new(Custom.new(:a))), '"a"' ]]
99

100
  RegexpTests   = [[ /^a/, '"(?-mix:^a)"' ], [/^\w{1,2}[a-z]+/ix, '"(?ix-m:^\\\\w{1,2}[a-z]+)"']]
101

102 103 104
  DateTests     = [[ Date.new(2005,2,1), %("2005/02/01") ]]
  TimeTests     = [[ Time.utc(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]]
  DateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]]
105

106
  StandardDateTests     = [[ Date.new(2005,2,1), %("2005-02-01") ]]
107 108
  StandardTimeTests     = [[ Time.utc(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000Z") ]]
  StandardDateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000+00:00") ]]
109
  StandardStringTests   = [[ 'this is the <string>', %("this is the <string>")]]
110

Y
Yehuda Katz 已提交
111 112 113 114 115
  def sorted_json(json)
    return json unless json =~ /^\{.*\}$/
    '{' + json[1..-2].split(',').sort.join(',') + '}'
  end

116
  constants.grep(/Tests$/).each do |class_tests|
117 118
    define_method("test_#{class_tests[0..-6].underscore}") do
      begin
119 120
        prev = ActiveSupport.use_standard_json_time_format

121
        ActiveSupport.escape_html_entities_in_json  = class_tests !~ /^Standard/
122 123
        ActiveSupport.use_standard_json_time_format = class_tests =~ /^Standard/
        self.class.const_get(class_tests).each do |pair|
Y
Yehuda Katz 已提交
124
          assert_equal pair.last, sorted_json(ActiveSupport::JSON.encode(pair.first))
125 126
        end
      ensure
127
        ActiveSupport.escape_html_entities_in_json  = false
128
        ActiveSupport.use_standard_json_time_format = prev
129 130 131
      end
    end
  end
132

133 134 135 136 137 138 139
  def test_process_status
    # There doesn't seem to be a good way to get a handle on a Process::Status object without actually
    # creating a child process, hence this to populate $?
    system("not_a_real_program_#{SecureRandom.hex}")
    assert_equal %({"exitstatus":#{$?.exitstatus},"pid":#{$?.pid}}), ActiveSupport::JSON.encode($?)
  end

T
Thomas Fuchs 已提交
140
  def test_hash_encoding
141 142 143 144
    assert_equal %({\"a\":\"b\"}), ActiveSupport::JSON.encode(:a => :b)
    assert_equal %({\"a\":1}), ActiveSupport::JSON.encode('a' => 1)
    assert_equal %({\"a\":[1,2]}), ActiveSupport::JSON.encode('a' => [1,2])
    assert_equal %({"1":2}), ActiveSupport::JSON.encode(1 => 2)
145

Y
Yehuda Katz 已提交
146
    assert_equal %({\"a\":\"b\",\"c\":\"d\"}), sorted_json(ActiveSupport::JSON.encode(:a => :b, :c => :d))
T
Thomas Fuchs 已提交
147
  end
148

149 150
  def test_utf8_string_encoded_properly
    result = ActiveSupport::JSON.encode('€2.99')
151
    assert_equal '"€2.99"', result
152
    assert_equal(Encoding::UTF_8, result.encoding)
153 154

    result = ActiveSupport::JSON.encode('✎☺')
155
    assert_equal '"✎☺"', result
156
    assert_equal(Encoding::UTF_8, result.encoding)
157
  end
158

159 160 161
  def test_non_utf8_string_transcodes
    s = '二'.encode('Shift_JIS')
    result = ActiveSupport::JSON.encode(s)
162
    assert_equal '"二"', result
163
    assert_equal Encoding::UTF_8, result.encoding
164 165
  end

166 167 168 169 170 171 172 173 174 175 176 177 178
  def test_wide_utf8_chars
    w = '𠜎'
    result = ActiveSupport::JSON.encode(w)
    assert_equal '"𠜎"', result
  end

  def test_wide_utf8_roundtrip
    hash = { string: "𐒑" }
    json = ActiveSupport::JSON.encode(hash)
    decoded_hash = ActiveSupport::JSON.decode(json)
    assert_equal "𐒑", decoded_hash['string']
  end

179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
  def test_reading_encode_big_decimal_as_string_option
    assert_deprecated do
      assert ActiveSupport.encode_big_decimal_as_string
    end
  end

  def test_setting_deprecated_encode_big_decimal_as_string_option
    assert_raise(NotImplementedError) do
      ActiveSupport.encode_big_decimal_as_string = true
    end

    assert_raise(NotImplementedError) do
      ActiveSupport.encode_big_decimal_as_string = false
    end
  end

195
  def test_exception_raised_when_encoding_circular_reference_in_array
196 197
    a = [1]
    a << a
198 199 200
    assert_deprecated do
      assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
    end
201
  end
202

203 204 205
  def test_exception_raised_when_encoding_circular_reference_in_hash
    a = { :name => 'foo' }
    a[:next] = a
206 207 208
    assert_deprecated do
      assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
    end
209 210 211 212 213
  end

  def test_exception_raised_when_encoding_circular_reference_in_hash_inside_array
    a = { :name => 'foo', :sub => [] }
    a[:sub] << a
214 215 216
    assert_deprecated do
      assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
    end
217 218
  end

219
  def test_hash_key_identifiers_are_always_quoted
220
    values = {0 => 0, 1 => 1, :_ => :_, "$" => "$", "a" => "a", :A => :A, :A0 => :A0, "A0B" => "A0B"}
221
    assert_equal %w( "$" "A" "A0" "A0B" "_" "a" "0" "1" ).sort, object_keys(ActiveSupport::JSON.encode(values))
222
  end
223

224
  def test_hash_should_allow_key_filtering_with_only
225
    assert_equal %({"a":1}), ActiveSupport::JSON.encode({'a' => 1, :b => 2, :c => 3}, :only => 'a')
226 227 228
  end

  def test_hash_should_allow_key_filtering_with_except
229
    assert_equal %({"b":2}), ActiveSupport::JSON.encode({'foo' => 'bar', :b => 2, :c => 3}, :except => ['foo', :c])
230
  end
231

232
  def test_time_to_json_includes_local_offset
233 234 235 236
    with_standard_json_time_format(true) do
      with_env_tz 'US/Eastern' do
        assert_equal %("2005-02-01T15:15:10.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10))
      end
237 238
    end
  end
239

240
  def test_hash_with_time_to_json
241 242 243
    with_standard_json_time_format(false) do
      assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { :time => Time.utc(2009) }.to_json
    end
244 245
  end

D
Dan Barry 已提交
246 247 248 249
  def test_nested_hash_with_float
    assert_nothing_raised do
      hash = {
        "CHI" => {
250
          :display_name => "chicago",
D
Dan Barry 已提交
251 252 253
          :latitude => 123.234
        }
      }
S
Santiago Pastorino 已提交
254
      ActiveSupport::JSON.encode(hash)
D
Dan Barry 已提交
255 256 257
    end
  end

258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
  def test_hash_like_with_options
    h = Hashlike.new
    json = h.to_json :only => [:foo]

    assert_equal({"foo"=>"hello"}, JSON.parse(json))
  end

  def test_object_to_json_with_options
    obj = Object.new
    obj.instance_variable_set :@foo, "hello"
    obj.instance_variable_set :@bar, "world"
    json = obj.to_json :only => ["foo"]

    assert_equal({"foo"=>"hello"}, JSON.parse(json))
  end

  def test_struct_to_json_with_options
    struct = Struct.new(:foo, :bar).new
    struct.foo = "hello"
    struct.bar = "world"
    json = struct.to_json :only => [:foo]

    assert_equal({"foo"=>"hello"}, JSON.parse(json))
  end

283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
  def test_hash_should_pass_encoding_options_to_children_in_as_json
    person = {
      :name => 'John',
      :address => {
        :city => 'London',
        :country => 'UK'
      }
    }
    json = person.as_json :only => [:address, :city]

    assert_equal({ 'address' => { 'city' => 'London' }}, json)
  end

  def test_hash_should_pass_encoding_options_to_children_in_to_json
    person = {
      :name => 'John',
      :address => {
        :city => 'London',
        :country => 'UK'
      }
    }
    json = person.to_json :only => [:address, :city]

    assert_equal(%({"address":{"city":"London"}}), json)
  end

  def test_array_should_pass_encoding_options_to_children_in_as_json
    people = [
      { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
      { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
    ]
    json = people.as_json :only => [:address, :city]
    expected = [
      { 'address' => { 'city' => 'London' }},
      { 'address' => { 'city' => 'Paris' }}
    ]

    assert_equal(expected, json)
  end

  def test_array_should_pass_encoding_options_to_children_in_to_json
    people = [
      { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
      { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
    ]
    json = people.to_json :only => [:address, :city]

    assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
  end

333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
  People = Class.new(BasicObject) do
    include Enumerable
    def initialize()
      @people = [
        { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
        { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
      ]
    end
    def each(*, &blk)
      @people.each do |p|
        yield p if blk
        p
      end.each
    end
  end

  def test_enumerable_should_generate_json_with_as_json
    json = People.new.as_json :only => [:address, :city]
    expected = [
      { 'address' => { 'city' => 'London' }},
      { 'address' => { 'city' => 'Paris' }}
354
    ]
355 356 357 358 359 360 361 362 363 364 365

    assert_equal(expected, json)
  end

  def test_enumerable_should_generate_json_with_to_json
    json = People.new.to_json :only => [:address, :city]
    assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
  end

  def test_enumerable_should_pass_encoding_options_to_children_in_as_json
    json = People.new.each.as_json :only => [:address, :city]
366 367 368 369 370 371 372 373 374
    expected = [
      { 'address' => { 'city' => 'London' }},
      { 'address' => { 'city' => 'Paris' }}
    ]

    assert_equal(expected, json)
  end

  def test_enumerable_should_pass_encoding_options_to_children_in_to_json
375
    json = People.new.each.to_json :only => [:address, :city]
376 377 378 379

    assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
  end

G
Godfrey Chan 已提交
380
  def test_hash_to_json_should_not_keep_options_around
381 382 383 384 385
    f = CustomWithOptions.new
    f.foo = "hello"
    f.bar = "world"

    hash = {"foo" => f, "other_hash" => {"foo" => "other_foo", "test" => "other_test"}}
386
    assert_equal({"foo"=>{"foo"=>"hello","bar"=>"world"},
387
                  "other_hash" => {"foo"=>"other_foo","test"=>"other_test"}}, ActiveSupport::JSON.decode(hash.to_json))
388 389
  end

G
Godfrey Chan 已提交
390 391 392 393 394 395 396 397 398 399
  def test_array_to_json_should_not_keep_options_around
    f = CustomWithOptions.new
    f.foo = "hello"
    f.bar = "world"

    array = [f, {"foo" => "other_foo", "test" => "other_test"}]
    assert_equal([{"foo"=>"hello","bar"=>"world"},
                  {"foo"=>"other_foo","test"=>"other_test"}], ActiveSupport::JSON.decode(array.to_json))
  end

400 401 402 403 404 405 406 407 408 409
  def test_hash_as_json_without_options
    json = { foo: OptionsTest.new }.as_json
    assert_equal({"foo" => :default}, json)
  end

  def test_array_as_json_without_options
    json = [ OptionsTest.new ].as_json
    assert_equal([:default], json)
  end

A
Alexey Nayden 已提交
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
  def test_struct_encoding
    Struct.new('UserNameAndEmail', :name, :email)
    Struct.new('UserNameAndDate', :name, :date)
    Struct.new('Custom', :name, :sub)
    user_email = Struct::UserNameAndEmail.new 'David', 'sample@example.com'
    user_birthday = Struct::UserNameAndDate.new 'David', Date.new(2010, 01, 01)
    custom = Struct::Custom.new 'David', user_birthday


    json_strings = ""
    json_string_and_date = ""
    json_custom = ""

    assert_nothing_raised do
      json_strings = user_email.to_json
      json_string_and_date = user_birthday.to_json
      json_custom = custom.to_json
    end

429 430 431
    assert_equal({"name" => "David",
                  "sub" => {
                    "name" => "David",
432
                    "date" => "2010-01-01" }}, ActiveSupport::JSON.decode(json_custom))
433 434

    assert_equal({"name" => "David", "email" => "sample@example.com"},
435
                 ActiveSupport::JSON.decode(json_strings))
436

437
    assert_equal({"name" => "David", "date" => "2010-01-01"},
438
                 ActiveSupport::JSON.decode(json_string_and_date))
A
Alexey Nayden 已提交
439
  end
440

441 442 443 444 445 446
  def test_nil_true_and_false_represented_as_themselves
    assert_equal nil,   nil.as_json
    assert_equal true,  true.as_json
    assert_equal false, false.as_json
  end

447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478
  def test_json_gem_dump_by_passing_active_support_encoder
    h = HashWithAsJson.new
    h[:foo] = "hello"
    h[:bar] = "world"

    assert_equal %({"foo":"hello","bar":"world"}), JSON.dump(h)
    assert_nil h.as_json_called
  end

  def test_json_gem_generate_by_passing_active_support_encoder
    h = HashWithAsJson.new
    h[:foo] = "hello"
    h[:bar] = "world"

    assert_equal %({"foo":"hello","bar":"world"}), JSON.generate(h)
    assert_nil h.as_json_called
  end

  def test_json_gem_pretty_generate_by_passing_active_support_encoder
    h = HashWithAsJson.new
    h[:foo] = "hello"
    h[:bar] = "world"

    assert_equal <<EXPECTED.chomp, JSON.pretty_generate(h)
{
  "foo": "hello",
  "bar": "world"
}
EXPECTED
    assert_nil h.as_json_called
  end

479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494
  def test_twz_to_json_with_use_standard_json_time_format_config_set_to_false
    with_standard_json_time_format(false) do
      zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
      time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
      assert_equal "\"1999/12/31 19:00:00 -0500\"", ActiveSupport::JSON.encode(time)
    end
  end

  def test_twz_to_json_with_use_standard_json_time_format_config_set_to_true
    with_standard_json_time_format(true) do
      zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
      time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
      assert_equal "\"1999-12-31T19:00:00.000-05:00\"", ActiveSupport::JSON.encode(time)
    end
  end

495
  def test_twz_to_json_with_custom_time_precision
496
    with_standard_json_time_format(true) do
497 498 499 500 501
      with_time_precision(0) do
        zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
        time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
        assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(time)
      end
502
    end
503 504 505 506
  end

  def test_time_to_json_with_custom_time_precision
    with_standard_json_time_format(true) do
507 508 509
      with_time_precision(0) do
        assert_equal "\"2000-01-01T00:00:00Z\"", ActiveSupport::JSON.encode(Time.utc(2000))
      end
510 511 512 513 514
    end
  end

  def test_datetime_to_json_with_custom_time_precision
    with_standard_json_time_format(true) do
515 516 517
      with_time_precision(0) do
        assert_equal "\"2000-01-01T00:00:00+00:00\"", ActiveSupport::JSON.encode(DateTime.new(2000))
      end
518
    end
519 520 521 522 523 524 525 526
  end

  def test_twz_to_json_when_wrapping_a_date_time
    zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
    time = ActiveSupport::TimeWithZone.new(DateTime.new(2000), zone)
    assert_equal '"1999-12-31T19:00:00.000-05:00"', ActiveSupport::JSON.encode(time)
  end

527
  protected
528

529 530 531
    def object_keys(json_object)
      json_object[1..-2].scan(/([^{}:,\s]+):/).flatten.sort
    end
532

533 534 535 536 537 538
    def with_standard_json_time_format(boolean = true)
      old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, boolean
      yield
    ensure
      ActiveSupport.use_standard_json_time_format = old
    end
539 540 541 542 543 544 545 546

    def with_time_precision(value)
      old_value = ActiveSupport::JSON::Encoding.time_precision
      ActiveSupport::JSON::Encoding.time_precision = value
      yield
    ensure
      ActiveSupport::JSON::Encoding.time_precision = old_value
    end
547
end