anyt-core 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +38 -0
- data/bin/anyt +17 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/lib/anyt/cli.rb +211 -0
- data/lib/anyt/client.rb +146 -0
- data/lib/anyt/command.rb +83 -0
- data/lib/anyt/config.rb +41 -0
- data/lib/anyt/dummy/application.rb +86 -0
- data/lib/anyt/dummy/config.ru +16 -0
- data/lib/anyt/dummy/routes.rb +4 -0
- data/lib/anyt/dummy/tmp/development_secret.txt +1 -0
- data/lib/anyt/dummy/tmp/local_secret.txt +1 -0
- data/lib/anyt/ext/minitest.rb +151 -0
- data/lib/anyt/remote_control.rb +33 -0
- data/lib/anyt/rpc.rb +44 -0
- data/lib/anyt/tests/core/ping_test.rb +23 -0
- data/lib/anyt/tests/core/welcome_test.rb +10 -0
- data/lib/anyt/tests/features/channel_state_test.rb +81 -0
- data/lib/anyt/tests/features/remote_disconnect_test.rb +35 -0
- data/lib/anyt/tests/features/server_restart_test.rb +30 -0
- data/lib/anyt/tests/request/channel_test.rb +28 -0
- data/lib/anyt/tests/request/connection_test.rb +54 -0
- data/lib/anyt/tests/request/disconnect_reasons_test.rb +25 -0
- data/lib/anyt/tests/request/disconnection_test.rb +148 -0
- data/lib/anyt/tests/streams/broadcast_test.rb +65 -0
- data/lib/anyt/tests/streams/multiple_clients_test.rb +61 -0
- data/lib/anyt/tests/streams/multiple_test.rb +60 -0
- data/lib/anyt/tests/streams/single_test.rb +83 -0
- data/lib/anyt/tests/streams/stop_test.rb +57 -0
- data/lib/anyt/tests/subscriptions/ack_test.rb +39 -0
- data/lib/anyt/tests/subscriptions/params_test.rb +29 -0
- data/lib/anyt/tests/subscriptions/perform_test.rb +60 -0
- data/lib/anyt/tests/subscriptions/transmissions_test.rb +32 -0
- data/lib/anyt/tests.rb +62 -0
- data/lib/anyt/utils/async_helpers.rb +16 -0
- data/lib/anyt/utils.rb +3 -0
- data/lib/anyt/version.rb +5 -0
- data/lib/anyt.rb +14 -0
- metadata +167 -0
| @@ -0,0 +1,151 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            ENV["TERM"] = "#{ENV["TERM"]}color" unless ENV["TERM"]&.match?(/color/)
         | 
| 4 | 
            +
            require "minitest/spec"
         | 
| 5 | 
            +
            require "minitest/unit"
         | 
| 6 | 
            +
            require "minitest/reporters"
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            module Anyt
         | 
| 9 | 
            +
              # Common tests helpers
         | 
| 10 | 
            +
              module TestHelpers
         | 
| 11 | 
            +
                def self.included(base)
         | 
| 12 | 
            +
                  base.let(:client) { build_client(ignore: %w[ping welcome]) }
         | 
| 13 | 
            +
                  base.after { @clients&.each { |client| client.close(allow_messages: true) } }
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def build_client(**args)
         | 
| 17 | 
            +
                  @clients ||= []
         | 
| 18 | 
            +
                  Anyt::Client.new(**args).tap do |client|
         | 
| 19 | 
            +
                    @clients << client
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def restart_server!
         | 
| 24 | 
            +
                  if Anyt.config.use_action_cable
         | 
| 25 | 
            +
                    remote_client.restart_action_cable
         | 
| 26 | 
            +
                  else
         | 
| 27 | 
            +
                    Command.restart
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def remote_client
         | 
| 32 | 
            +
                  @remote_client ||= RemoteControl::Client.connect(Anyt.config.remote_control_port)
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                # Verifies that the actual message Hash is a subset of the expected one
         | 
| 36 | 
            +
                # (so we can ignore some irrelevant fields)
         | 
| 37 | 
            +
                def assert_message(expected, actual)
         | 
| 38 | 
            +
                  assert_equal expected, actual.slice(*expected.keys)
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def assert_includes_message(collection, expected)
         | 
| 42 | 
            +
                  found = collection.find do |el|
         | 
| 43 | 
            +
                    el.slice(*expected.keys) == expected
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  assert found, "Expecte #{collection} to include a message matching #{expected}"
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
            end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
            module Anyt
         | 
| 52 | 
            +
              # Namespace for test channels
         | 
| 53 | 
            +
              module TestChannels; end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
              # Custom #connect handlers management
         | 
| 56 | 
            +
              module ConnectHandlers
         | 
| 57 | 
            +
                class << self
         | 
| 58 | 
            +
                  def call(connection)
         | 
| 59 | 
            +
                    handlers_for(connection).each do |(_, handler)|
         | 
| 60 | 
            +
                      connection.reject_unauthorized_connection unless
         | 
| 61 | 
            +
                        connection.instance_eval(&handler)
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  def add(tag, &block)
         | 
| 66 | 
            +
                    handlers << [tag, block]
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  private
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  def handlers_for(connection)
         | 
| 72 | 
            +
                    handlers.select do |(tag, _)|
         | 
| 73 | 
            +
                      connection.params["test"] == tag
         | 
| 74 | 
            +
                    end
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  def handlers
         | 
| 78 | 
            +
                    @handlers ||= []
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
              end
         | 
| 82 | 
            +
            end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
            # Kernel extensions
         | 
| 85 | 
            +
            module Kernel
         | 
| 86 | 
            +
              ## Wraps `describe` and include shared helpers
         | 
| 87 | 
            +
              private def feature(*args, &block)
         | 
| 88 | 
            +
                cls = describe(*args, &block)
         | 
| 89 | 
            +
                cls.include Anyt::TestHelpers
         | 
| 90 | 
            +
                cls
         | 
| 91 | 
            +
              end
         | 
| 92 | 
            +
            end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
            # Extend Minitest Spec DSL with custom methodss
         | 
| 95 | 
            +
            module Minitest::Spec::DSL
         | 
| 96 | 
            +
              # Simplified version of `it` which doesn't care
         | 
| 97 | 
            +
              # about unique method names
         | 
| 98 | 
            +
              def scenario(desc, &block)
         | 
| 99 | 
            +
                block ||= proc { skip "(no tests defined)" }
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                define_method "test_ #{desc}", &block
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                desc
         | 
| 104 | 
            +
              end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
              # Generates Channel class dynamically and
         | 
| 107 | 
            +
              # add memoized helper to access its name
         | 
| 108 | 
            +
              def channel(id = nil, &block)
         | 
| 109 | 
            +
                class_name = @name.gsub(/\s+/, "_")
         | 
| 110 | 
            +
                class_name += "_#{id}" if id
         | 
| 111 | 
            +
                class_name += "_channel"
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                cls = Class.new(ApplicationCable::Channel, &block)
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                Anyt::TestChannels.const_set(class_name.classify, cls)
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                helper_name = id ? "#{id}_channel" : "channel"
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                let(helper_name) { cls.name }
         | 
| 120 | 
            +
              end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
              # Add new #connect handler
         | 
| 123 | 
            +
              def connect_handler(tag, &block)
         | 
| 124 | 
            +
                Anyt::ConnectHandlers.add(tag, &block)
         | 
| 125 | 
            +
              end
         | 
| 126 | 
            +
            end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
            module Anyt
         | 
| 129 | 
            +
              # Patch Minitest load_plugins to disable Rails plugin
         | 
| 130 | 
            +
              # See: https://github.com/kern/minitest-reporters/issues/230
         | 
| 131 | 
            +
              module MinitestPatch
         | 
| 132 | 
            +
                def load_plugins
         | 
| 133 | 
            +
                  super
         | 
| 134 | 
            +
                  extensions.delete("rails")
         | 
| 135 | 
            +
                end
         | 
| 136 | 
            +
              end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
              # Patch Spec reporter
         | 
| 139 | 
            +
              module ReporterPatch # :nodoc:
         | 
| 140 | 
            +
                def record_print_status(test)
         | 
| 141 | 
            +
                  test_name = test.name.gsub(/^test_/, "").strip
         | 
| 142 | 
            +
                  print(magenta { pad_test(test_name) })
         | 
| 143 | 
            +
                  print_colored_status(test)
         | 
| 144 | 
            +
                  print(" (%.2fs)" % test.time) unless test.time.nil?
         | 
| 145 | 
            +
                  puts
         | 
| 146 | 
            +
                end
         | 
| 147 | 
            +
              end
         | 
| 148 | 
            +
            end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
            Minitest::Reporters::SpecReporter.prepend Anyt::ReporterPatch
         | 
| 151 | 
            +
            Minitest.singleton_class.prepend Anyt::MinitestPatch
         | 
| @@ -0,0 +1,33 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "drb/drb"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Anyt
         | 
| 6 | 
            +
              # Invoke commands within the running Ruby (Action Cable) server
         | 
| 7 | 
            +
              module RemoteControl
         | 
| 8 | 
            +
                class Server
         | 
| 9 | 
            +
                  class << self
         | 
| 10 | 
            +
                    alias_method :start, :new
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def initialize(port)
         | 
| 14 | 
            +
                    DRb.start_service(
         | 
| 15 | 
            +
                      "druby://localhost:#{port}",
         | 
| 16 | 
            +
                      Client.new
         | 
| 17 | 
            +
                    )
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                class Client
         | 
| 22 | 
            +
                  def self.connect(port)
         | 
| 23 | 
            +
                    DRb.start_service
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    DRbObject.new_with_uri("druby://localhost:#{port}")
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def restart_action_cable
         | 
| 29 | 
            +
                    ActionCable.server.restart
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
            end
         | 
    
        data/lib/anyt/rpc.rb
    ADDED
    
    | @@ -0,0 +1,44 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "anyt/dummy/application"
         | 
| 4 | 
            +
            require "anycable-rails"
         | 
| 5 | 
            +
            require "redis"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Anyt # :nodoc:
         | 
| 8 | 
            +
              # Runs AnyCable RPC server in the background
         | 
| 9 | 
            +
              class RPC
         | 
| 10 | 
            +
                using AsyncHelpers
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                attr_accessor :running
         | 
| 13 | 
            +
                attr_reader :server
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def start
         | 
| 16 | 
            +
                  AnyCable.logger.debug "Starting RPC server ..."
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  AnyCable.server_callbacks.each(&:call)
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  @server = AnyCable::GRPC::Server.new(
         | 
| 21 | 
            +
                    host: AnyCable.config.rpc_host,
         | 
| 22 | 
            +
                    **AnyCable.config.to_grpc_params
         | 
| 23 | 
            +
                  )
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  if defined?(::AnyCable::Middlewares::EnvSid)
         | 
| 26 | 
            +
                    AnyCable.middleware.use(::AnyCable::Middlewares::EnvSid)
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  AnyCable.middleware.freeze
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  server.start
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  AnyCable.logger.debug "RPC server started"
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
                # rubocop: enable Metrics/AbcSize,Metrics/MethodLength
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def stop
         | 
| 38 | 
            +
                  server&.stop
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                AnyCable.connection_factory = ActionCable.server.config.connection_class.call
         | 
| 42 | 
            +
                AnyCable.config.log_level = :fatal unless AnyCable.config.debug
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
            end
         | 
| @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            feature "Ping" do
         | 
| 4 | 
            +
              scenario %(
         | 
| 5 | 
            +
                Client receives pings with timestamps
         | 
| 6 | 
            +
              ) do
         | 
| 7 | 
            +
                client = build_client(ignore: ["welcome"])
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                previous_stamp = 0
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                2.times do
         | 
| 12 | 
            +
                  ping = client.receive
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  current_stamp = ping["message"]
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  assert_equal "ping", ping["type"]
         | 
| 17 | 
            +
                  assert_kind_of Integer, current_stamp
         | 
| 18 | 
            +
                  refute_operator previous_stamp, :>=, current_stamp
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  previous_stamp = current_stamp
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
            end
         | 
| @@ -0,0 +1,81 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            feature "Channel state" do
         | 
| 4 | 
            +
              channel do
         | 
| 5 | 
            +
                state_attr_accessor :user, :count
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def subscribed
         | 
| 8 | 
            +
                  self.user = {name: params["name"]}
         | 
| 9 | 
            +
                  self.count = 1
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  stream_from "state_counts"
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def tick
         | 
| 15 | 
            +
                  self.count += 2
         | 
| 16 | 
            +
                  transmit({count: count, name: user[:name]})
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def unsubscribed
         | 
| 20 | 
            +
                  return unless params["notify_disconnect"]
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  ActionCable.server.broadcast("state_counts", {data: "user left: #{user[:name]}"})
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
              let(:identifier) { {channel: channel, name: "chipolino"}.to_json }
         | 
| 27 | 
            +
             | 
| 28 | 
            +
              let(:client2) { build_client(ignore: %w[ping welcome]) }
         | 
| 29 | 
            +
              let(:identifier2) { {channel: channel, name: "chipollone", notify_disconnect: true}.to_json }
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              before do
         | 
| 32 | 
            +
                subscribe_request = {command: "subscribe", identifier: identifier}
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                client.send(subscribe_request)
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                ack = {
         | 
| 37 | 
            +
                  "identifier" => identifier, "type" => "confirm_subscription"
         | 
| 38 | 
            +
                }
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                assert_message ack, client.receive
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
              scenario %(
         | 
| 44 | 
            +
                Channel state is kept between commands
         | 
| 45 | 
            +
              ) do
         | 
| 46 | 
            +
                perform_request = {
         | 
| 47 | 
            +
                  :command => "message",
         | 
| 48 | 
            +
                  :identifier => identifier,
         | 
| 49 | 
            +
                  "data" => {"action" => "tick"}.to_json
         | 
| 50 | 
            +
                }
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                client.send(perform_request)
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                msg = {"identifier" => identifier, "message" => {"count" => 3, "name" => "chipolino"}}
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                assert_message msg, client.receive
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
              scenario %(
         | 
| 60 | 
            +
                Channel state is available in #unsubscribe callbacks
         | 
| 61 | 
            +
              ) do
         | 
| 62 | 
            +
                subscribe_request = {command: "subscribe", identifier: identifier2}
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                client2.send(subscribe_request)
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                ack = {
         | 
| 67 | 
            +
                  "identifier" => identifier2, "type" => "confirm_subscription"
         | 
| 68 | 
            +
                }
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                assert_message ack, client2.receive
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                client2.close
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                msg = {
         | 
| 75 | 
            +
                  "identifier" => identifier,
         | 
| 76 | 
            +
                  "message" => {"data" => "user left: chipollone"}
         | 
| 77 | 
            +
                }
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                assert_message msg, client.receive
         | 
| 80 | 
            +
              end
         | 
| 81 | 
            +
            end
         | 
| @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            feature "Remote disconnect" do
         | 
| 4 | 
            +
              connect_handler("uid") do
         | 
| 5 | 
            +
                self.uid = request.params["uid"]
         | 
| 6 | 
            +
                uid.present?
         | 
| 7 | 
            +
              end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              scenario %(
         | 
| 10 | 
            +
               Close single connection by id
         | 
| 11 | 
            +
              ) do
         | 
| 12 | 
            +
                client = build_client(qs: "test=uid&uid=26", ignore: %w[ping])
         | 
| 13 | 
            +
                assert_message({"type" => "welcome"}, client.receive)
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                # Prevent race conditions when we send disconnect before internal channel subscription has been made
         | 
| 16 | 
            +
                # (only for Action Cable)
         | 
| 17 | 
            +
                sleep 1
         | 
| 18 | 
            +
                ActionCable.server.remote_connections.where(uid: "26").disconnect
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                # Waiting for https://github.com/rails/rails/pull/39544
         | 
| 21 | 
            +
                unless Anyt.config.use_action_cable
         | 
| 22 | 
            +
                  assert_message(
         | 
| 23 | 
            +
                    {
         | 
| 24 | 
            +
                      "type" => "disconnect",
         | 
| 25 | 
            +
                      "reconnect" => true,
         | 
| 26 | 
            +
                      "reason" => "remote"
         | 
| 27 | 
            +
                    },
         | 
| 28 | 
            +
                    client.receive
         | 
| 29 | 
            +
                  )
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                client.wait_for_close
         | 
| 33 | 
            +
                assert client.closed?
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
| @@ -0,0 +1,30 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            feature "Server restart" do
         | 
| 4 | 
            +
              connect_handler("reasons") do
         | 
| 5 | 
            +
                next false if request.params[:reason] == "unauthorized"
         | 
| 6 | 
            +
                true
         | 
| 7 | 
            +
              end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              scenario %(
         | 
| 10 | 
            +
                Client receives disconnect message
         | 
| 11 | 
            +
              ) do
         | 
| 12 | 
            +
                client = build_client(
         | 
| 13 | 
            +
                  qs: "test=reasons&reason=server_restart",
         | 
| 14 | 
            +
                  ignore: %(ping)
         | 
| 15 | 
            +
                )
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                assert_message({"type" => "welcome"}, client.receive)
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                restart_server!
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                assert_message(
         | 
| 22 | 
            +
                  {
         | 
| 23 | 
            +
                    "type" => "disconnect",
         | 
| 24 | 
            +
                    "reconnect" => true,
         | 
| 25 | 
            +
                    "reason" => "server_restart"
         | 
| 26 | 
            +
                  },
         | 
| 27 | 
            +
                  client.receive
         | 
| 28 | 
            +
                )
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
            end
         | 
| @@ -0,0 +1,28 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            feature "Request" do
         | 
| 4 | 
            +
              channel do
         | 
| 5 | 
            +
                delegate :params, to: :connection
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def subscribed
         | 
| 8 | 
            +
                  reject unless params[:token] == "secret"
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              scenario %(
         | 
| 13 | 
            +
                Channel has access to request
         | 
| 14 | 
            +
              ) do
         | 
| 15 | 
            +
                client = build_client(qs: "token=secret", ignore: %w[ping])
         | 
| 16 | 
            +
                assert_message({"type" => "welcome"}, client.receive)
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                subscribe_request = {command: "subscribe", identifier: {channel: channel}.to_json}
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                client.send(subscribe_request)
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                ack = {
         | 
| 23 | 
            +
                  "identifier" => {channel: channel}.to_json, "type" => "confirm_subscription"
         | 
| 24 | 
            +
                }
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                assert_message ack, client.receive
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
            end
         | 
| @@ -0,0 +1,54 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            feature "Request" do
         | 
| 4 | 
            +
              target_host = URI.parse(Anyt.config.target_url).host
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              connect_handler("request_url") do
         | 
| 7 | 
            +
                request.url =~ /test=request_url/ && request.host == target_host
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              scenario %(
         | 
| 11 | 
            +
                Url is set during connection
         | 
| 12 | 
            +
              ) do
         | 
| 13 | 
            +
                client = build_client(qs: "test=request_url")
         | 
| 14 | 
            +
                assert_message({"type" => "welcome"}, client.receive)
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              connect_handler("cookies") do
         | 
| 18 | 
            +
                cookies[:username] == "john green"
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              scenario %(
         | 
| 22 | 
            +
                Reject when required cookies are not set
         | 
| 23 | 
            +
              ) do
         | 
| 24 | 
            +
                client = build_client(qs: "test=cookies")
         | 
| 25 | 
            +
                client.wait_for_close
         | 
| 26 | 
            +
                assert client.closed?
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
              scenario %(
         | 
| 30 | 
            +
                Accepts when required cookies are set
         | 
| 31 | 
            +
              ) do
         | 
| 32 | 
            +
                client = build_client(qs: "test=cookies", cookies: "username=john green")
         | 
| 33 | 
            +
                assert_message({"type" => "welcome"}, client.receive)
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
              connect_handler("headers") do
         | 
| 37 | 
            +
                request.headers["X-API-TOKEN"] == "abc"
         | 
| 38 | 
            +
              end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
              scenario %(
         | 
| 41 | 
            +
                Reject when required header is missing
         | 
| 42 | 
            +
              ) do
         | 
| 43 | 
            +
                client = build_client(qs: "test=headers")
         | 
| 44 | 
            +
                client.wait_for_close
         | 
| 45 | 
            +
                assert client.closed?
         | 
| 46 | 
            +
              end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
              scenario %(
         | 
| 49 | 
            +
                Accepts when required header is set
         | 
| 50 | 
            +
              ) do
         | 
| 51 | 
            +
                client = build_client(qs: "test=headers", headers: {"x-api-token" => "abc"})
         | 
| 52 | 
            +
                assert_message({"type" => "welcome"}, client.receive)
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
            end
         | 
| @@ -0,0 +1,25 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            feature "Request" do
         | 
| 4 | 
            +
              connect_handler("reasons") do
         | 
| 5 | 
            +
                next false if request.params[:reason] == "unauthorized"
         | 
| 6 | 
            +
                true
         | 
| 7 | 
            +
              end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              scenario %(
         | 
| 10 | 
            +
                Receives disconnect message when rejected
         | 
| 11 | 
            +
              ) do
         | 
| 12 | 
            +
                client = build_client(qs: "test=reasons&reason=unauthorized")
         | 
| 13 | 
            +
                assert_message(
         | 
| 14 | 
            +
                  {
         | 
| 15 | 
            +
                    "type" => "disconnect",
         | 
| 16 | 
            +
                    "reconnect" => false,
         | 
| 17 | 
            +
                    "reason" => "unauthorized"
         | 
| 18 | 
            +
                  },
         | 
| 19 | 
            +
                  client.receive
         | 
| 20 | 
            +
                )
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                client.wait_for_close
         | 
| 23 | 
            +
                assert client.closed?
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
            end
         | 
| @@ -0,0 +1,148 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            feature "Request" do
         | 
| 4 | 
            +
              channel(:a) do
         | 
| 5 | 
            +
                def subscribed
         | 
| 6 | 
            +
                  stream_from "request_a"
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def unsubscribed
         | 
| 10 | 
            +
                  ActionCable.server.broadcast("request_a", {data: "user left"})
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              channel(:b) do
         | 
| 15 | 
            +
                def subscribed
         | 
| 16 | 
            +
                  stream_from "request_b"
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def unsubscribed
         | 
| 20 | 
            +
                  ActionCable.server.broadcast("request_b", {data: "user left"})
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              channel(:c) do
         | 
| 25 | 
            +
                def subscribed
         | 
| 26 | 
            +
                  stream_from "request_c"
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def unsubscribed
         | 
| 30 | 
            +
                  ActionCable.server.broadcast("request_c", {data: "user left#{params[:id].presence}"})
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
              let(:client2) { build_client(ignore: %w[ping welcome]) }
         | 
| 35 | 
            +
             | 
| 36 | 
            +
              scenario %(
         | 
| 37 | 
            +
                Client disconnect invokes #unsubscribe callbacks
         | 
| 38 | 
            +
                for different channels
         | 
| 39 | 
            +
              ) do
         | 
| 40 | 
            +
                subscribe_request = {command: "subscribe", identifier: {channel: a_channel}.to_json}
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                client.send(subscribe_request)
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                ack = {
         | 
| 45 | 
            +
                  "identifier" => {channel: a_channel}.to_json, "type" => "confirm_subscription"
         | 
| 46 | 
            +
                }
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                assert_message ack, client.receive
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                subscribe_request = {command: "subscribe", identifier: {channel: b_channel}.to_json}
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                client.send(subscribe_request)
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                ack = {
         | 
| 55 | 
            +
                  "identifier" => {channel: b_channel}.to_json, "type" => "confirm_subscription"
         | 
| 56 | 
            +
                }
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                assert_message ack, client.receive
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                subscribe_request = {command: "subscribe", identifier: {channel: a_channel}.to_json}
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                client2.send(subscribe_request)
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                ack = {
         | 
| 65 | 
            +
                  "identifier" => {channel: a_channel}.to_json, "type" => "confirm_subscription"
         | 
| 66 | 
            +
                }
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                assert_message ack, client2.receive
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                subscribe_request = {command: "subscribe", identifier: {channel: b_channel}.to_json}
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                client2.send(subscribe_request)
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                ack = {
         | 
| 75 | 
            +
                  "identifier" => {channel: b_channel}.to_json, "type" => "confirm_subscription"
         | 
| 76 | 
            +
                }
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                assert_message ack, client2.receive
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                client2.close
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                msg = {
         | 
| 83 | 
            +
                  "identifier" => {channel: a_channel}.to_json,
         | 
| 84 | 
            +
                  "message" => {"data" => "user left"}
         | 
| 85 | 
            +
                }
         | 
| 86 | 
            +
                msg2 = {
         | 
| 87 | 
            +
                  "identifier" => {channel: b_channel}.to_json,
         | 
| 88 | 
            +
                  "message" => {"data" => "user left"}
         | 
| 89 | 
            +
                }
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                msgs = [client.receive, client.receive]
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                assert_includes_message msgs, msg
         | 
| 94 | 
            +
                assert_includes_message msgs, msg2
         | 
| 95 | 
            +
              end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
              scenario %(
         | 
| 98 | 
            +
                Client disconnect invokes #unsubscribe callbacks
         | 
| 99 | 
            +
                for multiple subscriptions from the same channel
         | 
| 100 | 
            +
              ) do
         | 
| 101 | 
            +
                subscribe_request = {command: "subscribe", identifier: {channel: c_channel}.to_json}
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                client.send(subscribe_request)
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                ack = {
         | 
| 106 | 
            +
                  "identifier" => {channel: c_channel}.to_json, "type" => "confirm_subscription"
         | 
| 107 | 
            +
                }
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                assert_message ack, client.receive
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                subscribe_request = {command: "subscribe", identifier: {channel: c_channel, id: 1}.to_json}
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                client2.send(subscribe_request)
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                ack = {
         | 
| 116 | 
            +
                  "identifier" => {channel: c_channel, id: 1}.to_json, "type" => "confirm_subscription"
         | 
| 117 | 
            +
                }
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                assert_message ack, client2.receive
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                subscribe_request = {command: "subscribe", identifier: {channel: c_channel, id: 2}.to_json}
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                client2.send(subscribe_request)
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                ack = {
         | 
| 126 | 
            +
                  "identifier" => {channel: c_channel, id: 2}.to_json, "type" => "confirm_subscription"
         | 
| 127 | 
            +
                }
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                assert_message ack, client2.receive
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                client2.close
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                msg = {
         | 
| 134 | 
            +
                  "identifier" => {channel: c_channel}.to_json,
         | 
| 135 | 
            +
                  "message" => {"data" => "user left1"}
         | 
| 136 | 
            +
                }
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                msg2 = {
         | 
| 139 | 
            +
                  "identifier" => {channel: c_channel}.to_json,
         | 
| 140 | 
            +
                  "message" => {"data" => "user left2"}
         | 
| 141 | 
            +
                }
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                msgs = [client.receive, client.receive]
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                assert_includes_message msgs, msg
         | 
| 146 | 
            +
                assert_includes_message msgs, msg2
         | 
| 147 | 
            +
              end
         | 
| 148 | 
            +
            end
         |