diff options
Diffstat (limited to 'spec/lib/gitlab/event_store/store_spec.rb')
-rw-r--r-- | spec/lib/gitlab/event_store/store_spec.rb | 262 |
1 files changed, 262 insertions, 0 deletions
diff --git a/spec/lib/gitlab/event_store/store_spec.rb b/spec/lib/gitlab/event_store/store_spec.rb new file mode 100644 index 00000000000..711e1d5b4d5 --- /dev/null +++ b/spec/lib/gitlab/event_store/store_spec.rb @@ -0,0 +1,262 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::EventStore::Store do + let(:event_klass) { stub_const('TestEvent', Class.new(Gitlab::EventStore::Event)) } + let(:event) { event_klass.new(data: data) } + let(:another_event_klass) { stub_const('TestAnotherEvent', Class.new(Gitlab::EventStore::Event)) } + + let(:worker) do + stub_const('EventSubscriber', Class.new).tap do |klass| + klass.class_eval do + include ApplicationWorker + include Gitlab::EventStore::Subscriber + + def handle_event(event) + event.data + end + end + end + end + + let(:another_worker) do + stub_const('AnotherEventSubscriber', Class.new).tap do |klass| + klass.class_eval do + include ApplicationWorker + include Gitlab::EventStore::Subscriber + end + end + end + + let(:unrelated_worker) do + stub_const('UnrelatedEventSubscriber', Class.new).tap do |klass| + klass.class_eval do + include ApplicationWorker + include Gitlab::EventStore::Subscriber + end + end + end + + before do + event_klass.class_eval do + def schema + { + 'required' => %w[name id], + 'type' => 'object', + 'properties' => { + 'name' => { 'type' => 'string' }, + 'id' => { 'type' => 'integer' } + } + } + end + end + end + + describe '#subscribe' do + it 'subscribes a worker to an event' do + store = described_class.new do |s| + s.subscribe worker, to: event_klass + end + + subscriptions = store.subscriptions[event_klass] + expect(subscriptions.map(&:worker)).to contain_exactly(worker) + end + + it 'subscribes multiple workers to an event' do + store = described_class.new do |s| + s.subscribe worker, to: event_klass + s.subscribe another_worker, to: event_klass + end + + subscriptions = store.subscriptions[event_klass] + expect(subscriptions.map(&:worker)).to contain_exactly(worker, another_worker) + end + + it 'subscribes a worker to multiple events is separate calls' do + store = described_class.new do |s| + s.subscribe worker, to: event_klass + s.subscribe worker, to: another_event_klass + end + + subscriptions = store.subscriptions[event_klass] + expect(subscriptions.map(&:worker)).to contain_exactly(worker) + + subscriptions = store.subscriptions[another_event_klass] + expect(subscriptions.map(&:worker)).to contain_exactly(worker) + end + + it 'subscribes a worker to multiple events in a single call' do + store = described_class.new do |s| + s.subscribe worker, to: [event_klass, another_event_klass] + end + + subscriptions = store.subscriptions[event_klass] + expect(subscriptions.map(&:worker)).to contain_exactly(worker) + + subscriptions = store.subscriptions[another_event_klass] + expect(subscriptions.map(&:worker)).to contain_exactly(worker) + end + + it 'subscribes a worker to an event with condition' do + store = described_class.new do |s| + s.subscribe worker, to: event_klass, if: ->(event) { event.data[:name] == 'Alice' } + end + + subscriptions = store.subscriptions[event_klass] + + expect(subscriptions.size).to eq(1) + + subscription = subscriptions.first + expect(subscription).to be_an_instance_of(Gitlab::EventStore::Subscription) + expect(subscription.worker).to eq(worker) + expect(subscription.condition.call(double(data: { name: 'Bob' }))).to eq(false) + expect(subscription.condition.call(double(data: { name: 'Alice' }))).to eq(true) + end + + it 'refuses the subscription if the target is not an Event object' do + expect do + described_class.new do |s| + s.subscribe worker, to: Integer + end + end.to raise_error( + Gitlab::EventStore::Error, + /Event being subscribed to is not a subclass of Gitlab::EventStore::Event/) + end + + it 'refuses the subscription if the subscriber is not a worker' do + expect do + described_class.new do |s| + s.subscribe double, to: event_klass + end + end.to raise_error( + Gitlab::EventStore::Error, + /Subscriber is not an ApplicationWorker/) + end + end + + describe '#publish' do + let(:data) { { name: 'Bob', id: 123 } } + + context 'when event has a subscribed worker' do + let(:store) do + described_class.new do |store| + store.subscribe worker, to: event_klass + store.subscribe another_worker, to: another_event_klass + end + end + + it 'dispatches the event to the subscribed worker' do + expect(worker).to receive(:perform_async).with('TestEvent', data) + expect(another_worker).not_to receive(:perform_async) + + store.publish(event) + end + + context 'when other workers subscribe to the same event' do + let(:store) do + described_class.new do |store| + store.subscribe worker, to: event_klass + store.subscribe another_worker, to: event_klass + store.subscribe unrelated_worker, to: another_event_klass + end + end + + it 'dispatches the event to each subscribed worker' do + expect(worker).to receive(:perform_async).with('TestEvent', data) + expect(another_worker).to receive(:perform_async).with('TestEvent', data) + expect(unrelated_worker).not_to receive(:perform_async) + + store.publish(event) + end + end + + context 'when an error is raised' do + before do + allow(worker).to receive(:perform_async).and_raise(NoMethodError, 'the error message') + end + + it 'is rescued and tracked' do + expect(Gitlab::ErrorTracking) + .to receive(:track_and_raise_for_dev_exception) + .with(kind_of(NoMethodError), event_class: event.class.name, event_data: event.data) + .and_call_original + + expect { store.publish(event) }.to raise_error(NoMethodError, 'the error message') + end + end + + it 'raises and tracks an error when event is published inside a database transaction' do + expect(Gitlab::ErrorTracking) + .to receive(:track_and_raise_for_dev_exception) + .at_least(:once) + .and_call_original + + expect do + ApplicationRecord.transaction do + store.publish(event) + end + end.to raise_error(Sidekiq::Worker::EnqueueFromTransactionError) + end + + it 'refuses publishing if the target is not an Event object' do + expect { store.publish(double(:event)) } + .to raise_error( + Gitlab::EventStore::Error, + /Event being published is not an instance of Gitlab::EventStore::Event/) + end + end + + context 'when event has subscribed workers with condition' do + let(:store) do + described_class.new do |s| + s.subscribe worker, to: event_klass, if: -> (event) { event.data[:name] == 'Bob' } + s.subscribe another_worker, to: event_klass, if: -> (event) { event.data[:name] == 'Alice' } + end + end + + let(:event) { event_klass.new(data: data) } + + it 'dispatches the event to the workers satisfying the condition' do + expect(worker).to receive(:perform_async).with('TestEvent', data) + expect(another_worker).not_to receive(:perform_async) + + store.publish(event) + end + end + end + + describe 'subscriber' do + let(:data) { { name: 'Bob', id: 123 } } + let(:event_name) { event.class.name } + let(:worker_instance) { worker.new } + + subject { worker_instance.perform(event_name, data) } + + it 'handles the event' do + expect(worker_instance).to receive(:handle_event).with(instance_of(event.class)) + + expect_any_instance_of(event.class) do |event| + expect(event).to receive(:data).and_return(data) + end + + subject + end + + context 'when the event name does not exist' do + let(:event_name) { 'UnknownClass' } + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::EventStore::InvalidEvent) + end + end + + context 'when the worker does not define handle_event method' do + let(:worker_instance) { another_worker.new } + + it 'raises an error' do + expect { subject }.to raise_error(NotImplementedError) + end + end + end +end |