Refactor message metadata tests

Prior to this commit, there were several places to potentially add a new
message metadata test:

* `SharedMessageMetadataTests` module
* `MessageEncryptorMetadataTest` class
  (includes `SharedMessageMetadataTests`)
* `MessageEncryptorMetadataMarshalTest` class
  (subclasses `MessageEncryptorMetadataTest`)
* `MessageEncryptorMetadataJSONTest` class
  (subclasses `MessageEncryptorMetadataTest`)
* `MessageEncryptorMetadataJsonWithMarshalFallbackTest` class
  (subclasses `MessageEncryptorMetadataTest`)
* `MessageVerifierMetadataTest` class
  (includes `SharedMessageMetadataTests`)
* `MessageVerifierMetadataMarshalTest` class
  (subclasses `MessageVerifierMetadataTest`)
* `MessageVerifierMetadataJsonWithMarshalFallbackTest` class
  (subclasses `MessageVerifierMetadataTest`)
* `MessageVerifierMetadataJsonTest` class
  (subclasses `MessageVerifierMetadataTest`)
* `MessageVerifierMetadataCustomJSONTest` class
  (subclasses `MessageVerifierMetadataTest`)
* `MessageEncryptorMetadataNullSerializerTest` class
  (subclasses `MessageVerifierMetadataTest`)

This commit refactors the message metadata tests, reducing the list to:

* `MessageMetadataTests` module
* `MessageEncryptorMetadataTest` class (includes `MessageMetadataTests`)
* `MessageVerifierMetadataTest` class (includes `MessageMetadataTests`)

This makes it easier to add new tests, as well as new testing scenarios
(e.g. new encryptor / verifier configurations).

Additionally, this commit fixes two tests that were ineffective:

* The `test_passing_expires_in_less_than_a_second_is_not_expired` test
  (which is now simply a part of the "message expires with :expires_in"
  test) did not use the `with_usec: true` option.  Therefore, the time
  resulting from `travel 0.5.seconds` was truncated, effectively
  traveling 0 seconds.
* The `test_verify_with_use_standard_json_time_format_as_false` test
  (which is now named "expiration works with
  ActiveSupport.use_standard_json_time_format = false") did not specify
  an expiration time.  Therefore, the conditional branch that it was
  supposed to test was never exercised.
This commit is contained in:
Jonathan Hefner 2022-12-26 14:32:56 -06:00
parent c5af4f4505
commit c103a4328a
6 changed files with 234 additions and 219 deletions

View File

@ -4,7 +4,6 @@ require_relative "abstract_unit"
require "openssl"
require "active_support/time"
require "active_support/json"
require_relative "metadata/shared_metadata_tests"
class MessageEncryptorTest < ActiveSupport::TestCase
class JSONSerializer
@ -404,44 +403,3 @@ class MessageEncryptorWithHybridSerializerAndWithoutMarshalDumpTest < MessageEnc
super
end
end
class MessageEncryptorMetadataTest < ActiveSupport::TestCase
include SharedMessageMetadataTests
setup do
@secret = SecureRandom.random_bytes(32)
@encryptor = ActiveSupport::MessageEncryptor.new(@secret, **encryptor_options)
end
private
def generate(message, **options)
@encryptor.encrypt_and_sign(message, **options)
end
def parse(data, **options)
@encryptor.decrypt_and_verify(data, **options)
end
def encryptor_options; {} end
end
class MessageEncryptorMetadataMarshalTest < MessageEncryptorMetadataTest
private
def encryptor_options
{ serializer: Marshal }
end
end
class MessageEncryptorMetadataJSONTest < MessageEncryptorMetadataTest
private
def encryptor_options
{ serializer: MessageEncryptorTest::JSONSerializer.new }
end
end
class MessageEncryptorMetadataJsonWithMarshalFallbackTest < MessageEncryptorMetadataTest
private
def encryptor_options
{ serializer: ActiveSupport::JsonWithMarshalFallback }
end
end

View File

@ -4,7 +4,6 @@ require_relative "abstract_unit"
require "openssl"
require "active_support/time"
require "active_support/json"
require_relative "metadata/shared_metadata_tests"
class MessageVerifierTest < ActiveSupport::TestCase
class JSONSerializer
@ -289,91 +288,3 @@ class DefaultJsonSerializerMessageVerifierTest < JsonSerializeAndNoFallbackMessa
ActiveSupport::MessageVerifier.default_message_verifier_serializer = @default_verifier
end
end
class MessageVerifierMetadataTest < ActiveSupport::TestCase
include SharedMessageMetadataTests
setup do
@verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!", **verifier_options)
end
def test_verify_raises_when_purpose_differs
assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do
@verifier.verify(generate(data, purpose: "payment"), purpose: "shipping")
end
end
def test_verify_with_use_standard_json_time_format_as_false
format_before = ActiveSupport.use_standard_json_time_format
ActiveSupport.use_standard_json_time_format = false
assert_equal "My Name", @verifier.verify(generate("My Name"))
ensure
ActiveSupport.use_standard_json_time_format = format_before
end
def test_verify_raises_when_expired
signed_message = generate(data, expires_in: 1.month)
travel 2.months
assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do
@verifier.verify(signed_message)
end
end
private
def generate(message, **options)
@verifier.generate(message, **options)
end
def parse(message, **options)
@verifier.verified(message, **options)
end
def verifier_options
Hash.new
end
end
class MessageVerifierMetadataMarshalTest < MessageVerifierMetadataTest
private
def verifier_options
{ serializer: Marshal }
end
end
class MessageVerifierMetadataJsonWithMarshalFallbackTest < MessageVerifierMetadataTest
private
def verifier_options
{ serializer: ActiveSupport::JsonWithMarshalFallback }
end
end
class MessageVerifierMetadataJsonTest < MessageVerifierMetadataTest
private
def verifier_options
{ serializer: JSON }
end
end
class MessageVerifierMetadataCustomJSONTest < MessageVerifierMetadataTest
private
def verifier_options
{ serializer: MessageVerifierTest::JSONSerializer.new }
end
end
class MessageEncryptorMetadataNullSerializerTest < MessageVerifierMetadataTest
private
def data
"string message"
end
def null_serializing?
true
end
def verifier_options
{ serializer: ActiveSupport::MessageEncryptor::NullSerializer }
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require_relative "../abstract_unit"
require_relative "message_metadata_tests"
class MessageEncryptorMetadataTest < ActiveSupport::TestCase
include MessageMetadataTests
private
def make_codec(**options)
@secret ||= SecureRandom.random_bytes(32)
ActiveSupport::MessageEncryptor.new(@secret, **options)
end
def encode(data, encryptor, **options)
encryptor.encrypt_and_sign(data, **options)
end
def decode(message, encryptor, **options)
encryptor.decrypt_and_verify(message, **options)
rescue ActiveSupport::MessageVerifier::InvalidSignature
nil
end
end

View File

@ -0,0 +1,139 @@
# frozen_string_literal: true
require "active_support/json"
require "active_support/time"
module MessageMetadataTests
extend ActiveSupport::Concern
included do
test "message :purpose must match specified :purpose" do
each_scenario do |data, codec|
assert_roundtrip data, codec, { purpose: "x" }, { purpose: "x" }
assert_no_roundtrip data, codec, { purpose: "x" }, { purpose: "y" }
assert_no_roundtrip data, codec, { purpose: "x" }, {}
assert_no_roundtrip data, codec, {}, { purpose: "x" }
end
end
test ":purpose can be a symbol" do
each_scenario do |data, codec|
assert_roundtrip data, codec, { purpose: :x }, { purpose: :x }
assert_roundtrip data, codec, { purpose: :x }, { purpose: "x" }
assert_roundtrip data, codec, { purpose: "x" }, { purpose: :x }
end
end
test "message expires with :expires_at" do
freeze_time do
each_scenario do |data, codec|
message = encode(data, codec, expires_at: 1.second.from_now)
travel 0.5.seconds, with_usec: true
assert_equal data, decode(message, codec)
travel 0.5.seconds, with_usec: true
assert_nil decode(message, codec)
end
end
end
test "message expires with :expires_in" do
freeze_time do
each_scenario do |data, codec|
message = encode(data, codec, expires_in: 1.second)
travel 0.5.seconds, with_usec: true
assert_equal data, decode(message, codec)
travel 0.5.seconds, with_usec: true
assert_nil decode(message, codec)
end
end
end
test ":expires_at overrides :expires_in" do
each_scenario do |data, codec|
message = encode(data, codec, expires_at: 1.hour.from_now, expires_in: 1.second)
travel 1.minute
assert_equal data, decode(message, codec)
travel 1.hour
assert_nil decode(message, codec)
end
end
test "messages do not expire by default" do
each_scenario do |data, codec|
message = encode(data, codec, purpose: "x")
travel 1000.years
assert_equal data, decode(message, codec, purpose: "x")
end
end
test "expiration works with ActiveSupport.use_standard_json_time_format = false" do
original_use_standard_json_time_format = ActiveSupport.use_standard_json_time_format
ActiveSupport.use_standard_json_time_format = false
each_scenario do |data, codec|
assert_roundtrip data, codec, { expires_at: 1.hour.from_now }
end
ensure
ActiveSupport.use_standard_json_time_format = original_use_standard_json_time_format
end
test "metadata works with NullSerializer" do
codec = make_codec(serializer: ActiveSupport::MessageEncryptor::NullSerializer)
assert_roundtrip "a string", codec, { purpose: "x", expires_in: 1.year }, { purpose: "x" }
end
end
private
class CustomSerializer
def self.dump(value)
JSON.dump(value) << "!"
end
def self.load(value)
JSON.load(value.chomp!("!"))
end
end
SERIALIZERS = [
Marshal,
JSON,
ActiveSupport::JSON,
ActiveSupport::JsonWithMarshalFallback,
CustomSerializer,
]
DATA = [
"a string",
{ "a_number" => 123, "a_time" => Time.local(2004), "an_object" => { "key" => "value" } },
["a string", 123, Time.local(2004), { "key" => "value" }],
]
def each_scenario
SERIALIZERS.each do |serializer|
codec = make_codec(serializer: serializer)
DATA.each do |data|
yield data, codec
end
end
end
def roundtrip(data, codec, encode_options = {}, decode_options = {})
decode(encode(data, codec, **encode_options), codec, **decode_options)
end
def assert_roundtrip(data, codec, encode_options = {}, decode_options = {})
assert_equal data, roundtrip(data, codec, encode_options, decode_options)
end
def assert_no_roundtrip(data, codec, encode_options = {}, decode_options = {})
assert_nil roundtrip(data, codec, encode_options, decode_options)
end
end

View File

@ -0,0 +1,71 @@
# frozen_string_literal: true
require_relative "../abstract_unit"
require_relative "message_metadata_tests"
class MessageVerifierMetadataTest < ActiveSupport::TestCase
include MessageMetadataTests
test "#verify raises when :purpose does not match" do
each_scenario do |data, verifier|
assert_equal data, verifier.verify(verifier.generate(data, purpose: "x"), purpose: "x")
assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
verifier.verify(verifier.generate(data, purpose: "x"), purpose: "y")
end
assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
verifier.verify(verifier.generate(data), purpose: "y")
end
assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
verifier.verify(verifier.generate(data, purpose: "x"))
end
end
end
test "#verify raises when message is expired via :expires_at" do
freeze_time do
each_scenario do |data, verifier|
message = verifier.generate(data, expires_at: 1.second.from_now)
travel 0.5.seconds, with_usec: true
assert_equal data, verifier.verify(message)
travel 0.5.seconds, with_usec: true
assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
verifier.verify(message)
end
end
end
end
test "#verify raises when message is expired via :expires_in" do
freeze_time do
each_scenario do |data, verifier|
message = verifier.generate(data, expires_in: 1.second)
travel 0.5.seconds, with_usec: true
assert_equal data, verifier.verify(message)
travel 0.5.seconds, with_usec: true
assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
verifier.verify(message)
end
end
end
end
private
def make_codec(**options)
ActiveSupport::MessageVerifier.new("secret", **options)
end
def encode(data, verifier, **options)
verifier.generate(data, **options)
end
def decode(message, verifier, **options)
verifier.verified(message, **options)
end
end

View File

@ -1,88 +0,0 @@
# frozen_string_literal: true
module SharedMessageMetadataTests
def null_serializing?
false
end
def test_encryption_and_decryption_with_same_purpose
assert_equal data, parse(generate(data, purpose: "checkout"), purpose: "checkout")
assert_equal data, parse(generate(data))
string_message = "address: #23, main street"
assert_equal string_message, parse(generate(string_message, purpose: "shipping"), purpose: "shipping")
end
def test_verifies_array_when_purpose_matches
skip if null_serializing?
data = [ "credit_card_no: 5012-6748-9087-5678", { "card_holder" => "Donald", "issued_on" => Time.local(2017) }, 12345 ]
assert_equal data, parse(generate(data, purpose: :registration), purpose: :registration)
end
def test_encryption_and_decryption_with_different_purposes_returns_nil
assert_nil parse(generate(data, purpose: "payment"), purpose: "sign up")
assert_nil parse(generate(data, purpose: "payment"))
assert_nil parse(generate(data), purpose: "sign up")
end
def test_purpose_using_symbols
assert_equal data, parse(generate(data, purpose: :checkout), purpose: :checkout)
assert_equal data, parse(generate(data, purpose: :checkout), purpose: "checkout")
assert_equal data, parse(generate(data, purpose: "checkout"), purpose: :checkout)
end
def test_passing_expires_at_sets_expiration_date
encrypted_message = generate(data, expires_at: 1.hour.from_now)
travel 59.minutes
assert_equal data, parse(encrypted_message)
travel 2.minutes
assert_nil parse(encrypted_message)
end
def test_set_relative_expiration_date_by_passing_expires_in
encrypted_message = generate(data, expires_in: 2.hours)
travel 1.hour
assert_equal data, parse(encrypted_message)
travel 1.hour + 1.second
assert_nil parse(encrypted_message)
end
def test_passing_expires_in_less_than_a_second_is_not_expired
freeze_time do
encrypted_message = generate(data, expires_in: 1.second)
travel 0.5.seconds
assert_equal data, parse(encrypted_message)
travel 1.second
assert_nil parse(encrypted_message)
end
end
def test_favor_expires_at_over_expires_in
payment_related_message = generate(data, purpose: "payment", expires_at: 2.year.from_now, expires_in: 1.second)
travel 1.year
assert_equal data, parse(payment_related_message, purpose: :payment)
travel 1.year + 1.day
assert_nil parse(payment_related_message, purpose: "payment")
end
def test_skip_expires_at_and_expires_in_to_disable_expiration_check
payment_related_message = generate(data, purpose: "payment")
travel 100.years
assert_equal data, parse(payment_related_message, purpose: "payment")
end
private
def data
{ "credit_card_no" => "5012-6784-9087-5678", "card_holder" => { "name" => "Donald" } }
end
end