diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c413af9..1124ae7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,4 +46,6 @@ jobs: POSTGRES_HOST: 127.0.0.1 POSTGRES_PORT: 5432 ADAPTER: postgresql + - name: Run rubocop + run: bundle exec rubocop . diff --git a/.rubocop.yml b/.rubocop.yml index 68f9313..2be1106 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,18 +1,28 @@ +require: + - rubocop-factory_bot + - rubocop-rspec + AllCops: + NewCops: enable TargetRubyVersion: 2.7 + Exclude: + - lib/active_outbox/generators/templates/migration.rb -Metrics/BlockLength: +Gemspec/RequireMFA: + Enabled: false + +Lint/DuplicateMethods: Exclude: - - app/admin/**/* - - config/**/* - - spec/**/* + - lib/active_outbox/configuration.rb + +Metrics/MethodLength: + Max: 12 -Style/StringLiteralsInInterpolation: - Enabled: true - EnforcedStyle: double_quotes +RSpec/MultipleMemoizedHelpers: + Max: 7 -Layout/LineLength: - Max: 120 +RSpec/NestedGroups: + Max: 4 -Layout/MultilineMethodCallIndentation: - EnforcedStyle: indented +Style/Documentation: + Enabled: false diff --git a/Gemfile b/Gemfile index 90778cc..e8eb7c0 100644 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,13 @@ source 'https://rubygems.org' gemspec -group :test do - gem 'database_cleaner-active_record', '~> 2.0' -end +gem 'byebug', '~> 11.1.3' +gem 'database_cleaner-active_record', '~> 2.1.0' +gem 'pg', '~> 1.5.4' +gem 'pry-rails', '~> 0.3.9' +gem 'reek', '~> 6.1.4' +gem 'rspec', '~> 3.12.0' +gem 'rubocop', '~> 1.56.3', require: false +gem 'rubocop-rspec', '~> 2.24.1', require: false +gem 'simplecov', '~> 0.22.0' +gem 'sqlite3', '1.4.2' diff --git a/Gemfile.lock b/Gemfile.lock index 7e8feca..f98c026 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,7 @@ PATH remote: . specs: active_outbox (0.0.2) - rails (~> 7.0) + rails (~> 7.0.8) GEM remote: https://rubygems.org/ @@ -114,7 +114,7 @@ GEM net-protocol net-protocol (0.2.1) timeout - net-smtp (0.3.3) + net-smtp (0.4.0) net-protocol nio4r (2.5.9) nokogiri (1.15.4) @@ -201,6 +201,14 @@ GEM unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.29.0) parser (>= 3.2.1.0) + rubocop-capybara (2.19.0) + rubocop (~> 1.41) + rubocop-factory_bot (2.24.0) + rubocop (~> 1.33) + rubocop-rspec (2.24.1) + rubocop (~> 1.33) + rubocop-capybara (~> 2.17) + rubocop-factory_bot (~> 2.22) ruby-progressbar (1.13.0) simplecov (0.22.0) docile (~> 1.1) @@ -217,7 +225,7 @@ GEM websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - zeitwerk (2.6.11) + zeitwerk (2.6.12) PLATFORMS ruby @@ -228,12 +236,13 @@ PLATFORMS DEPENDENCIES active_outbox! byebug (~> 11.1.3) - database_cleaner-active_record (~> 2.0) + database_cleaner-active_record (~> 2.1.0) pg (~> 1.5.4) pry-rails (~> 0.3.9) reek (~> 6.1.4) - rspec (~> 3.0) + rspec (~> 3.12.0) rubocop (~> 1.56.3) + rubocop-rspec (~> 2.24.1) simplecov (~> 0.22.0) sqlite3 (= 1.4.2) diff --git a/active_outbox.gemspec b/active_outbox.gemspec index 7dbf5f5..2018598 100644 --- a/active_outbox.gemspec +++ b/active_outbox.gemspec @@ -1,29 +1,17 @@ # frozen_string_literal: true -Gem::Specification.new do |s| - s.name = 'active_outbox' - s.version = '0.0.2' - s.summary = 'ActiveOutbox' - s.description = 'A Transactional Outbox implementation for ActiveRecord' - s.authors = ['Guillermo Aguirre'] - s.email = 'guillermoaguirre1@gmail.com' - s.files = Dir['LICENSE.txt', 'README.md', 'lib/**/*', 'lib/active_outbox.rb'] - s.executables = ['outbox'] - s.homepage = - 'https://rubygems.org/gems/active_outbox' - s.license = 'MIT' - s.required_ruby_version = '>= 2.7.0' +Gem::Specification.new do |spec| + spec.authors = ['Guillermo Aguirre'] + spec.files = Dir['LICENSE.txt', 'README.md', 'lib/**/*', 'lib/active_outbox.rb'] + spec.name = 'active_outbox' + spec.summary = 'A Transactional Outbox implementation for ActiveRecord' + spec.version = '0.0.2' - # Dependencies - s.add_dependency 'rails', '~> 7.0' + spec.email = 'guillermoaguirre1@gmail.com' + spec.executables = ['outbox'] + spec.homepage = 'https://rubygems.org/gems/active_outbox' + spec.license = 'MIT' + spec.required_ruby_version = '>= 2.7.8' - # Development dependencies - s.add_development_dependency 'byebug', '~> 11.1.3' - s.add_development_dependency 'pg', '~> 1.5.4' - s.add_development_dependency 'pry-rails', '~> 0.3.9' - s.add_development_dependency 'reek', '~> 6.1.4' - s.add_development_dependency 'rspec', '~> 3.0' - s.add_development_dependency 'rubocop', '~> 1.56.3' - s.add_development_dependency 'simplecov', '~> 0.22.0' - s.add_development_dependency 'sqlite3', '1.4.2' + spec.add_dependency 'rails', '~> 7.0.8' end diff --git a/lib/active_outbox/generators/active_outbox_generator.rb b/lib/active_outbox/generators/active_outbox_generator.rb index 7a66d82..ec3eb74 100644 --- a/lib/active_outbox/generators/active_outbox_generator.rb +++ b/lib/active_outbox/generators/active_outbox_generator.rb @@ -11,7 +11,7 @@ class ActiveOutboxGenerator < ActiveRecord::Generators::Base class_option :root_components_path, type: :string, default: Rails.root def create_migration_files - migration_path = "#{options["root_components_path"]}/db/migrate" + migration_path = "#{options['root_components_path']}/db/migrate" migration_template( 'migration.rb', "#{migration_path}/outbox_create_#{table_name}.rb", diff --git a/lib/active_outbox/generators/templates/migration.rb b/lib/active_outbox/generators/templates/migration.rb index 1b045da..ff7b31a 100644 --- a/lib/active_outbox/generators/templates/migration.rb +++ b/lib/active_outbox/generators/templates/migration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class OutboxCreate<%= table_name.camelize.singularize %> < ActiveRecord::Migration<%= migration_version %> def change create_table :<%= table_name %> do |t| diff --git a/lib/active_outbox/outboxable.rb b/lib/active_outbox/outboxable.rb index 32cf63f..e7d929b 100644 --- a/lib/active_outbox/outboxable.rb +++ b/lib/active_outbox/outboxable.rb @@ -14,7 +14,7 @@ module Outboxable const_name = "#{klass}_#{value}" unless module_parent::Events.const_defined?(const_name) - module_parent::Events.const_set(const_name, "#{const_name}#{namespace.blank? ? "" : "."}#{namespace}") + module_parent::Events.const_set(const_name, "#{const_name}#{namespace.blank? ? '' : '.'}#{namespace}") end event_name = module_parent::Events.const_get(const_name) @@ -24,32 +24,24 @@ module Outboxable end def save(**options, &block) - @outbox_event = options[:outbox_event].underscore.upcase if options[:outbox_event].present? - + assign_outbox_event(options) super(**options, &block) end def save!(**options, &block) - @outbox_event = options[:outbox_event].underscore.upcase if options[:outbox_event].present? - + assign_outbox_event(options) super(**options, &block) end private - def create_outbox!(action, event_name) - unless self.class.module_parent.const_defined?('OUTBOX_MODEL') - *namespace, _klass = self.class.name.underscore.upcase.split('/') - namespace.reverse.join('.') - outbox_model_name = ActiveOutbox.configuration.outbox_mapping[self.class.module_parent.name.underscore] || - ActiveOutbox.configuration.outbox_mapping['default'] - raise OutboxClassNotFoundError if outbox_model_name.nil? - - outbox_model = outbox_model_name.safe_constantize - self.class.module_parent.const_set('OUTBOX_MODEL', outbox_model) - end + def assign_outbox_event(options) + @outbox_event = options[:outbox_event].underscore.upcase if options[:outbox_event].present? + end - outbox = self.class.module_parent.const_get('OUTBOX_MODEL').new( + def create_outbox!(action, event_name) + outbox_model = determine_outbox_model + outbox = outbox_model.new( aggregate: self.class.name, aggregate_identifier: try(:identifier) || id, event: @outbox_event || event_name, @@ -58,42 +50,60 @@ def create_outbox!(action, event_name) ) @outbox_event = nil - if outbox.invalid? - outbox.errors.each do |error| - errors.import(error, attribute: "outbox.#{error.attribute}") - end + handle_outbox_errors(outbox) if outbox.invalid? + outbox.save! + end + + def determine_outbox_model + unless self.class.module_parent.const_defined?('OUTBOX_MODEL') + outbox_model_name = determine_outbox_model_name + outbox_model = outbox_model_name.safe_constantize + self.class.module_parent.const_set('OUTBOX_MODEL', outbox_model) end + self.class.module_parent.const_get('OUTBOX_MODEL') + end - outbox.save! + def determine_outbox_model_name + outbox_model_name_from_config || raise(OutboxClassNotFoundError) end - def formatted_payload(action) - payload = payload(action) - case ActiveRecord::Base.connection.adapter_name.downcase - when 'postgresql' - payload - else - payload.to_json + def outbox_model_name_from_config + namespace_outbox_mapping || default_outbox_mapping + end + + def namespace_outbox_mapping + namespace = self.class.name.split('/').first + ActiveOutbox.configuration.outbox_mapping[namespace&.underscore] + end + + def default_outbox_mapping + ActiveOutbox.configuration.outbox_mapping['default'] + end + + def handle_outbox_errors(outbox) + outbox.errors.each do |error| + errors.import(error, attribute: "outbox.#{error.attribute}") end end - def payload(action) - payload = { before: nil, after: nil } + def formatted_payload(action) + payload = construct_payload(action) + ActiveRecord::Base.connection.adapter_name.downcase == 'postgresql' ? payload : payload.to_json + end + + def construct_payload(action) case action when :create - payload[:after] = as_json + { before: nil, after: as_json } when :update - # previous_changes => { 'name' => ['bob', 'robert'] } changes = previous_changes.transform_values(&:first) - payload[:before] = as_json.merge(changes) - payload[:after] = as_json + { before: as_json.merge(changes), after: as_json } when :destroy - payload[:before] = as_json + { before: as_json, after: nil } else raise ActiveRecord::RecordNotSaved.new("Failed to create Outbox payload for #{self.class.name}: #{identifier}", self) end - payload end end end diff --git a/spec/lib/active_outbox/outboxable_spec.rb b/spec/lib/active_outbox/outboxable_spec.rb new file mode 100644 index 0000000..603c0a2 --- /dev/null +++ b/spec/lib/active_outbox/outboxable_spec.rb @@ -0,0 +1,269 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ActiveOutbox::Outboxable do + let(:fake_model_instance) { FakeModel.new(identifier: identifier) } + let(:identifier) { SecureRandom.uuid } + + let(:outbox_record_attributes) do + { + 'event' => event, + 'aggregate' => 'FakeModel', + 'aggregate_identifier' => aggregate_identifier, + 'payload' => { + 'before' => payload_before, + 'after' => payload_after + } + } + end + let(:aggregate_identifier) { identifier } + let(:payload_before) { nil } + let(:payload_after) { FakeModel.last.as_json } + + shared_examples 'creates the outbox record' do + it { expect { subject }.to change(Outbox, :count).by(1) } + end + + shared_examples 'creates the record and the outbox record' do + include_examples 'creates the outbox record' + + it { expect { subject }.to change(FakeModel, :count).by(1) } + end + + shared_examples 'creates the outbox record with the correct data' do + it { expect { subject }.to create_outbox_record(Outbox).with_attributes(-> { outbox_record_attributes }) } + end + + shared_examples 'updates the record' do + specify do + expect { subject }.to not_change(FakeModel, :count) + .and change(fake_model_instance, :identifier).to(new_identifier) + end + end + + shared_examples 'does not create neither the record nor the outbox record' do + it { expect { subject }.not_to change(FakeModel, :count) } + it { expect { subject }.not_to change(Outbox, :count) } + end + + shared_examples 'raises an error and does not create neither the record nor the outbox record' do |error_class| + it { expect { subject }.to raise_error(error_class) } + it { expect { subject }.to raise_error(error_class).and not_change(FakeModel, :count) } + it { expect { subject }.to raise_error(error_class).and not_change(Outbox, :count) } + end + + describe '#save' do + subject(:save_instance) { fake_model_instance.save } + + context 'when record is created' do + context 'when outbox record is created' do + let(:event) { 'FAKE_MODEL_CREATED' } + + include_examples 'creates the record and the outbox record' + include_examples 'creates the outbox record with the correct data' + + it { is_expected.to be true } + end + + context 'when there is a record invalid error when creating the outbox record' do + before do + payload = { + before: nil, + after: { + id: 1, + identifier: '7d8f60e3-5e7f-4c11-b18b-5cc01ceea3da' + } + } + + outbox = Outbox.new( + identifier: SecureRandom.uuid, + event: nil, + payload: payload, + aggregate: FakeModel.name, + aggregate_identifier: fake_model_instance.identifier + ) + + allow(Outbox).to receive(:new).and_return(outbox) + end + + include_examples 'does not create neither the record nor the outbox record' + + it { is_expected.to be false } + + it 'adds the errors to the model' do + expect { save_instance }.to change { + fake_model_instance.errors.messages + }.from({}).to({ 'outbox.event': ["can't be blank"] }) + end + end + + context 'when there is an error when creating the outbox record' do + before do + outbox = instance_double(Outbox, invalid?: false) + allow(Outbox).to receive(:new).and_return(outbox) + allow(outbox).to receive(:save!).and_raise(ActiveRecord::RecordNotSaved) + end + + include_examples 'raises an error and does not create neither the record nor the outbox record', + ActiveRecord::RecordNotSaved + end + end + + context 'when the record could not be created' do + let(:identifier) { nil } + + include_examples 'does not create neither the record nor the outbox record' + + it { is_expected.to be false } + end + + context 'when record is updated' do + subject(:save_instance) do + fake_model_instance.identifier = new_identifier + fake_model_instance.save + end + + let(:fake_model_instance) { FakeModel.create(identifier: identifier) } + + context 'when outbox record is created' do + let(:event) { 'FAKE_MODEL_UPDATED' } + let(:aggregate_identifier) { new_identifier } + let(:payload_before) { fake_model_instance.as_json } + + before { payload_before } + + include_examples 'creates the outbox record' + include_examples 'creates the outbox record with the correct data' + include_examples 'updates the record' do + let(:new_identifier) { SecureRandom.uuid } + end + + it { is_expected.to be true } + end + end + end + + describe '#save!' do + subject(:bang_save_instance) { fake_model_instance.save! } + + context 'when record is created' do + context 'when outbox record is created' do + include_examples 'creates the record and the outbox record' + + it { is_expected.to be true } + end + + context 'when there is a record invalid error when creating the outbox record' do + before do + payload = { + before: nil, + after: { + id: 1, + identifier: '7d8f60e3-5e7f-4c11-b18b-5cc01ceea3da' + } + } + + outbox = Outbox.new( + identifier: SecureRandom.uuid, + event: nil, + payload: payload, + aggregate: FakeModel.name, + aggregate_identifier: fake_model_instance.identifier + ) + + allow(Outbox).to receive(:new).and_return(outbox) + end + + include_examples 'raises an error and does not create neither the record nor the outbox record', + ActiveRecord::RecordInvalid + + it 'adds the errors to the model' do + expect { bang_save_instance }.to raise_error(ActiveRecord::RecordInvalid) + .and change { fake_model_instance.errors.messages }.from({}).to({ 'outbox.event': ["can't be blank"] }) + end + end + + context 'when there is an error when creating the outbox record' do + before do + outbox = instance_double(Outbox, invalid?: false) + allow(Outbox).to receive(:new).and_return(outbox) + allow(outbox).to receive(:save!).and_raise(ActiveRecord::RecordNotSaved) + end + + include_examples 'raises an error and does not create neither the record nor the outbox record', + ActiveRecord::RecordNotSaved + end + end + + context 'when the record could not be created' do + let(:identifier) { nil } + + include_examples 'raises an error and does not create neither the record nor the outbox record', + ActiveRecord::RecordInvalid + end + end + + describe '#create' do + subject(:create_instance) { FakeModel.create(identifier: identifier) } + + context 'when record is created' do + context 'when outbox record is created' do + let(:event) { 'FAKE_MODEL_CREATED' } + + include_examples 'creates the record and the outbox record' + include_examples 'creates the outbox record with the correct data' + + it { is_expected.to eq(FakeModel.last) } + end + end + end + + describe '#update' do + subject(:update_instance) { fake_model_instance.update(identifier: new_identifier) } + + let!(:fake_model_instance) { FakeModel.create(identifier: identifier) } + + context 'when record is updated' do + context 'when outbox record is created' do + let(:event) { 'FAKE_MODEL_UPDATED' } + let(:aggregate_identifier) { new_identifier } + let(:payload_before) { fake_model_instance.as_json } + let(:payload_after) { fake_model_instance.reload.as_json } + + before { payload_before } + + include_examples 'creates the outbox record' + include_examples 'creates the outbox record with the correct data' + include_examples 'updates the record' do + let(:new_identifier) { SecureRandom.uuid } + end + + it { is_expected.to be true } + end + end + end + + describe '#destroy' do + subject(:destroy_instance) { fake_model_instance.destroy } + + let!(:fake_model_instance) { FakeModel.create(identifier: identifier) } + + context 'when record is destroyed' do + context 'when outbox record is created' do + let(:event) { 'FAKE_MODEL_DESTROYED' } + let(:payload_before) { fake_model_instance.as_json } + let(:payload_after) { nil } + + include_examples 'creates the outbox record' + include_examples 'creates the outbox record with the correct data' + + it { is_expected.to eq(fake_model_instance) } + + it 'destroys the record' do + expect { destroy_instance }.to change(FakeModel, :count).by(-1) + end + end + end + end +end diff --git a/spec/lib/outboxable_spec.rb b/spec/lib/outboxable_spec.rb deleted file mode 100644 index 38f4c7c..0000000 --- a/spec/lib/outboxable_spec.rb +++ /dev/null @@ -1,341 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ActiveOutbox::Outboxable do - describe '#save' do - subject { fake_model_instance.save } - - let(:fake_model_instance) { FakeModel.new(identifier: identifier) } - let(:identifier) { SecureRandom.uuid } - - context 'when record is created' do - context 'when outbox record is created' do - it { is_expected.to be true } - - it 'creates the record' do - expect { subject }.to change(FakeModel, :count).by(1) - end - - it 'creates the outbox record' do - expect { subject }.to change(Outbox, :count).by(1) - end - - it 'creates the outbox record with the correct data' do - expect { subject }.to create_outbox_record(Outbox).with_attributes(lambda { - { - 'event' => 'FAKE_MODEL_CREATED', - 'aggregate' => 'FakeModel', - 'aggregate_identifier' => identifier, - 'payload' => { - 'before' => nil, - 'after' => FakeModel.last.as_json - } - } - }) - end - end - - context 'when there is a record invalid error when creating the outbox record' do - before do - payload = { - before: nil, - after: { - id: 1, - identifier: '7d8f60e3-5e7f-4c11-b18b-5cc01ceea3da' - } - } - outbox = Outbox.new(identifier: SecureRandom.uuid, event: nil, payload: payload, aggregate: FakeModel.name, - aggregate_identifier: fake_model_instance.identifier) - allow(Outbox).to receive(:new).and_return(outbox) - end - - it { is_expected.to be false } - - it 'does not create the record' do - expect { subject }.to change(FakeModel, :count).by(0) - end - - it 'does not create the outbox record' do - expect { subject }.to change(Outbox, :count).by(0) - end - - it 'adds the errors to the model' do - expect { subject }.to change { - fake_model_instance.errors.messages - }.from({}).to({ "outbox.event": ["can't be blank"] }) - end - end - - context 'when there is an error when creating the outbox record' do - before do - outbox = instance_double(Outbox, invalid?: false) - allow(Outbox).to receive(:new).and_return(outbox) - allow(outbox).to receive(:save!).and_raise(ActiveRecord::RecordNotSaved) - end - - it 'raises error' do - expect { subject }.to raise_error(ActiveRecord::RecordNotSaved) - end - - it 'does not create the record' do - expect { subject }.to raise_error(ActiveRecord::RecordNotSaved).and change(FakeModel, :count).by(0) - end - - it 'does not create the outbox record' do - expect { subject }.to raise_error(ActiveRecord::RecordNotSaved).and change(Outbox, :count).by(0) - end - end - end - - context 'when the record could not be created' do - let(:identifier) { nil } - - it { is_expected.to be false } - - it 'does not create the record' do - expect { subject }.to change(FakeModel, :count).by(0) - end - - it 'does not create the outbox record' do - expect { subject }.to change(Outbox, :count).by(0) - end - end - - context 'when record is updated' do - subject do - fake_model_instance.identifier = new_identifier - fake_model_instance.save - end - - let(:fake_model_instance) { FakeModel.create(identifier: identifier) } - let!(:fake_model_json) { fake_model_instance.as_json } - let(:identifier) { SecureRandom.uuid } - let(:new_identifier) { SecureRandom.uuid } - - context 'when outbox record is created' do - it { is_expected.to be true } - - it 'updates the record' do - expect { subject }.to change(FakeModel, :count).by(0) - .and change(fake_model_instance, - :identifier).to(new_identifier) - end - - it 'creates the outbox record' do - expect { subject }.to change(Outbox, :count).by(1) - end - - it 'creates the outbox record with the correct data' do - expect { subject }.to create_outbox_record(Outbox).with_attributes(lambda { - { - 'event' => 'FAKE_MODEL_UPDATED', - 'aggregate' => 'FakeModel', - 'aggregate_identifier' => new_identifier, - 'payload' => { - 'before' => fake_model_json, - 'after' => FakeModel.last.as_json - } - } - }) - end - end - end - end - - describe '#save!' do - subject { fake_model_instance.save! } - - let(:identifier) { SecureRandom.uuid } - let(:fake_model_instance) { FakeModel.new(identifier: identifier) } - - context 'when record is created' do - context 'when outbox record is created' do - it { is_expected.to be true } - - it 'creates the record' do - expect { subject }.to change(FakeModel, :count).by(1) - end - - it 'creates the outbox record' do - expect { subject }.to change(Outbox, :count).by(1) - end - end - - context 'when there is a record invalid error when creating the outbox record' do - before do - payload = { - before: nil, - after: { - id: 1, - identifier: '7d8f60e3-5e7f-4c11-b18b-5cc01ceea3da' - } - } - outbox = Outbox.new(identifier: SecureRandom.uuid, event: nil, payload: payload, aggregate: FakeModel.name, - aggregate_identifier: fake_model_instance.identifier) - allow(Outbox).to receive(:new).and_return(outbox) - end - - it 'raises error' do - expect { subject }.to raise_error(ActiveRecord::RecordInvalid) - end - - it 'does not create the record' do - expect { subject }.to raise_error(ActiveRecord::RecordInvalid).and change(FakeModel, :count).by(0) - end - - it 'does not create the outbox record' do - expect { subject }.to raise_error(ActiveRecord::RecordInvalid).and change(Outbox, :count).by(0) - end - - it 'adds the errors to the model' do - expect { subject }.to raise_error(ActiveRecord::RecordInvalid) - .and change { fake_model_instance.errors.messages }.from({}).to({ "outbox.event": ["can't be blank"] }) - end - end - - context 'when there is an error when creating the outbox record' do - before do - outbox = instance_double(Outbox, invalid?: false) - allow(Outbox).to receive(:new).and_return(outbox) - allow(outbox).to receive(:save!).and_raise(ActiveRecord::RecordNotSaved) - end - - it 'raises error' do - expect { subject }.to raise_error(ActiveRecord::RecordNotSaved) - end - - it 'does not create the record' do - expect { subject }.to raise_error(ActiveRecord::RecordNotSaved).and change(FakeModel, :count).by(0) - end - - it 'does not create the outbox record' do - expect { subject }.to raise_error(ActiveRecord::RecordNotSaved).and change(Outbox, :count).by(0) - end - end - end - - context 'when the record could not be created' do - let(:identifier) { nil } - - it 'raises error' do - expect { subject }.to raise_error(ActiveRecord::RecordInvalid) - end - - it 'does not create the record' do - expect { subject }.to raise_error(ActiveRecord::RecordInvalid).and change(FakeModel, :count).by(0) - end - - it 'does not create the outbox record' do - expect { subject }.to raise_error(ActiveRecord::RecordInvalid).and change(Outbox, :count).by(0) - end - end - end - - describe '#create' do - subject { FakeModel.create(identifier: identifier) } - - let(:identifier) { SecureRandom.uuid } - - context 'when record is created' do - context 'when outbox record is created' do - it { is_expected.to eq(FakeModel.last) } - - it 'creates the record' do - expect { subject }.to change(FakeModel, :count).by(1) - end - - it 'creates the outbox record' do - expect { subject }.to change(Outbox, :count).by(1) - end - - it 'creates the outbox record with the correct data' do - expect { subject }.to create_outbox_record(Outbox).with_attributes(lambda { - { - 'event' => 'FAKE_MODEL_CREATED', - 'aggregate' => 'FakeModel', - 'aggregate_identifier' => identifier, - 'payload' => { - 'before' => nil, - 'after' => FakeModel.last.as_json - } - } - }) - end - end - end - end - - describe '#update' do - subject { fake_model.update(identifier: new_identifier) } - - let!(:fake_model) { FakeModel.create(identifier: identifier) } - let!(:fake_old_model) { fake_model.as_json } - let(:identifier) { SecureRandom.uuid } - let(:new_identifier) { SecureRandom.uuid } - - context 'when record is updated' do - context 'when outbox record is created' do - it { is_expected.to be true } - - it 'updates the record' do - expect { subject }.to change(FakeModel, :count).by(0) - .and change(fake_model, :identifier).to(new_identifier) - end - - it 'creates the outbox record' do - expect { subject }.to change(Outbox, :count).by(1) - end - - it 'creates the outbox record with the correct data' do - expect { subject }.to create_outbox_record(Outbox).with_attributes(lambda { - { - 'event' => 'FAKE_MODEL_UPDATED', - 'aggregate' => 'FakeModel', - 'aggregate_identifier' => new_identifier, - 'payload' => { - 'before' => fake_old_model, - 'after' => fake_model.reload.as_json - } - } - }) - end - end - end - end - - describe '#destroy' do - subject { fake_model.destroy } - - let!(:fake_model) { FakeModel.create(identifier: identifier) } - let(:identifier) { SecureRandom.uuid } - - context 'when record is destroyed' do - context 'when outbox record is created' do - it { is_expected.to eq(fake_model) } - - it 'destroys the record' do - expect { subject }.to change(FakeModel, :count).by(-1) - end - - it 'creates the outbox record' do - expect { subject }.to change(Outbox, :count).by(1) - end - - it 'creates the outbox record with the correct data' do - expect { subject }.to create_outbox_record(Outbox).with_attributes(lambda { - { - 'event' => 'FAKE_MODEL_DESTROYED', - 'aggregate' => 'FakeModel', - 'aggregate_identifier' => identifier, - 'payload' => { - 'before' => fake_model.as_json, - 'after' => nil - } - } - }) - end - end - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f891e70..7761a7b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -20,9 +20,9 @@ db_config = if ENV['ADAPTER'] == 'postgresql' { adapter: 'postgresql', - username: ENV['POSTGRES_USER'], - host: ENV['POSTGRES_HOST'], - port: ENV['POSTGRES_PORT'] + username: ENV.fetch('POSTGRES_USER', nil), + host: ENV.fetch('POSTGRES_HOST', nil), + port: ENV.fetch('POSTGRES_PORT', nil) } else { @@ -82,12 +82,12 @@ def self.name DatabaseCleaner.clean_with(:truncation) end - config.before(:each) do + config.before do DatabaseCleaner.strategy = :transaction DatabaseCleaner.start end - config.after(:each) do + config.after do DatabaseCleaner.clean end end diff --git a/spec/support/outboxable_test_helpers.rb b/spec/support/outboxable_test_helpers.rb index e9eba36..7574185 100644 --- a/spec/support/outboxable_test_helpers.rb +++ b/spec/support/outboxable_test_helpers.rb @@ -23,7 +23,7 @@ module OutboxableTestHelpers end match_when_negated(notify_expectation_failures: true) do |actual| - expect { actual.call }.to_not change(outbox_class, :count) + expect { actual.call }.not_to change(outbox_class, :count) end supports_block_expectations