diff --git a/test/test_mailer_url_for.rb b/test/test_mailer_url_for.rb new file mode 100644 index 0000000..22f06b8 --- /dev/null +++ b/test/test_mailer_url_for.rb @@ -0,0 +1,86 @@ +require 'minitest/autorun' +require 'prism' +require 'tmpdir' +require 'fileutils' + +class MailerUrlForTest < Minitest::Test + def setup + @dir = Dir.mktmpdir('prism-mailer-url-for') + FileUtils.mkdir_p(File.join(@dir, 'url_for')) + write('url_for/welcome.text.erb', "Click here: <%= url_for(:confirmation, id: user_id) %>\n") + + @router = Prism::Router.new + @router.add('get', '/confirmations/:id', name: :confirmation) + + mailer_dir = @dir + @mailer_class = Class.new(Prism::Mailer::Base) do + self.template_roots = [mailer_dir] + + def welcome(user_id) + @user_id = user_id + mail(to: 'someone@example.com', subject: 'Welcome!') + end + end + Object.const_set(:UrlForMailer, @mailer_class) + end + + def teardown + Object.send(:remove_const, :UrlForMailer) if Object.const_defined?(:UrlForMailer) + FileUtils.remove_entry(@dir) + Prism::Mailer.router = nil + Prism::Mailer.default_url_options = nil + end + + def write(relative, source) + File.write(File.join(@dir, relative), source) + end + + def test_url_for_raises_without_a_configured_router + Prism::Mailer.router = nil + error = assert_raises(RuntimeError) { UrlForMailer.welcome(42) } + assert_match(/no router configured/, error.message) + end + + def test_url_for_raises_without_a_host + Prism::Mailer.router = @router + error = assert_raises(ArgumentError) { UrlForMailer.welcome(42) } + assert_match(/:host/, error.message) + end + + def test_url_for_builds_an_absolute_url_from_global_default_url_options + Prism::Mailer.router = @router + Prism::Mailer.default_url_options = { host: 'example.com' } + + body = UrlForMailer.welcome(42).message.text_body + assert_equal "Click here: http://example.com/confirmations/42\n", body + end + + def test_per_mailer_class_default_url_options_win_over_the_global_ones + Prism::Mailer.router = @router + Prism::Mailer.default_url_options = { host: 'example.com', protocol: 'http' } + UrlForMailer.default_url_options(host: 'mail.example.com', protocol: 'https') + + body = UrlForMailer.welcome(42).message.text_body + assert_equal "Click here: https://mail.example.com/confirmations/42\n", body + end + + def test_port_is_included_when_given + Prism::Mailer.router = @router + Prism::Mailer.default_url_options = { host: 'example.com', port: 3000 } + + body = UrlForMailer.welcome(42).message.text_body + assert_equal "Click here: http://example.com:3000/confirmations/42\n", body + end + + def test_application_enable_mailer_wires_router_and_default_url_options + app_class = Class.new(Prism::Application) + app_class.router.add('get', '/confirmations/:id', name: :confirmation) + app_class.enable_mailer(host: 'app.example.com', protocol: 'https') + + assert_same app_class.router, Prism::Mailer.router + assert_equal({ host: 'app.example.com', protocol: 'https' }, Prism::Mailer.default_url_options) + + body = UrlForMailer.welcome(7).message.text_body + assert_equal "Click here: https://app.example.com/confirmations/7\n", body + end +end diff --git a/test/test_message_encryptor.rb b/test/test_message_encryptor.rb new file mode 100644 index 0000000..89dfa0e --- /dev/null +++ b/test/test_message_encryptor.rb @@ -0,0 +1,59 @@ +require 'minitest/autorun' +require 'prism' + +class MessageEncryptorTest < Minitest::Test + def setup + @key = Prism::MessageEncryptor.derive_key('super-secret-app-key') + @encryptor = Prism::MessageEncryptor.new(@key) + end + + def test_derived_key_has_correct_length + assert_equal 32, @key.bytesize + end + + def test_derive_key_is_deterministic_for_same_secret_and_salt + assert_equal @key, Prism::MessageEncryptor.derive_key('super-secret-app-key') + end + + def test_derive_key_differs_per_salt + other = Prism::MessageEncryptor.derive_key('super-secret-app-key', salt: 'other-salt') + refute_equal @key, other + end + + def test_round_trips_a_hash + token = @encryptor.encrypt_and_sign('admin' => true, 'user_id' => 7) + assert_equal({ 'admin' => true, 'user_id' => 7 }, @encryptor.decrypt_and_verify(token)) + end + + def test_ciphertext_does_not_contain_plaintext + token = @encryptor.encrypt_and_sign('super-secret-value-xyz') + refute_includes token, 'super-secret-value-xyz' + end + + def test_two_encryptions_of_same_value_differ + first = @encryptor.encrypt_and_sign('same value') + second = @encryptor.encrypt_and_sign('same value') + refute_equal first, second + end + + def test_tampered_ciphertext_fails_to_decrypt + token = @encryptor.encrypt_and_sign('user_id' => 1) + encrypted, iv, tag = token.split('--', 3) + tampered = "#{encrypted}AA--#{iv}--#{tag}" + assert_raises(Prism::MessageEncryptor::InvalidMessage) { @encryptor.decrypt_and_verify(tampered) } + end + + def test_wrong_key_cannot_decrypt + token = @encryptor.encrypt_and_sign('user_id' => 1) + other = Prism::MessageEncryptor.new(Prism::MessageEncryptor.derive_key('a-completely-different-key')) + assert_raises(Prism::MessageEncryptor::InvalidMessage) { other.decrypt_and_verify(token) } + end + + def test_malformed_message_raises + assert_raises(Prism::MessageEncryptor::InvalidMessage) { @encryptor.decrypt_and_verify('garbage') } + end + + def test_rejects_key_of_wrong_length + assert_raises(ArgumentError) { Prism::MessageEncryptor.new('too-short') } + end +end