diff --git a/README.md b/README.md index 5f64a7e..3145807 100644 --- a/README.md +++ b/README.md @@ -337,6 +337,34 @@ Or you can use `collect` end ``` +## Async delivering messages + +If you use `Rails` application or you use `ActiveJob`, you can deliver messages using `ActiveJob` + +### Configuration + +You have the next configuration + +```ruby + GraphQL::AnyCable.configure do |config| + # ... other configurations + config.delivery_method = "inline" # the default value "inline", also can be "active_job" + config.queue = "default" # the name of ActiveJob queue + config.job_class = "GraphQL::Adapters::TriggerJob" # the name executor job + end +``` + +`delivery_method` can be either `inline` or `active_job`. +`inline` means that delivering messaging will work sync. +`active_job` - It will adds delivering messages operations to `ActiveJob` with queue `default` and using job `GraphQL::Adapters::TriggerJob` + +You can change the queue or job_class, buy changing it in configuration + +Or you can run code + +```ruby +GraphQL::AnyCable.delivery_method("active_job", queue: "broadcasting", job_class: "GraphQL::Adapters::TriggerJob") +``` ## Testing applications which use `graphql-anycable` diff --git a/graphql-anycable.gemspec b/graphql-anycable.gemspec index 2e008c1..68fa63b 100644 --- a/graphql-anycable.gemspec +++ b/graphql-anycable.gemspec @@ -30,7 +30,6 @@ Gem::Specification.new do |spec| spec.add_dependency "anyway_config", ">= 1.3", "< 3" spec.add_dependency "graphql", ">= 1.11", "< 3" spec.add_dependency "redis", ">= 4.2.0" - spec.add_dependency "activejob", ">= 5.2.0" spec.add_development_dependency "anycable-rails" spec.add_development_dependency "bundler", "~> 2.0" @@ -38,5 +37,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency "railties" spec.add_development_dependency "rake", ">= 12.3.3" spec.add_development_dependency "rspec", "~> 3.0" - spec.add_development_dependency "rspec-rails", "~> 5.0" + spec.add_development_dependency "activejob", ">= 6.0.0" end diff --git a/lib/graphql-anycable.rb b/lib/graphql-anycable.rb index 43323ca..866e120 100644 --- a/lib/graphql-anycable.rb +++ b/lib/graphql-anycable.rb @@ -6,8 +6,8 @@ require_relative "graphql/anycable/cleaner" require_relative "graphql/anycable/config" require_relative "graphql/anycable/railtie" if defined?(Rails) -require_relative "graphql/adapters/base_job" require_relative "graphql/anycable/stats" +require_relative "graphql/adapters/delivery_adapter" require_relative "graphql/subscriptions/anycable_subscriptions" module GraphQL @@ -26,6 +26,16 @@ def self.stats(**options) Stats.new(**options).collect end + def self.delivery_method(kind, queue: "default", job_class: "GraphQL::Adapters::TriggerJob") + config.delivery_method = kind + config.queue = queue + config.job_class = job_class + end + + def self.delivery_adapter(object) + Adapters::DeliveryAdapter.new(executor_object: object) + end + module_function def redis diff --git a/lib/graphql/adapters/base_job.rb b/lib/graphql/adapters/base_job.rb deleted file mode 100644 index 6bd7f35..0000000 --- a/lib/graphql/adapters/base_job.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require "active_job" - -module GraphQL - module Adapters - class BaseJob < ActiveJob::Base - DEFAULT_QUEUE_NAME = :default - - queue_as { GraphQL::AnyCable.config.async_broadcasting["queue"] || DEFAULT_QUEUE_NAME } - - def perform(serialized_payload, execute_method, event_name, args = {}, object = nil, options = {}) - schema = schema_parse(serialized_payload) - - schema.public_send(execute_method, event_name, args, object, **options) - end - - private - - def schema_parse(serialized_payload) - payload = JSON.parse(serialized_payload) - - payload["schema"] = payload["schema"].constantize - payload["serializer"] = payload["serializer"].constantize - - GraphQL::Subscriptions::AnyCableSubscriptions.new(**payload.transform_keys(&:to_sym)) - end - end - end -end diff --git a/lib/graphql/adapters/delivery_adapter.rb b/lib/graphql/adapters/delivery_adapter.rb new file mode 100644 index 0000000..62fba74 --- /dev/null +++ b/lib/graphql/adapters/delivery_adapter.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module GraphQL + module Adapters + class DeliveryAdapter + DELIVERY_METHOD_INLINE = "inline" + DELIVERY_METHOD_ACTIVE_JOB = "active_job" + + attr_reader :executor_object, :executor_method + + def initialize(executor_object:) + @executor_object = executor_object + @executor_method = executor_object.class::EXECUTOR_METHOD_NAME + end + + def trigger(event_name, args, object, **elements) + if config.delivery_method == DELIVERY_METHOD_INLINE + return executor_object.public_send(executor_method, event_name, args, object, **elements) + end + + return if config.delivery_method != DELIVERY_METHOD_ACTIVE_JOB + + executor_class_job.set(queue: config.queue).perform_later( + executor_object.collected_arguments, + executor_method, + event_name, + args, + object, + elements + ) + end + + private + + def executor_class_job + custom_job_class = config.job_class + + return Adapters::TriggerJob unless custom_job_class + + custom_job_class.constantize + end + + def config + GraphQL::AnyCable.config + end + end + end +end diff --git a/lib/graphql/adapters/trigger_job.rb b/lib/graphql/adapters/trigger_job.rb new file mode 100644 index 0000000..d231624 --- /dev/null +++ b/lib/graphql/adapters/trigger_job.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module GraphQL + module Adapters + class TriggerJob < ActiveJob::Base + def perform(payload, execute_method, event_name, args = {}, object = nil, options = {}) + schema = schema_parse(payload) + + schema.public_send(execute_method, event_name, args, object, **options) + end + + private + + def schema_parse(serialized_payload) + payload = serialized_payload + + payload[:schema] = payload[:schema].safe_constantize + payload[:serializer] = payload[:serializer].safe_constantize + + GraphQL::Subscriptions::AnyCableSubscriptions.new(**payload) + end + end + end +end diff --git a/lib/graphql/anycable/config.rb b/lib/graphql/anycable/config.rb index 2816c4c..cba1c19 100644 --- a/lib/graphql/anycable/config.rb +++ b/lib/graphql/anycable/config.rb @@ -13,8 +13,7 @@ class Config < Anyway::Config attr_config use_client_provided_uniq_id: true attr_config redis_prefix: "graphql" # Here, we set clear redis_prefix without any hyphen. The hyphen is added at the end of this value on our side. - attr_config use_async_broadcasting: true - attr_config async_broadcasting: { queue: "broadcasting", class: "GraphQL::Adapters::BaseJob" } + attr_config delivery_method: "inline", queue: "default", job_class: "GraphQL::Adapters::TriggerJob" end end end diff --git a/lib/graphql/anycable/railtie.rb b/lib/graphql/anycable/railtie.rb index 2b27333..82d7ace 100644 --- a/lib/graphql/anycable/railtie.rb +++ b/lib/graphql/anycable/railtie.rb @@ -5,6 +5,13 @@ module GraphQL module AnyCable class Railtie < ::Rails::Railtie + initializer 'graphql_anycable.load_trigger_job' do + #ActiveSupport.on_load(:active_job) do + require "graphql/adapters/trigger_job" if defined?(ActiveJob::Base) + #end + end + + rake_tasks do path = File.expand_path(__dir__) Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f } diff --git a/lib/graphql/subscriptions/anycable_subscriptions.rb b/lib/graphql/subscriptions/anycable_subscriptions.rb index c324607..19aa5fc 100644 --- a/lib/graphql/subscriptions/anycable_subscriptions.rb +++ b/lib/graphql/subscriptions/anycable_subscriptions.rb @@ -57,6 +57,8 @@ class AnyCableSubscriptions < GraphQL::Subscriptions def_delegators :"GraphQL::AnyCable", :redis, :config alias_method :trigger_sync, :trigger + attr_reader :collected_arguments + SUBSCRIPTION_PREFIX = "subscription:" # HASH: Stores subscription data: query, context, … FINGERPRINTS_PREFIX = "fingerprints:" # ZSET: To get fingerprints by topic SUBSCRIPTIONS_PREFIX = "subscriptions:" # SET: To get subscriptions by fingerprint @@ -67,7 +69,7 @@ class AnyCableSubscriptions < GraphQL::Subscriptions def initialize(serializer: Serialize, **rest) @serializer = serializer - @serialized_arguments = serialize_arguments(serializer, rest) + @collected_arguments = collect_arguments(serializer, rest) super end @@ -212,24 +214,11 @@ def delete_channel_subscriptions(channel_or_id) end def trigger(event_name, args, object, **elements) - unless config.use_async_broadcasting - return trigger_sync(event_name, args, object, **elements) - end - - executor_class_job.perform_later( - serialized_arguments, - EXECUTOR_METHOD_NAME, - event_name, - args, - object, - elements - ) + AnyCable.delivery_adapter(self).trigger(event_name, args, object, **elements) end private - attr_reader :serialized_arguments - def anycable @anycable ||= ::AnyCable.broadcast_adapter end @@ -266,31 +255,13 @@ def redis_key(prefix) "#{config.redis_prefix}-#{prefix}" end - def executor_class_job - custom_class = config.async_broadcasting["class"] - - return Adapters::BaseJob unless custom_class - - Object.const_get(config.async_broadcasting["class"]) - end - - def perform(event, object) - unless config.use_async_broadcasting - return public_send(EXECUTOR_METHOD_NAME, event, object) - end - - args = [Marshal.dump(self), EXECUTOR_METHOD_NAME, Marshal.dump(event), Marshal.dump(object)] - - executor_class_job.perform_later(*args) - end - - def serialize_arguments(serializer, payload) + def collect_arguments(serializer, payload) payload = payload.dup payload[:serializer] = serializer.to_s payload[:schema] = payload[:schema].to_s - JSON.dump(payload) + payload end end end diff --git a/spec/adapters/base_job_spec.rb b/spec/adapters/base_job_spec.rb deleted file mode 100644 index b2463c6..0000000 --- a/spec/adapters/base_job_spec.rb +++ /dev/null @@ -1,100 +0,0 @@ -# frozen_string_literal: true - -require "active_job" - -RSpec.describe GraphQL::Adapters::BaseJob, type: :job do - ActiveJob::Base.queue_adapter = :inline - - subject(:job) { described_class.perform_later(*job_payload) } - subject(:broadcast_changes) { AnycableSchema.subscriptions.trigger(*trigger_sync_arguments) } - - before do - AnycableSchema.execute( - query: query, - context: { channel: channel, subscription_id: subscription_id }, - variables: {}, - operation_name: "SomeSubscription", - ) - end - - let(:trigger_sync_arguments) do - [ - :product_updated, - {}, - {id: 1, title: "foo"} - ] - end - - let(:job_payload) do - [ - "{\"schema\":\"AnycableSchema\",\"serializer\":\"GraphQL::Subscriptions::Serialize\"}", - "trigger_sync", - *trigger_sync_arguments - ] - end - - let(:query) do - <<~GRAPHQL - subscription SomeSubscription { productUpdated { id } } - GRAPHQL - end - - let(:channel) do - socket = double("Socket", istate: AnyCable::Socket::State.new({})) - connection = double("Connection", anycable_socket: socket) - double("Channel", id: "legacy_id", params: { "channelId" => "legacy_id" }, stream_from: nil, connection: connection) - end - - let(:subscription_id) do - "some-truly-random-number" - end - - context "when config.use_async_broadcasting is true" do - around do |ex| - GraphQL::AnyCable.config.use_async_broadcasting = true - ex.run - GraphQL::AnyCable.config.use_async_broadcasting = false - end - - it "executes AnyCableSubscriptions" do - ActiveJob::Base.queue_adapter = :inline - - expect_any_instance_of(GraphQL::Subscriptions::AnyCableSubscriptions) - .to receive(:trigger_sync).with(*trigger_sync_arguments) - - expect_any_instance_of(GraphQL::Adapters::BaseJob).to receive(:perform).and_call_original - - broadcast_changes - end - - it "adds BaseJob to enqueued_jobs" do - ActiveJob::Base.queue_adapter = :test - - expect { broadcast_changes }.to change(ActiveJob::Base.queue_adapter.enqueued_jobs, :size).by(1) - end - end - - context "when config.use_async_broadcasting is false" do - before do - GraphQL::AnyCable.config.use_async_broadcasting = false - end - - it "executes AnyCableSubscriptions" do - ActiveJob::Base.queue_adapter = :inline - - expect_any_instance_of(GraphQL::Subscriptions::AnyCableSubscriptions) - .to receive(:trigger_sync).with(*trigger_sync_arguments) - - expect_any_instance_of(GraphQL::Adapters::BaseJob).to_not receive(:perform) - - broadcast_changes - end - - it "does not add BaseJob to enqueued_jobs" do - ActiveJob::Base.queue_adapter = :test - - expect { broadcast_changes }.to change(ActiveJob::Base.queue_adapter.enqueued_jobs, :size).by(0) - end - end -end - diff --git a/spec/adapters/trigger_job_spec.rb b/spec/adapters/trigger_job_spec.rb new file mode 100644 index 0000000..d7c8a08 --- /dev/null +++ b/spec/adapters/trigger_job_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require "active_job" +require "graphql/adapters/trigger_job" + +RSpec.describe GraphQL::Adapters::TriggerJob do + subject(:job) { described_class.perform_later(*job_payload) } + subject(:trigger_changes) { AnycableSchema.subscriptions.trigger(*trigger_sync_arguments) } + + before do + AnycableSchema.execute( + query: query, + context: { channel: channel, subscription_id: subscription_id }, + variables: {}, + operation_name: "SomeSubscription", + ) + end + + let(:trigger_sync_arguments) do + [ + :product_updated, + {}, + {id: 1, title: "foo"} + ] + end + + let(:job_payload) do + [ + { schema: "AnycableSchema", "serializer": "GraphQL::Subscriptions::Serialize" }, + "trigger_sync", + *trigger_sync_arguments + ] + end + + let(:query) do + <<~GRAPHQL + subscription SomeSubscription { productUpdated { id } } + GRAPHQL + end + + let(:channel) do + socket = double("Socket", istate: AnyCable::Socket::State.new({})) + connection = double("Connection", anycable_socket: socket) + double("Channel", id: "legacy_id", params: { "channelId" => "legacy_id" }, stream_from: nil, connection: connection) + end + + let(:subscription_id) do + "some-truly-random-number" + end + + context "when config.delivery_method is active_job" do + around do |ex| + old_queue = ActiveJob::Base.queue_adapter + old_value = GraphQL::AnyCable.config.delivery_method + + GraphQL::AnyCable.config.delivery_method = "active_job" + ActiveJob::Base.queue_adapter = :inline + + ex.run + + GraphQL::AnyCable.config.delivery_method = old_value + ActiveJob::Base.queue_adapter = old_queue + end + + it "executes AnyCableSubscriptions" do + expect_any_instance_of(GraphQL::Subscriptions::AnyCableSubscriptions) + .to receive(:trigger_sync).with(*trigger_sync_arguments) + + expect_any_instance_of(GraphQL::Adapters::TriggerJob).to receive(:perform).and_call_original + expect(GraphQL::Adapters::TriggerJob).to receive(:set).with(queue: "default").and_call_original + + trigger_changes + end + + context "when config.queue is 'test'" do + around do |ex| + old_queue = GraphQL::AnyCable.config.queue + GraphQL::AnyCable.config.queue = "test" + + ex.run + + GraphQL::AnyCable.config.queue = old_queue + end + + it "executes AnyCableSubscriptions" do + expect_any_instance_of(GraphQL::Subscriptions::AnyCableSubscriptions) + .to receive(:trigger_sync).with(*trigger_sync_arguments) + + expect_any_instance_of(GraphQL::Adapters::TriggerJob).to receive(:perform).and_call_original + expect(GraphQL::Adapters::TriggerJob).to receive(:set).with(queue: "test").and_call_original + + trigger_changes + end + end + end +end + diff --git a/spec/graphql/anycable_spec.rb b/spec/graphql/anycable_spec.rb index 7149d1b..cf3649b 100644 --- a/spec/graphql/anycable_spec.rb +++ b/spec/graphql/anycable_spec.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require "active_job" +require "graphql/adapters/trigger_job" + RSpec.describe GraphQL::AnyCable do subject do AnycableSchema.execute( @@ -263,9 +266,39 @@ describe ".stats" do it "calls Graphql::AnyCable::Stats" do - allow_any_instance_of(GraphQL::AnyCable::Stats).to receive(:collect) + expect_any_instance_of(GraphQL::AnyCable::Stats).to receive(:collect) described_class.stats end end + + describe ".delivery_adapter" do + it "calls Adapters::DeliveryAdapter" do + expect(GraphQL::Adapters::DeliveryAdapter).to receive(:new).with(executor_object: "any_object") + + described_class.delivery_adapter("any_object") + end + end + + describe ".delivery_method" do + let(:config) { GraphQL::AnyCable.config } + + after do + config.delivery_method = "inline" + config.queue = "default" + config.job_class = "GraphQL::Adapters::TriggerJob" + end + + it "changes config" do + expect(config.delivery_method).to eq("inline") + expect(config.queue).to eq("default") + expect(config.job_class).to eq("GraphQL::Adapters::TriggerJob") + + described_class.delivery_method("active_job", queue: "test", job_class: "CustomJob") + + expect(config.delivery_method).to eq("active_job") + expect(config.queue).to eq("test") + expect(config.job_class).to eq("CustomJob") + end + end end diff --git a/spec/graphql/broadcast_spec.rb b/spec/graphql/broadcast_spec.rb index fe5d485..4a642c4 100644 --- a/spec/graphql/broadcast_spec.rb +++ b/spec/graphql/broadcast_spec.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true +require "active_job" +require "graphql/adapters/trigger_job" + RSpec.describe "Broadcasting" do + include_context 'Global ID' + def subscribe(query) BroadcastSchema.execute( query: query, @@ -17,7 +22,12 @@ def subscribe(query) end let(:object) do - double("Post", id: 1, title: "Broadcasting…", actions: %w[Edit Delete]) + global_id_instance_double( + Struct.new(:id, :title, :actions), + id: 1, + title: "Broadcasting…", + actions: %w[Edit Delete] + ) end let(:query) do @@ -34,52 +44,149 @@ def subscribe(query) allow(anycable).to receive(:broadcast) end - context "when all clients asks for broadcastable fields only" do - let(:query) do - <<~GRAPHQL.strip - subscription SomeSubscription { postCreated{ id title } } - GRAPHQL + context "when config.deliver_method is active_job" do + around(:all) do |ex| + old_queue = ActiveJob::Base.queue_adapter + old_value = GraphQL::AnyCable.config.delivery_method + + GraphQL::AnyCable.config.delivery_method = "active_job" + ActiveJob::Base.queue_adapter = :inline + + ex.run + + GraphQL::AnyCable.config.delivery_method = old_value + ActiveJob::Base.queue_adapter = old_queue end - it "uses broadcasting to resolve query only once" do - 2.times { subscribe(query) } - BroadcastSchema.subscriptions.trigger(:post_created, {}, object) - expect(object).to have_received(:title).once - expect(anycable).to have_received(:broadcast).once + context "when all clients asks for broadcastable fields only" do + let(:query) do + <<~GRAPHQL.strip + subscription SomeSubscription { postCreated{ id title } } + GRAPHQL + end + + it "uses broadcasting to resolve query only once" do + 2.times { subscribe(query) } + expect_any_instance_of(GraphQL::Adapters::TriggerJob).to receive(:perform).and_call_original + + BroadcastSchema.subscriptions.trigger(:post_created, {}, object) + + expect(object).to have_received(:title).once + expect(anycable).to have_received(:broadcast).once + end end - end - context "when all clients asks for non-broadcastable fields" do - let(:query) do - <<~GRAPHQL.strip - subscription SomeSubscription { postCreated{ id title actions } } - GRAPHQL + context "when all clients asks for non-broadcastable fields" do + let(:query) do + <<~GRAPHQL.strip + subscription SomeSubscription { postCreated{ id title actions } } + GRAPHQL + end + + it "resolves query for every client" do + 2.times { subscribe(query) } + + expect_any_instance_of(GraphQL::Adapters::TriggerJob).to receive(:perform).and_call_original + + BroadcastSchema.subscriptions.trigger(:post_created, {}, object) + expect(object).to have_received(:title).twice + expect(anycable).to have_received(:broadcast).twice + end end - it "resolves query for every client" do - 2.times { subscribe(query) } - BroadcastSchema.subscriptions.trigger(:post_created, {}, object) - expect(object).to have_received(:title).twice - expect(anycable).to have_received(:broadcast).twice + context "when one of subscriptions got expired" do + let(:query) do + <<~GRAPHQL.strip + subscription SomeSubscription { postCreated{ id title } } + GRAPHQL + end + + let(:redis) { AnycableSchema.subscriptions.redis } + + it "doesn't fail" do + 3.times { subscribe(query) } + redis.keys("graphql-subscription:*").last.tap(&redis.method(:del)) + expect(redis.keys("graphql-subscription:*").size).to eq(2) + + expect_any_instance_of(GraphQL::Adapters::TriggerJob).to receive(:perform).and_call_original + + expect { BroadcastSchema.subscriptions.trigger(:post_created, {}, object) }.not_to raise_error + expect(object).to have_received(:title).once + expect(anycable).to have_received(:broadcast).once + end end end - context "when one of subscriptions got expired" do - let(:query) do - <<~GRAPHQL.strip - subscription SomeSubscription { postCreated{ id title } } - GRAPHQL + context "when config.deliver_method is inline" do + around(:all) do |ex| + old_queue = ActiveJob::Base.queue_adapter + old_value = GraphQL::AnyCable.config.delivery_method + + GraphQL::AnyCable.config.delivery_method = "inline" + ActiveJob::Base.queue_adapter = :test + + ex.run + + GraphQL::AnyCable.config.delivery_method = old_value + ActiveJob::Base.queue_adapter = old_queue + end + + context "when all clients asks for broadcastable fields only" do + let(:query) do + <<~GRAPHQL.strip + subscription SomeSubscription { postCreated{ id title } } + GRAPHQL + end + + it "uses broadcasting to resolve query only once" do + 2.times { subscribe(query) } + + expect_any_instance_of(GraphQL::Adapters::TriggerJob).to_not receive(:perform) + + BroadcastSchema.subscriptions.trigger(:post_created, {}, object) + expect(object).to have_received(:title).once + expect(anycable).to have_received(:broadcast).once + end end - let(:redis) { AnycableSchema.subscriptions.redis } + context "when all clients asks for non-broadcastable fields" do + let(:query) do + <<~GRAPHQL.strip + subscription SomeSubscription { postCreated{ id title actions } } + GRAPHQL + end + + it "resolves query for every client" do + 2.times { subscribe(query) } + + expect_any_instance_of(GraphQL::Adapters::TriggerJob).to_not receive(:perform) + + BroadcastSchema.subscriptions.trigger(:post_created, {}, object) + expect(object).to have_received(:title).twice + expect(anycable).to have_received(:broadcast).twice + end + end + + context "when one of subscriptions got expired" do + let(:query) do + <<~GRAPHQL.strip + subscription SomeSubscription { postCreated{ id title } } + GRAPHQL + end + + let(:redis) { AnycableSchema.subscriptions.redis } + + it "doesn't fail" do + 3.times { subscribe(query) } + redis.keys("graphql-subscription:*").last.tap(&redis.method(:del)) + expect(redis.keys("graphql-subscription:*").size).to eq(2) + + expect_any_instance_of(GraphQL::Adapters::TriggerJob).to_not receive(:perform) - it "doesn't fail" do - 3.times { subscribe(query) } - redis.keys("graphql-subscription:*").last.tap(&redis.method(:del)) - expect(redis.keys("graphql-subscription:*").size).to eq(2) - expect { BroadcastSchema.subscriptions.trigger(:post_created, {}, object) }.not_to raise_error - expect(object).to have_received(:title).once - expect(anycable).to have_received(:broadcast).once + expect { BroadcastSchema.subscriptions.trigger(:post_created, {}, object) }.not_to raise_error + expect(object).to have_received(:title).once + expect(anycable).to have_received(:broadcast).once + end end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 60abe6e..ed9bc0e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,9 +7,12 @@ require "pry" require_relative "support/graphql_schema" +require_relative "support/global_id_context" require_relative "redis_helper" RSpec.configure do |config| + config.include_context 'Global ID', include_shared: true + # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = ".rspec_status" diff --git a/spec/support/global_id_context.rb b/spec/support/global_id_context.rb new file mode 100644 index 0000000..bd94630 --- /dev/null +++ b/spec/support/global_id_context.rb @@ -0,0 +1,17 @@ +RSpec.shared_context "Global ID", :global_id do + # TO work this `expect(object).to have_received(:title).once` + # we need to send a spy object (double/instance_double), otherwise we will get an error from Rspec + # "... that object is not a spy or method has not been stubbed" + # Here, we add GlobalID to an instance_double and mock "find" method, for emulation ActiveRecord + def global_id_instance_double(doubled_class, stubs={}) + object = instance_double(doubled_class, stubs).extend(GlobalID::Identification) + + allow(object.class).to receive(:find).and_return(object) + + object + end + + before do + allow(GlobalID).to receive(:app).and_return("example") unless GlobalID.app + end +end