diff options
Diffstat (limited to 'actioncable/test/subscription_adapter')
9 files changed, 455 insertions, 0 deletions
diff --git a/actioncable/test/subscription_adapter/async_test.rb b/actioncable/test/subscription_adapter/async_test.rb new file mode 100644 index 0000000000..6e038259b5 --- /dev/null +++ b/actioncable/test/subscription_adapter/async_test.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "test_helper" +require_relative "common" + +class AsyncAdapterTest < ActionCable::TestCase + include CommonSubscriptionAdapterTest + + def setup + super + + @tx_adapter.shutdown + @tx_adapter = @rx_adapter + end + + def cable_config + { adapter: "async" } + end +end diff --git a/actioncable/test/subscription_adapter/base_test.rb b/actioncable/test/subscription_adapter/base_test.rb new file mode 100644 index 0000000000..999dc0cba1 --- /dev/null +++ b/actioncable/test/subscription_adapter/base_test.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "test_helper" +require "stubs/test_server" + +class ActionCable::SubscriptionAdapter::BaseTest < ActionCable::TestCase + ## TEST THAT ERRORS ARE RETURNED FOR INHERITORS THAT DON'T OVERRIDE METHODS + + class BrokenAdapter < ActionCable::SubscriptionAdapter::Base + end + + setup do + @server = TestServer.new + @server.config.subscription_adapter = BrokenAdapter + @server.config.allowed_request_origins = %w( http://rubyonrails.com ) + end + + test "#broadcast returns NotImplementedError by default" do + assert_raises NotImplementedError do + BrokenAdapter.new(@server).broadcast("channel", "payload") + end + end + + test "#subscribe returns NotImplementedError by default" do + callback = lambda { puts "callback" } + success_callback = lambda { puts "success" } + + assert_raises NotImplementedError do + BrokenAdapter.new(@server).subscribe("channel", callback, success_callback) + end + end + + test "#unsubscribe returns NotImplementedError by default" do + callback = lambda { puts "callback" } + + assert_raises NotImplementedError do + BrokenAdapter.new(@server).unsubscribe("channel", callback) + end + end + + # TEST METHODS THAT ARE REQUIRED OF THE ADAPTER'S BACKEND STORAGE OBJECT + + test "#broadcast is implemented" do + assert_nothing_raised do + SuccessAdapter.new(@server).broadcast("channel", "payload") + end + end + + test "#subscribe is implemented" do + callback = lambda { puts "callback" } + success_callback = lambda { puts "success" } + + assert_nothing_raised do + SuccessAdapter.new(@server).subscribe("channel", callback, success_callback) + end + end + + test "#unsubscribe is implemented" do + callback = lambda { puts "callback" } + + assert_nothing_raised do + SuccessAdapter.new(@server).unsubscribe("channel", callback) + end + end +end diff --git a/actioncable/test/subscription_adapter/channel_prefix.rb b/actioncable/test/subscription_adapter/channel_prefix.rb new file mode 100644 index 0000000000..3071facd9d --- /dev/null +++ b/actioncable/test/subscription_adapter/channel_prefix.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "test_helper" + +class ActionCable::Server::WithIndependentConfig < ActionCable::Server::Base + # ActionCable::Server::Base defines config as a class variable. + # Need config to be an instance variable here as we're testing 2 separate configs + def config + @config ||= ActionCable::Server::Configuration.new + end +end + +module ChannelPrefixTest + def test_channel_prefix + server2 = ActionCable::Server::WithIndependentConfig.new + server2.config.cable = alt_cable_config + server2.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN } + + adapter_klass = server2.config.pubsub_adapter + + rx_adapter2 = adapter_klass.new(server2) + tx_adapter2 = adapter_klass.new(server2) + + subscribe_as_queue("channel") do |queue| + subscribe_as_queue("channel", rx_adapter2) do |queue2| + @tx_adapter.broadcast("channel", "hello world") + tx_adapter2.broadcast("channel", "hello world 2") + + assert_equal "hello world", queue.pop + assert_equal "hello world 2", queue2.pop + end + end + end + + def alt_cable_config + cable_config.merge(channel_prefix: "foo") + end +end diff --git a/actioncable/test/subscription_adapter/common.rb b/actioncable/test/subscription_adapter/common.rb new file mode 100644 index 0000000000..b3e9ae9d5c --- /dev/null +++ b/actioncable/test/subscription_adapter/common.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +require "test_helper" +require "concurrent" + +require "active_support/core_ext/hash/indifferent_access" +require "pathname" + +module CommonSubscriptionAdapterTest + WAIT_WHEN_EXPECTING_EVENT = 3 + WAIT_WHEN_NOT_EXPECTING_EVENT = 0.2 + + def setup + server = ActionCable::Server::Base.new + server.config.cable = cable_config.with_indifferent_access + server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN } + + adapter_klass = server.config.pubsub_adapter + + @rx_adapter = adapter_klass.new(server) + @tx_adapter = adapter_klass.new(server) + end + + def teardown + [@rx_adapter, @tx_adapter].uniq.compact.each(&:shutdown) + end + + def subscribe_as_queue(channel, adapter = @rx_adapter) + queue = Queue.new + + callback = -> data { queue << data } + subscribed = Concurrent::Event.new + adapter.subscribe(channel, callback, Proc.new { subscribed.set }) + subscribed.wait(WAIT_WHEN_EXPECTING_EVENT) + assert_predicate subscribed, :set? + + yield queue + + sleep WAIT_WHEN_NOT_EXPECTING_EVENT + assert_empty queue + ensure + adapter.unsubscribe(channel, callback) if subscribed.set? + end + + def test_subscribe_and_unsubscribe + subscribe_as_queue("channel") do |queue| + end + end + + def test_basic_broadcast + subscribe_as_queue("channel") do |queue| + @tx_adapter.broadcast("channel", "hello world") + + assert_equal "hello world", queue.pop + end + end + + def test_broadcast_after_unsubscribe + keep_queue = nil + subscribe_as_queue("channel") do |queue| + keep_queue = queue + + @tx_adapter.broadcast("channel", "hello world") + + assert_equal "hello world", queue.pop + end + + @tx_adapter.broadcast("channel", "hello void") + + sleep WAIT_WHEN_NOT_EXPECTING_EVENT + assert_empty keep_queue + end + + def test_multiple_broadcast + subscribe_as_queue("channel") do |queue| + @tx_adapter.broadcast("channel", "bananas") + @tx_adapter.broadcast("channel", "apples") + + received = [] + 2.times { received << queue.pop } + assert_equal ["apples", "bananas"], received.sort + end + end + + def test_identical_subscriptions + subscribe_as_queue("channel") do |queue| + subscribe_as_queue("channel") do |queue_2| + @tx_adapter.broadcast("channel", "hello") + + assert_equal "hello", queue_2.pop + end + + assert_equal "hello", queue.pop + end + end + + def test_simultaneous_subscriptions + subscribe_as_queue("channel") do |queue| + subscribe_as_queue("other channel") do |queue_2| + @tx_adapter.broadcast("channel", "apples") + @tx_adapter.broadcast("other channel", "oranges") + + assert_equal "apples", queue.pop + assert_equal "oranges", queue_2.pop + end + end + end + + def test_channel_filtered_broadcast + subscribe_as_queue("channel") do |queue| + @tx_adapter.broadcast("other channel", "one") + @tx_adapter.broadcast("channel", "two") + + assert_equal "two", queue.pop + end + end + + def test_long_identifiers + channel_1 = "a" * 100 + "1" + channel_2 = "a" * 100 + "2" + subscribe_as_queue(channel_1) do |queue| + subscribe_as_queue(channel_2) do |queue_2| + @tx_adapter.broadcast(channel_1, "apples") + @tx_adapter.broadcast(channel_2, "oranges") + + assert_equal "apples", queue.pop + assert_equal "oranges", queue_2.pop + end + end + end +end diff --git a/actioncable/test/subscription_adapter/inline_test.rb b/actioncable/test/subscription_adapter/inline_test.rb new file mode 100644 index 0000000000..6305626b2b --- /dev/null +++ b/actioncable/test/subscription_adapter/inline_test.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "test_helper" +require_relative "common" + +class InlineAdapterTest < ActionCable::TestCase + include CommonSubscriptionAdapterTest + + def setup + super + + @tx_adapter.shutdown + @tx_adapter = @rx_adapter + end + + def cable_config + { adapter: "inline" } + end +end diff --git a/actioncable/test/subscription_adapter/postgresql_test.rb b/actioncable/test/subscription_adapter/postgresql_test.rb new file mode 100644 index 0000000000..5fb26a8896 --- /dev/null +++ b/actioncable/test/subscription_adapter/postgresql_test.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "test_helper" +require_relative "common" + +require "active_record" + +class PostgresqlAdapterTest < ActionCable::TestCase + include CommonSubscriptionAdapterTest + + def setup + database_config = { "adapter" => "postgresql", "database" => "activerecord_unittest" } + ar_tests = File.expand_path("../../../activerecord/test", __dir__) + if Dir.exist?(ar_tests) + require File.join(ar_tests, "config") + require File.join(ar_tests, "support/config") + local_config = ARTest.config["connections"]["postgresql"]["arunit"] + database_config.update local_config if local_config + end + + ActiveRecord::Base.establish_connection database_config + + begin + ActiveRecord::Base.connection + rescue + @rx_adapter = @tx_adapter = nil + skip "Couldn't connect to PostgreSQL: #{database_config.inspect}" + end + + super + end + + def teardown + super + + ActiveRecord::Base.clear_all_connections! + end + + def cable_config + { adapter: "postgresql" } + end + + def test_clear_active_record_connections_adapter_still_works + server = ActionCable::Server::Base.new + server.config.cable = cable_config.with_indifferent_access + server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN } + + adapter_klass = Class.new(server.config.pubsub_adapter) do + def active? + !@listener.nil? + end + end + + adapter = adapter_klass.new(server) + + subscribe_as_queue("channel", adapter) do |queue| + adapter.broadcast("channel", "hello world") + assert_equal "hello world", queue.pop + end + + ActiveRecord::Base.clear_reloadable_connections! + + assert adapter.active? + end +end diff --git a/actioncable/test/subscription_adapter/redis_test.rb b/actioncable/test/subscription_adapter/redis_test.rb new file mode 100644 index 0000000000..ac2d8ef724 --- /dev/null +++ b/actioncable/test/subscription_adapter/redis_test.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "test_helper" +require_relative "common" +require_relative "channel_prefix" + +require "action_cable/subscription_adapter/redis" + +class RedisAdapterTest < ActionCable::TestCase + include CommonSubscriptionAdapterTest + include ChannelPrefixTest + + def cable_config + { adapter: "redis", driver: "ruby" } + end +end + +class RedisAdapterTest::Hiredis < RedisAdapterTest + def cable_config + super.merge(driver: "hiredis") + end +end + +class RedisAdapterTest::AlternateConfiguration < RedisAdapterTest + def cable_config + alt_cable_config = super.dup + alt_cable_config.delete(:url) + alt_cable_config.merge(host: "127.0.0.1", port: 6379, db: 12) + end +end + +class RedisAdapterTest::Connector < ActionCable::TestCase + test "slices url, host, port, db, password and id from config" do + config = { url: 1, host: 2, port: 3, db: 4, password: 5, id: "Some custom ID" } + + assert_called_with ::Redis, :new, [ config ] do + connect config.merge(other: "unrelated", stuff: "here") + end + end + + test "adds default id if it is not specified" do + config = { url: 1, host: 2, port: 3, db: 4, password: 5, id: "ActionCable-PID-#{$$}" } + + assert_called_with ::Redis, :new, [ config ] do + connect config.except(:id) + end + end + + def connect(config) + ActionCable::SubscriptionAdapter::Redis.redis_connector.call(config) + end +end diff --git a/actioncable/test/subscription_adapter/subscriber_map_test.rb b/actioncable/test/subscription_adapter/subscriber_map_test.rb new file mode 100644 index 0000000000..ed81099cbc --- /dev/null +++ b/actioncable/test/subscription_adapter/subscriber_map_test.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "test_helper" + +class SubscriberMapTest < ActionCable::TestCase + test "broadcast should not change subscribers" do + setup_subscription_map + origin = @subscription_map.instance_variable_get(:@subscribers).dup + + @subscription_map.broadcast("not_exist_channel", "") + + assert_equal origin, @subscription_map.instance_variable_get(:@subscribers) + end + + private + def setup_subscription_map + @subscription_map = ActionCable::SubscriptionAdapter::SubscriberMap.new + end +end diff --git a/actioncable/test/subscription_adapter/test_adapter_test.rb b/actioncable/test/subscription_adapter/test_adapter_test.rb new file mode 100644 index 0000000000..3fe07adb4a --- /dev/null +++ b/actioncable/test/subscription_adapter/test_adapter_test.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "test_helper" +require_relative "common" + +class ActionCable::SubscriptionAdapter::TestTest < ActionCable::TestCase + include CommonSubscriptionAdapterTest + + def setup + super + + @tx_adapter.shutdown + @tx_adapter = @rx_adapter + end + + def cable_config + { adapter: "test" } + end + + test "#broadcast stores messages for streams" do + @tx_adapter.broadcast("channel", "payload") + @tx_adapter.broadcast("channel2", "payload2") + + assert_equal ["payload"], @tx_adapter.broadcasts("channel") + assert_equal ["payload2"], @tx_adapter.broadcasts("channel2") + end + + test "#clear_messages deletes recorded broadcasts for the channel" do + @tx_adapter.broadcast("channel", "payload") + @tx_adapter.broadcast("channel2", "payload2") + + @tx_adapter.clear_messages("channel") + + assert_equal [], @tx_adapter.broadcasts("channel") + assert_equal ["payload2"], @tx_adapter.broadcasts("channel2") + end + + test "#clear deletes all recorded broadcasts" do + @tx_adapter.broadcast("channel", "payload") + @tx_adapter.broadcast("channel2", "payload2") + + @tx_adapter.clear + + assert_equal [], @tx_adapter.broadcasts("channel") + assert_equal [], @tx_adapter.broadcasts("channel2") + end +end |