# (c) 2017 Ribose Inc. # # No point in ensuring a trailing comma in multiline argument lists here. # rubocop:disable Style/TrailingCommaInArguments require "spec_helper" RSpec.shared_examples "Attr Masker gem feature specs" do before do stub_const "User", user_class_definition User.class_eval do def jedi? email.ends_with? "@jedi.example.test" end end end let!(:han) do User.create!( first_name: "Han", last_name: "Solo", email: "han@example.test", avatar: Marshal.dump("Millenium Falcon photo"), ) end let!(:luke) do User.create!( first_name: "Luke", last_name: "Skywalker", email: "luke@jedi.example.test", avatar: Marshal.dump("photo with a light saber"), ) end example "Masking a single text attribute with default options" do User.class_eval do attr_masker :last_name end expect { run_rake_task }.not_to(change { User.count }) [han, luke].each do |record| expect { record.reload }.to( change { record.last_name }.to("(redacted)") & preserve { record.first_name } & preserve { record.email } ) end end example "Specifying multiple attributes in an attr_masker declaration" do User.class_eval do attr_masker :first_name, :last_name end expect { run_rake_task }.not_to(change { User.count }) [han, luke].each do |record| expect { record.reload }.to( change { record.first_name }.to("(redacted)") & change { record.last_name }.to("(redacted)") & preserve { record.email } ) end end example "Skipping some records when a symbol is passed to :if option" do User.class_eval do attr_masker :first_name, :last_name, if: :jedi? end expect { run_rake_task }.not_to(change { User.count }) expect { han.reload }.to( preserve { han.first_name } & preserve { han.last_name } & preserve { han.email } ) expect { luke.reload }.to( change { luke.first_name }.to("(redacted)") & change { luke.last_name }.to("(redacted)") & preserve { luke.email } ) end example "Skipping some records when a lambda is passed to :if option" do User.class_eval do attr_masker :first_name, :last_name, if: ->(r) { r.jedi? } end expect { run_rake_task }.not_to(change { User.count }) expect { han.reload }.to( preserve { han.first_name } & preserve { han.last_name } & preserve { han.email } ) expect { luke.reload }.to( change { luke.first_name }.to("(redacted)") & change { luke.last_name }.to("(redacted)") & preserve { luke.email } ) end example "Skipping some records when a symbol is passed to :unless option" do User.class_eval do attr_masker :first_name, :last_name, unless: :jedi? end expect { run_rake_task }.not_to(change { User.count }) expect { han.reload }.to( change { han.first_name }.to("(redacted)") & change { han.last_name }.to("(redacted)") & preserve { han.email } ) expect { luke.reload }.to( preserve { luke.first_name } & preserve { luke.last_name } & preserve { luke.email } ) end example "Skipping some records when a lambda is passed to :unless option" do User.class_eval do attr_masker :first_name, :last_name, unless: ->(r) { r.jedi? } end expect { run_rake_task }.not_to(change { User.count }) expect { han.reload }.to( change { han.first_name }.to("(redacted)") & change { han.last_name }.to("(redacted)") & preserve { han.email } ) expect { luke.reload }.to( preserve { luke.first_name } & preserve { luke.last_name } & preserve { luke.email } ) end example "Using a custom masker" do reverse_masker = ->(value:, **_) do value.reverse end upcase_masker = ->(value:, **_) do value.upcase end User.class_eval do attr_masker :first_name, masker: reverse_masker attr_masker :last_name, masker: upcase_masker end expect { run_rake_task }.not_to(change { User.count }) expect { han.reload }.to( change { han.first_name }.to("naH") & change { han.last_name }.to("SOLO") & preserve { han.email } ) expect { luke.reload }.to( change { luke.first_name }.to("ekuL") & change { luke.last_name }.to("SKYWALKER") & preserve { luke.email } ) end example "Using a custom masker with model" do first_name_masker = ->(value:, model:, **_) do value.reverse + model.email end last_name_masker = ->(value:, model:, **_) do model.email + value.upcase end User.class_eval do attr_masker :first_name, masker: first_name_masker attr_masker :last_name, masker: last_name_masker end expect { run_rake_task }.not_to(change { User.count }) expect { han.reload }.to( change { han.first_name }.to("naHhan@example.test") & change { han.last_name }.to("han@example.testSOLO") & preserve { han.email } ) expect { luke.reload }.to( change { luke.first_name }.to("ekuLluke@jedi.example.test") & change { luke.last_name }.to("luke@jedi.example.testSKYWALKER") & preserve { luke.email } ) end example "Masked value is assigned via attribute writer" do User.class_eval do attr_masker :first_name, :last_name def first_name=(value) self[:first_name] = "#{value} with side effects" end def last_name=(value) self[:last_name] = value end end expect { run_rake_task }.not_to(change { User.count }) expect { han.reload }.to( change { han.first_name }.to("(redacted) with side effects") & change { han.last_name }.to("(redacted)") & preserve { han.email } ) expect { luke.reload }.to( change { luke.first_name }.to("(redacted) with side effects") & change { luke.last_name }.to("(redacted)") & preserve { luke.email } ) end example "Masking a marshalled attribute" do User.class_eval do attr_masker :avatar, marshal: true end expect { run_rake_task }.not_to(change { User.count }) expect { han.reload }.to( preserve { han.first_name } & preserve { han.last_name } & preserve { han.email } & change { han.avatar } ) expect(han.avatar).to eq(Marshal.dump("(redacted)")) expect { luke.reload }.to( preserve { luke.first_name } & preserve { luke.last_name } & preserve { luke.email } & change { luke.avatar } ) expect(luke.avatar).to eq(Marshal.dump("(redacted)")) end example "Masking a marshalled attribute with a custom marshaller" do module CustomMarshal module_function def load_marshalled(*args) Marshal.load(*args) # rubocop:disable Security/MarshalLoad end def dump_json(*args) JSON.dump(json: args) end end User.class_eval do attr_masker( :avatar, marshal: true, marshaler: CustomMarshal, load_method: :load_marshalled, dump_method: :dump_json, ) end expect { run_rake_task }.not_to(change { User.count }) expect { han.reload }.to( preserve { han.first_name } & preserve { han.last_name } & preserve { han.email } & change { han.avatar } ) expect(han.avatar).to eq({ json: ["(redacted)"] }.to_json) expect { luke.reload }.to( preserve { luke.first_name } & preserve { luke.last_name } & preserve { luke.email } & change { luke.avatar } ) expect(luke.avatar).to eq({ json: ["(redacted)"] }.to_json) end example "Masking an attribute which spans on more than one table column \ (or document field)" do User.class_eval do attr_masker :full_name, column_names: %i[first_name last_name], masker: ->(**_) { "(first) (last)" } def full_name [first_name, last_name].join(" ") end def full_name=(value) self.first_name = value.split(" ")[-2] self.last_name = value.split(" ")[-1] end end expect { run_rake_task }.not_to(change { User.count }) expect { han.reload }.to( change { han.first_name }.to("(first)") & change { han.last_name }.to("(last)") & preserve { han.email } & preserve { han.avatar } ) expect { luke.reload }.to( change { luke.first_name }.to("(first)") & change { luke.last_name }.to("(last)") & preserve { luke.email } & preserve { luke.avatar } ) end example "It is disabled in production environment" do allow(Rails).to receive(:env) { "production".inquiry } User.class_eval do attr_masker :last_name end expect { run_rake_task }.to( preserve { User.count } & raise_exception(AttrMasker::Error) ) [han, luke].each do |record| expect { record.reload }.not_to(change { record }) end end example "It masks records disregarding default scope" do User.class_eval do attr_masker :last_name default_scope -> { where(last_name: "Solo") } end expect { run_rake_task }.not_to(change { User.unscoped.count }) [han, luke].each do |record| expect { record.reload }.to( change { record.last_name }.to("(redacted)") ) end end example "It loads configuration file", :force_config_file_reload do expect { run_rake_task }.to change { $CONFIG_LOADED_AT } end def run_rake_task Rake::Task["db:mask"].execute end end # rubocop:enable Style/TrailingCommaInArguments