diff options
Diffstat (limited to 'actioncable/test/channel')
-rw-r--r-- | actioncable/test/channel/base_test.rb | 274 | ||||
-rw-r--r-- | actioncable/test/channel/broadcasting_test.rb | 39 | ||||
-rw-r--r-- | actioncable/test/channel/naming_test.rb | 12 | ||||
-rw-r--r-- | actioncable/test/channel/periodic_timers_test.rb | 85 | ||||
-rw-r--r-- | actioncable/test/channel/rejection_test.rb | 56 | ||||
-rw-r--r-- | actioncable/test/channel/stream_test.rb | 230 | ||||
-rw-r--r-- | actioncable/test/channel/test_case_test.rb | 188 |
7 files changed, 884 insertions, 0 deletions
diff --git a/actioncable/test/channel/base_test.rb b/actioncable/test/channel/base_test.rb new file mode 100644 index 0000000000..eb0e1673b0 --- /dev/null +++ b/actioncable/test/channel/base_test.rb @@ -0,0 +1,274 @@ +# frozen_string_literal: true + +require "test_helper" +require "minitest/mock" +require "stubs/test_connection" +require "stubs/room" + +class ActionCable::Channel::BaseTest < ActionCable::TestCase + class ActionCable::Channel::Base + def kick + @last_action = [ :kick ] + end + + def topic + end + end + + class BasicChannel < ActionCable::Channel::Base + def chatters + @last_action = [ :chatters ] + end + end + + class ChatChannel < BasicChannel + attr_reader :room, :last_action + after_subscribe :toggle_subscribed + after_unsubscribe :toggle_subscribed + + def initialize(*) + @subscribed = false + super + end + + def subscribed + @room = Room.new params[:id] + @actions = [] + end + + def unsubscribed + @room = nil + end + + def toggle_subscribed + @subscribed = !@subscribed + end + + def leave + @last_action = [ :leave ] + end + + def speak(data) + @last_action = [ :speak, data ] + end + + def topic(data) + @last_action = [ :topic, data ] + end + + def subscribed? + @subscribed + end + + def get_latest + transmit data: "latest" + end + + def receive + @last_action = [ :receive ] + end + + private + def rm_rf + @last_action = [ :rm_rf ] + end + end + + setup do + @user = User.new "lifo" + @connection = TestConnection.new(@user) + @channel = ChatChannel.new @connection, "{id: 1}", id: 1 + end + + test "should subscribe to a channel" do + @channel.subscribe_to_channel + assert_equal 1, @channel.room.id + end + + test "on subscribe callbacks" do + @channel.subscribe_to_channel + assert @channel.subscribed + end + + test "channel params" do + assert_equal({ id: 1 }, @channel.params) + end + + test "unsubscribing from a channel" do + @channel.subscribe_to_channel + + assert @channel.room + assert_predicate @channel, :subscribed? + + @channel.unsubscribe_from_channel + + assert_not @channel.room + assert_not_predicate @channel, :subscribed? + end + + test "connection identifiers" do + assert_equal @user.name, @channel.current_user.name + end + + test "callable action without any argument" do + @channel.perform_action "action" => :leave + assert_equal [ :leave ], @channel.last_action + end + + test "callable action with arguments" do + data = { "action" => :speak, "content" => "Hello World" } + + @channel.perform_action data + assert_equal [ :speak, data ], @channel.last_action + end + + test "should not dispatch a private method" do + @channel.perform_action "action" => :rm_rf + assert_nil @channel.last_action + end + + test "should not dispatch a public method defined on Base" do + @channel.perform_action "action" => :kick + assert_nil @channel.last_action + end + + test "should dispatch a public method defined on Base and redefined on channel" do + data = { "action" => :topic, "content" => "This is Sparta!" } + + @channel.perform_action data + assert_equal [ :topic, data ], @channel.last_action + end + + test "should dispatch calling a public method defined in an ancestor" do + @channel.perform_action "action" => :chatters + assert_equal [ :chatters ], @channel.last_action + end + + test "should dispatch receive action when perform_action is called with empty action" do + data = { "content" => "hello" } + @channel.perform_action data + assert_equal [ :receive ], @channel.last_action + end + + test "transmitting data" do + @channel.perform_action "action" => :get_latest + + expected = { "identifier" => "{id: 1}", "message" => { "data" => "latest" } } + assert_equal expected, @connection.last_transmission + end + + test "do not send subscription confirmation on initialize" do + assert_nil @connection.last_transmission + end + + test "subscription confirmation on subscribe_to_channel" do + expected = { "identifier" => "{id: 1}", "type" => "confirm_subscription" } + @channel.subscribe_to_channel + assert_equal expected, @connection.last_transmission + end + + test "actions available on Channel" do + available_actions = %w(room last_action subscribed unsubscribed toggle_subscribed leave speak subscribed? get_latest receive chatters topic).to_set + assert_equal available_actions, ChatChannel.action_methods + end + + test "invalid action on Channel" do + assert_logged("Unable to process ActionCable::Channel::BaseTest::ChatChannel#invalid_action") do + @channel.perform_action "action" => :invalid_action + end + end + + test "notification for perform_action" do + begin + events = [] + ActiveSupport::Notifications.subscribe "perform_action.action_cable" do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + + data = { "action" => :speak, "content" => "hello" } + @channel.perform_action data + + assert_equal 1, events.length + assert_equal "perform_action.action_cable", events[0].name + assert_equal "ActionCable::Channel::BaseTest::ChatChannel", events[0].payload[:channel_class] + assert_equal :speak, events[0].payload[:action] + assert_equal data, events[0].payload[:data] + ensure + ActiveSupport::Notifications.unsubscribe "perform_action.action_cable" + end + end + + test "notification for transmit" do + begin + events = [] + ActiveSupport::Notifications.subscribe "transmit.action_cable" do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + + @channel.perform_action "action" => :get_latest + expected_data = { data: "latest" } + + assert_equal 1, events.length + assert_equal "transmit.action_cable", events[0].name + assert_equal "ActionCable::Channel::BaseTest::ChatChannel", events[0].payload[:channel_class] + assert_equal expected_data, events[0].payload[:data] + assert_nil events[0].payload[:via] + ensure + ActiveSupport::Notifications.unsubscribe "transmit.action_cable" + end + end + + test "notification for transmit_subscription_confirmation" do + begin + @channel.subscribe_to_channel + + events = [] + ActiveSupport::Notifications.subscribe "transmit_subscription_confirmation.action_cable" do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + + @channel.stub(:subscription_confirmation_sent?, false) do + @channel.send(:transmit_subscription_confirmation) + + assert_equal 1, events.length + assert_equal "transmit_subscription_confirmation.action_cable", events[0].name + assert_equal "ActionCable::Channel::BaseTest::ChatChannel", events[0].payload[:channel_class] + end + ensure + ActiveSupport::Notifications.unsubscribe "transmit_subscription_confirmation.action_cable" + end + end + + test "notification for transmit_subscription_rejection" do + begin + events = [] + ActiveSupport::Notifications.subscribe "transmit_subscription_rejection.action_cable" do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + + @channel.send(:transmit_subscription_rejection) + + assert_equal 1, events.length + assert_equal "transmit_subscription_rejection.action_cable", events[0].name + assert_equal "ActionCable::Channel::BaseTest::ChatChannel", events[0].payload[:channel_class] + ensure + ActiveSupport::Notifications.unsubscribe "transmit_subscription_rejection.action_cable" + end + end + + private + def assert_logged(message) + old_logger = @connection.logger + log = StringIO.new + @connection.instance_variable_set(:@logger, Logger.new(log)) + + begin + yield + + log.rewind + assert_match message, log.read + ensure + @connection.instance_variable_set(:@logger, old_logger) + end + end +end diff --git a/actioncable/test/channel/broadcasting_test.rb b/actioncable/test/channel/broadcasting_test.rb new file mode 100644 index 0000000000..2cbfabc1d0 --- /dev/null +++ b/actioncable/test/channel/broadcasting_test.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "test_helper" +require "stubs/test_connection" +require "stubs/room" + +class ActionCable::Channel::BroadcastingTest < ActionCable::TestCase + class ChatChannel < ActionCable::Channel::Base + end + + setup do + @connection = TestConnection.new + end + + test "broadcasts_to" do + assert_called_with( + ActionCable.server, + :broadcast, + [ + "action_cable:channel:broadcasting_test:chat:Room#1-Campfire", + "Hello World" + ] + ) do + ChatChannel.broadcast_to(Room.new(1), "Hello World") + end + end + + test "broadcasting_for with an object" do + assert_equal "Room#1-Campfire", ChatChannel.broadcasting_for(Room.new(1)) + end + + test "broadcasting_for with an array" do + assert_equal "Room#1-Campfire:Room#2-Campfire", ChatChannel.broadcasting_for([ Room.new(1), Room.new(2) ]) + end + + test "broadcasting_for with a string" do + assert_equal "hello", ChatChannel.broadcasting_for("hello") + end +end diff --git a/actioncable/test/channel/naming_test.rb b/actioncable/test/channel/naming_test.rb new file mode 100644 index 0000000000..45652d9cc9 --- /dev/null +++ b/actioncable/test/channel/naming_test.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "test_helper" + +class ActionCable::Channel::NamingTest < ActionCable::TestCase + class ChatChannel < ActionCable::Channel::Base + end + + test "channel_name" do + assert_equal "action_cable:channel:naming_test:chat", ChatChannel.channel_name + end +end diff --git a/actioncable/test/channel/periodic_timers_test.rb b/actioncable/test/channel/periodic_timers_test.rb new file mode 100644 index 0000000000..0c979f4c7c --- /dev/null +++ b/actioncable/test/channel/periodic_timers_test.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require "test_helper" +require "stubs/test_connection" +require "stubs/room" +require "active_support/time" + +class ActionCable::Channel::PeriodicTimersTest < ActionCable::TestCase + class ChatChannel < ActionCable::Channel::Base + # Method name arg + periodically :send_updates, every: 1 + + # Proc arg + periodically -> { ping }, every: 2 + + # Block arg + periodically every: 3 do + ping + end + + private + def ping + end + end + + setup do + @connection = TestConnection.new + end + + test "periodic timers definition" do + timers = ChatChannel.periodic_timers + + assert_equal 3, timers.size + + timers.each_with_index do |timer, i| + assert_kind_of Proc, timer[0] + assert_equal i + 1, timer[1][:every] + end + end + + test "disallow negative and zero periods" do + [ 0, 0.0, 0.seconds, -1, -1.seconds, "foo", :foo, Object.new ].each do |invalid| + e = assert_raise ArgumentError do + ChatChannel.periodically :send_updates, every: invalid + end + assert_match(/Expected every:/, e.message) + end + end + + test "disallow block and arg together" do + e = assert_raise ArgumentError do + ChatChannel.periodically(:send_updates, every: 1) { ping } + end + assert_match(/not both/, e.message) + end + + test "disallow unknown args" do + [ "send_updates", Object.new, nil ].each do |invalid| + e = assert_raise ArgumentError do + ChatChannel.periodically invalid, every: 1 + end + assert_match(/Expected a Symbol/, e.message) + end + end + + test "timer start and stop" do + mock = Minitest::Mock.new + 3.times { mock.expect(:shutdown, nil) } + + assert_called( + @connection.server.event_loop, + :timer, + times: 3, + returns: mock + ) do + channel = ChatChannel.new @connection, "{id: 1}", id: 1 + + channel.subscribe_to_channel + channel.unsubscribe_from_channel + assert_equal [], channel.send(:active_periodic_timers) + end + + assert mock.verify + end +end diff --git a/actioncable/test/channel/rejection_test.rb b/actioncable/test/channel/rejection_test.rb new file mode 100644 index 0000000000..683eafcac0 --- /dev/null +++ b/actioncable/test/channel/rejection_test.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require "test_helper" +require "minitest/mock" +require "stubs/test_connection" +require "stubs/room" + +class ActionCable::Channel::RejectionTest < ActionCable::TestCase + class SecretChannel < ActionCable::Channel::Base + def subscribed + reject if params[:id] > 0 + end + + def secret_action + end + end + + setup do + @user = User.new "lifo" + @connection = TestConnection.new(@user) + end + + test "subscription rejection" do + subscriptions = Minitest::Mock.new + subscriptions.expect(:remove_subscription, SecretChannel, [SecretChannel]) + + @connection.stub(:subscriptions, subscriptions) do + @channel = SecretChannel.new @connection, "{id: 1}", id: 1 + @channel.subscribe_to_channel + + expected = { "identifier" => "{id: 1}", "type" => "reject_subscription" } + assert_equal expected, @connection.last_transmission + end + + assert subscriptions.verify + end + + test "does not execute action if subscription is rejected" do + subscriptions = Minitest::Mock.new + subscriptions.expect(:remove_subscription, SecretChannel, [SecretChannel]) + + @connection.stub(:subscriptions, subscriptions) do + @channel = SecretChannel.new @connection, "{id: 1}", id: 1 + @channel.subscribe_to_channel + + expected = { "identifier" => "{id: 1}", "type" => "reject_subscription" } + assert_equal expected, @connection.last_transmission + assert_equal 1, @connection.transmissions.size + + @channel.perform_action("action" => :secret_action) + assert_equal 1, @connection.transmissions.size + end + + assert subscriptions.verify + end +end diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb new file mode 100644 index 0000000000..9ad2213d47 --- /dev/null +++ b/actioncable/test/channel/stream_test.rb @@ -0,0 +1,230 @@ +# frozen_string_literal: true + +require "test_helper" +require "minitest/mock" +require "stubs/test_connection" +require "stubs/room" + +module ActionCable::StreamTests + class Connection < ActionCable::Connection::Base + attr_reader :websocket + + def send_async(method, *args) + send method, *args + end + end + + class ChatChannel < ActionCable::Channel::Base + def subscribed + if params[:id] + @room = Room.new params[:id] + stream_from "test_room_#{@room.id}", coder: pick_coder(params[:coder]) + end + end + + def send_confirmation + transmit_subscription_confirmation + end + + private + def pick_coder(coder) + case coder + when nil, "json" + ActiveSupport::JSON + when "custom" + DummyEncoder + when "none" + nil + end + end + end + + module DummyEncoder + extend self + def encode(*) '{ "foo": "encoded" }' end + def decode(*) { foo: "decoded" } end + end + + class SymbolChannel < ActionCable::Channel::Base + def subscribed + stream_from :channel + end + end + + class StreamTest < ActionCable::TestCase + test "streaming start and stop" do + run_in_eventmachine do + connection = TestConnection.new + pubsub = Minitest::Mock.new connection.pubsub + + pubsub.expect(:subscribe, nil, ["test_room_1", Proc, Proc]) + pubsub.expect(:unsubscribe, nil, ["test_room_1", Proc]) + + connection.stub(:pubsub, pubsub) do + channel = ChatChannel.new connection, "{id: 1}", id: 1 + channel.subscribe_to_channel + + wait_for_async + channel.unsubscribe_from_channel + end + + assert pubsub.verify + end + end + + test "stream from non-string channel" do + run_in_eventmachine do + connection = TestConnection.new + pubsub = Minitest::Mock.new connection.pubsub + + pubsub.expect(:subscribe, nil, ["channel", Proc, Proc]) + pubsub.expect(:unsubscribe, nil, ["channel", Proc]) + + connection.stub(:pubsub, pubsub) do + channel = SymbolChannel.new connection, "" + channel.subscribe_to_channel + + wait_for_async + + channel.unsubscribe_from_channel + end + + assert pubsub.verify + end + end + + test "stream_for" do + run_in_eventmachine do + connection = TestConnection.new + + channel = ChatChannel.new connection, "" + channel.subscribe_to_channel + channel.stream_for Room.new(1) + wait_for_async + + pubsub_call = channel.pubsub.class.class_variable_get "@@subscribe_called" + + assert_equal "action_cable:stream_tests:chat:Room#1-Campfire", pubsub_call[:channel] + assert_instance_of Proc, pubsub_call[:callback] + assert_instance_of Proc, pubsub_call[:success_callback] + end + end + + test "stream_from subscription confirmation" do + run_in_eventmachine do + connection = TestConnection.new + + channel = ChatChannel.new connection, "{id: 1}", id: 1 + channel.subscribe_to_channel + + assert_nil connection.last_transmission + + wait_for_async + + confirmation = { "identifier" => "{id: 1}", "type" => "confirm_subscription" } + connection.transmit(confirmation) + + assert_equal confirmation, connection.last_transmission, "Did not receive subscription confirmation within 0.1s" + end + end + + test "subscription confirmation should only be sent out once" do + run_in_eventmachine do + connection = TestConnection.new + + channel = ChatChannel.new connection, "test_channel" + channel.send_confirmation + channel.send_confirmation + + wait_for_async + + expected = { "identifier" => "test_channel", "type" => "confirm_subscription" } + assert_equal expected, connection.last_transmission, "Did not receive subscription confirmation" + + assert_equal 1, connection.transmissions.size + end + end + end + + require "action_cable/subscription_adapter/async" + + class UserCallbackChannel < ActionCable::Channel::Base + def subscribed + stream_from :channel do + Thread.current[:ran_callback] = true + end + end + end + + class MultiChatChannel < ActionCable::Channel::Base + def subscribed + stream_from "main_room" + stream_from "test_all_rooms" + end + end + + class StreamFromTest < ActionCable::TestCase + setup do + @server = TestServer.new(subscription_adapter: ActionCable::SubscriptionAdapter::Async) + @server.config.allowed_request_origins = %w( http://rubyonrails.com ) + end + + test "custom encoder" do + run_in_eventmachine do + connection = open_connection + subscribe_to connection, identifiers: { id: 1 } + + assert_called(connection.websocket, :transmit) do + @server.broadcast "test_room_1", { foo: "bar" }, { coder: DummyEncoder } + wait_for_async + wait_for_executor connection.server.worker_pool.executor + end + end + end + + test "user supplied callbacks are run through the worker pool" do + run_in_eventmachine do + connection = open_connection + receive(connection, command: "subscribe", channel: UserCallbackChannel.name, identifiers: { id: 1 }) + + @server.broadcast "channel", {} + wait_for_async + assert_not Thread.current[:ran_callback], "User callback was not run through the worker pool" + end + end + + test "subscription confirmation should only be sent out once with multiple stream_from" do + run_in_eventmachine do + connection = open_connection + expected = { "identifier" => { "channel" => MultiChatChannel.name }.to_json, "type" => "confirm_subscription" } + assert_called_with(connection.websocket, :transmit, [expected.to_json]) do + receive(connection, command: "subscribe", channel: MultiChatChannel.name, identifiers: {}) + wait_for_async + end + end + end + + private + def subscribe_to(connection, identifiers:) + receive connection, command: "subscribe", identifiers: identifiers + end + + def open_connection + env = Rack::MockRequest.env_for "/test", "HTTP_HOST" => "localhost", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket", "HTTP_ORIGIN" => "http://rubyonrails.com" + + Connection.new(@server, env).tap do |connection| + connection.process + assert_predicate connection.websocket, :possible? + + wait_for_async + assert_predicate connection.websocket, :alive? + end + end + + def receive(connection, command:, identifiers:, channel: "ActionCable::StreamTests::ChatChannel") + identifier = JSON.generate(identifiers.merge(channel: channel)) + connection.dispatch_websocket_message JSON.generate(command: command, identifier: identifier) + wait_for_async + end + end +end diff --git a/actioncable/test/channel/test_case_test.rb b/actioncable/test/channel/test_case_test.rb new file mode 100644 index 0000000000..63d0d6207e --- /dev/null +++ b/actioncable/test/channel/test_case_test.rb @@ -0,0 +1,188 @@ +# frozen_string_literal: true + +require "test_helper" + +class TestTestChannel < ActionCable::Channel::Base +end + +class NonInferrableExplicitClassChannelTest < ActionCable::Channel::TestCase + tests TestTestChannel + + def test_set_channel_class_manual + assert_equal TestTestChannel, self.class.channel_class + end +end + +class NonInferrableSymbolNameChannelTest < ActionCable::Channel::TestCase + tests :test_test_channel + + def test_set_channel_class_manual_using_symbol + assert_equal TestTestChannel, self.class.channel_class + end +end + +class NonInferrableStringNameChannelTest < ActionCable::Channel::TestCase + tests "test_test_channel" + + def test_set_channel_class_manual_using_string + assert_equal TestTestChannel, self.class.channel_class + end +end + +class SubscriptionsTestChannel < ActionCable::Channel::Base +end + +class SubscriptionsTestChannelTest < ActionCable::Channel::TestCase + def setup + stub_connection + end + + def test_no_subscribe + assert_nil subscription + end + + def test_subscribe + subscribe + + assert subscription.confirmed? + assert_not subscription.rejected? + assert_equal 1, connection.transmissions.size + assert_equal ActionCable::INTERNAL[:message_types][:confirmation], + connection.transmissions.last["type"] + end +end + +class StubConnectionTest < ActionCable::Channel::TestCase + tests SubscriptionsTestChannel + + def test_connection_identifiers + stub_connection username: "John", admin: true + + subscribe + + assert_equal "John", subscription.username + assert subscription.admin + end +end + +class RejectionTestChannel < ActionCable::Channel::Base + def subscribed + reject + end +end + +class RejectionTestChannelTest < ActionCable::Channel::TestCase + def test_rejection + subscribe + + assert_not subscription.confirmed? + assert subscription.rejected? + assert_equal 1, connection.transmissions.size + assert_equal ActionCable::INTERNAL[:message_types][:rejection], + connection.transmissions.last["type"] + end +end + +class StreamsTestChannel < ActionCable::Channel::Base + def subscribed + stream_from "test_#{params[:id] || 0}" + end +end + +class StreamsTestChannelTest < ActionCable::Channel::TestCase + def test_stream_without_params + subscribe + + assert_equal "test_0", streams.last + end + + def test_stream_with_params + subscribe id: 42 + + assert_equal "test_42", streams.last + end +end + +class PerformTestChannel < ActionCable::Channel::Base + def echo(data) + data.delete("action") + transmit data + end + + def ping + transmit type: "pong" + end +end + +class PerformTestChannelTest < ActionCable::Channel::TestCase + def setup + stub_connection user_id: 2016 + subscribe id: 5 + end + + def test_perform_with_params + perform :echo, text: "You are man!" + + assert_equal({ "text" => "You are man!" }, transmissions.last) + end + + def test_perform_and_transmit + perform :ping + + assert_equal "pong", transmissions.last["type"] + end +end + +class PerformUnsubscribedTestChannelTest < ActionCable::Channel::TestCase + tests PerformTestChannel + + def test_perform_when_unsubscribed + assert_raises do + perform :echo + end + end +end + +class BroadcastsTestChannel < ActionCable::Channel::Base + def broadcast(data) + ActionCable.server.broadcast( + "broadcast_#{params[:id]}", + text: data["message"], user_id: user_id + ) + end + + def broadcast_to_user(data) + user = User.new user_id + + self.class.broadcast_to user, text: data["message"] + end +end + +class BroadcastsTestChannelTest < ActionCable::Channel::TestCase + def setup + stub_connection user_id: 2017 + subscribe id: 5 + end + + def test_broadcast_matchers_included + assert_broadcast_on("broadcast_5", user_id: 2017, text: "SOS") do + perform :broadcast, message: "SOS" + end + end + + def test_broadcast_to_object + user = User.new(2017) + + assert_broadcasts(user, 1) do + perform :broadcast_to_user, text: "SOS" + end + end + + def test_broadcast_to_object_with_data + user = User.new(2017) + + assert_broadcast_on(user, text: "SOS") do + perform :broadcast_to_user, message: "SOS" + end + end +end |