Repository: rails/rails
File: activerecord/test/cases/signed_id_test.rb (Ruby)

Code before:

    Account.signed_id_verifier = ActiveSupport::MessageVerifier.new("old secret")
    old_account_signed_id = @account.signed_id
    Account.signed_id_verifier = ActiveSupport::MessageVerifier.new("new secret")
    Account.signed_id_verifier.rotate("old secret")
    on_rotation_is_called = false
    assert Account.find_signed(old_account_signed_id, on_rotation: -> { on_rotation_is_called = true })
    assert on_rotation_is_called
    on_rotation_is_called = false
    assert Account.find_signed!(old_account_signed_id, on_rotation: -> { on_rotation_is_called = true })
    assert on_rotation_is_called
  ensure
    Account.signed_id_verifier = old_verifier
  end

  test "Account uses legacy signed ID verifier when secret is set for the model" do
    account_signed_id_with_message_verifiers = @account.signed_id
    assert_equal @account, Account.find_signed(account_signed_id_with_message_verifiers)

    ActiveRecord.deprecator.silence do
      Account.signed_id_verifier_secret = "account secret"
    end
    account_signed_id_with_secret = @account.signed_id
    assert_equal @account, Account.find_signed(account_signed_id_with_secret)

    assert_not_equal account_signed_id_with_message_verifiers, account_signed_id_with_secret
  ensure
    ActiveRecord.deprecator.silence do
      Account.signed_id_verifier_secret = nil
      Account.instance_variable_set :@signed_id_verifier, nil
    end
  end

  test "model-specific signed_id_verifier_secret does not affect other models" do
    toy_signed_id = @toy.signed_id

    ActiveRecord.deprecator.silence do
      Account.signed_id_verifier_secret = "account secret"
    end

    assert_equal @toy.signed_id, toy_signed_id
  ensure
    ActiveRecord.deprecator.silence do
      Account.signed_id_verifier_secret = nil
      Account.instance_variable_set :@signed_id_verifier, nil
    end
  end

  test "fail to work when signed_id_verifier_secret lambda is nil" do
    ActiveRecord.deprecator.silence do
      Account.signed_id_verifier_secret = -> { nil }

Review comment (security):
This isn't actually testing that `signed_id_verifier` uses legacy options — it is merely testing that the signed ID changes when the secret changes.

We can test that `signed_id_verifier` uses legacy options by round-tripping a message, similar to my suggestion for the tests in `railties/test/application/configuration_test.rb`.


Code after:
  test "when signed_id_verifier_secret is set, signed_id_verifier behaves as singleton instance variable" do
    ActiveRecord.deprecator.silence do
      model_class.signed_id_verifier_secret = "secret"
    end

    assert_no_changes -> { [ActiveRecord::Base.signed_id_verifier, model_subclass.signed_id_verifier] } do
      model_class.signed_id_verifier = ActiveSupport::MessageVerifier.new("secret")
    end
  end

  test "when signed_id_verifier_secret is set, signed_id_verifier uses legacy options by default" do
    ActiveRecord.deprecator.silence do
      model_class.signed_id_verifier_secret = "secret"
    end

    legacy_verifier = ActiveSupport::MessageVerifier.new("secret", digest: "SHA256", serializer: JSON, url_safe: true)

    assert_equal "message", legacy_verifier.verify(model_class.signed_id_verifier.generate("message"))
    assert_equal "message", model_class.signed_id_verifier.verify(legacy_verifier.generate("message"))
  end

  test "on_rotation callback using custom verifier" do
    model_class.signed_id_verifier = ActiveSupport::MessageVerifier.new("old secret")
    old_signed_id = model.signed_id

    on_rotation_is_called = false
    model_class.signed_id_verifier = ActiveSupport::MessageVerifier.new("new secret", on_rotation: -> { on_rotation_is_called = true })
    model_class.signed_id_verifier.rotate("old secret")

    model_class.find_signed(old_signed_id)
    assert on_rotation_is_called
  end

  test "on_rotation callback using find_signed & find_signed!" do
    model_class.signed_id_verifier = ActiveSupport::MessageVerifier.new("old secret")
    old_signed_id = model.signed_id

    model_class.signed_id_verifier = ActiveSupport::MessageVerifier.new("new secret")
    model_class.signed_id_verifier.rotate("old secret")

    on_rotation_is_called = false
    assert model_class.find_signed(old_signed_id, on_rotation: -> { on_rotation_is_called = true })
    assert on_rotation_is_called

    on_rotation_is_called = false
    assert model_class.find_signed!(old_signed_id, on_rotation: -> { on_rotation_is_called = true })
    assert on_rotation_is_called
  end

  test "cannot get a signed ID for a new record" do
    assert_raises ArgumentError, match: /Cannot get a signed_id for a new record/ do