ReggieB
ReggieB

Reputation: 8212

How do you use fixtures with attr_encrypted

I want to test a model that uses attr_encrypted to encrypt a secret in the database

class Thing
  attr_encrypted :secret, encode: true
end

But when I define the secret in a fixture the encoded newline character gets escaped out.

one:
  encrypted_secret: '<%= Thing.encrypt_secret(SecureRandom.uuid) %>'

That is:

'axZFZEknxUSYdUlPhwLBbj8CwSeCW5at2INA98EcCcY7MVFdmXvk7Sb4DZhC\nm6qD\n'

Is stored in the database as:

'axZFZEknxUSYdUlPhwLBbj8CwSeCW5at2INA98EcCcY7MVFdmXvk7Sb4DZhC
 m6qD'

The problem with this is that this then fails:

thing = things(:one)
assert_equal thing, Thing.find_by_secret(thing.secret)

Thing.find_by_secret(thing.secret) returns nil because the resulting SQL query tries to match the two versions of the encryped secret and fails to get a match.

I have tried:

one:
  encrypted_secret: 'axZFZEknxUSYdUlPhwLBbj8CwSeCW5at2INA98EcCcY7MVFdmXvk7Sb4DZhC\nm6qD\n'

but get the same result.

How can I configure my fixtures to work with attr_encrypted?

Upvotes: 5

Views: 1016

Answers (2)

Fumisky Wells
Fumisky Wells

Reputation: 1199

I faced the same situation under Rails4 + attr_encrypted + fixture + Minitest environment, and here my workaround is.

In summary, I had the following steps:

  1. write plain (= unencrypted) text fixture with a specific file extention (in my case, it is *.yml.noenc).
  2. write rake-task to convert from the plain fixture (.yml.noenc) to encrypted fixture (.yml).

Let me explain the detail below.

For example, "Message" model has two attributes 'name' and 'body' which are required to be encrypted as follows:

class Message < ActiveRecord::Base
  attr_encrypted :name, key: ...
  attr_encrypted :body, key: ...
  ...
end
  1. write test/fixtures/messages.yml.noenc as follows, which has plain name and body text:
msg1:
  name: Hello
  body: Hello, I am here...
msg2:
  name: How are you
  body: Good morning, ...
  1. write like the following rake-task (e.g. lib/tasks/encrypt_fixture.rake) to convert messages.yml.noenc to messages.yml:
require 'active_record/fixtures'

src_yml   = 'test/fixtures/messages.yml.noenc'
dest_yml  = 'test/fixtures/messages.yml'

task 'test'   => dest_yml

namespace :[MY_APP] do
  desc "generate encrypted fixture"
  file dest_yml => src_yml do |t|
    require Rails.root + 'config/environment'

    encrypted_hash  = {}
    for k, v in YAML.load(ERB.new(File.read(Rails.root + src_yml)).result) do
      msg = Message.new(v.merge([ANY ADDITIONAL ATTRS]))
      encrypted_hash[k] = {
        'encrypted_name'    => msg.encrypted_name,
        'encrypted_name_iv' => msg.encrypted_name_iv,
        'encrypted_body'    => msg.encrypted_body,
        'encrypted_body_iv' => msg.encrypted_body_iv,
        [ANY ADDITIONAL KEY_N_VALUE]
      }
    end

    File.open(Rails.root + t.name, 'w') do |f|
      f.write(<<EOH)
    #----------------------------------------------------------------------
    # DO NOT MODIFY THIS FILE!!
    #
    # This file is generated from #{src_yml} by:
    #
    #   (edit #{src_yml})
    #   $ rake [MY_APP]:generate_fixture, or
    #   $ rake
    #----------------------------------------------------------------------
EOH
      f.write(encrypted_hash.to_yaml)
    end
  end
end

Please substitute [MY_APP], [ANY ADDITIONAL ATTRS], and [ANY ADDITIONAL KEY_N_VALUE] to actual values.

Then, 'rake' or 'rake test' checks file dependency between messages.yml.noenc and messages.yml, and generate messages.yml when necessary before 'rake test'.

Upvotes: 0

ReggieB
ReggieB

Reputation: 8212

A solution that works is to replace all '\n' with '\\n' and use double quotes. This works:

one:
  encryped_secret: "<%= Thing.encrypt_secret(SecureRandom.uuid).gsub(/\n/, '\\\\n') %>"

Is there a tidier way to do this?

Upvotes: 0

Related Questions