Skip to content

palkan/wsdirector

Repository files navigation

Cult Of Martians Gem Version Build

WebSocket Director

Command line tool for testing WebSocket servers using scenarios.

Suitable for testing any websocket server implementation, like Action Cable, AnyCable, Phoenix Channels, GraphQL WS and so on.

đź“– Read also WebSocket Director: scenario-based integration tests for realtime apps

Installation

Install CLI:

gem install wsdirector-cli

Or use WebSockets Director as a library (see below for intructions):

# Gemfile
gem "wsdirector-core", "~> 1.0"

Usage

Create YAML file with simple testing script:

# script.yml
- receive: "Welcome" # expect to receive message
- send:
    data: "send message" # send message, all messages in data will be parse to json
- receive:
    data: "receive message" # expect to receive json message

and run it with this command:

wsdirector -f script.yml -u ws://websocket.server:9876/ws

#=> 1 clients, 0 failures

You can also use positional arguments:

wsdirector script.yml ws://websocket.server:9876/ws

You can create more complex scenarios with multiple client groups:

# script.yml
- client: # first clients group
    name: "publisher" # optional group name
    multiplier: ":scale" # :scale take number from -s param, and run :scale number of clients in this group
    actions:
      - receive:
          data: "Welcome"
      - wait_all # makes all clients in all groups wait untill every client get this point (global barrier)
      - send:
          data: "test message"
- client:
    name: "listeners"
    multiplier: ":scale * 2"
    actions:
      - receive:
          data: "Welcome"
      - wait_all
      - receive:
          multiplier: ":scale" # you can use multiplier with any action
          data: "test message"

Run with scale factor:

wsdirector -f script.yml -u ws://websocket.server:9876 -s 10

#=> Group publisher: 10 clients, 0 failures
#=> Group listeners: 20 clients, 0 failures

The simpliest scenario is just checking that socket is succesfully connected:

- client:
    name: connection check
    # no actions

Run with loop option:

  # script.yml
  - client:
      name: "listeners"
      loop:
        multiplier: ":scale" # :scale take number from -s param, and run :scale number of clients in this group
        actions:
          - receive:
              data:
                type: "welcome"
          - send:
              data:
                command: "subscribe"
                identifier: "{\"channel\":\"Channel\"}"
          - receive:
              data:
                identifier: "{\"channel\":\"Channel\"}"
                type: "confirm_subscription"
          - wait_all
          - receive:
              multiplier: ":scale + 1"

By default, receive action expects the exact data match. In some cases, it's useful to only match the specified keys (inclusion). For that, you can use data> field instead:

- client:
  actions:
    - receive:
        data:
          type: "welcome"
    - send:
        data:
          command: "subscribe"
          identifier: "{\"channel\":\"Channel\"}"
    - receive:
        data>:
          type: "confirm_subscription"

Also you can pass a JSON file with some testing scripts:

wsdirector -f scenario.json -u ws://websocket.server:9876

or pass a JSON scenario directly to the CLI without creating a file:

wsdirector -i '[{"receive": {"data":"welcome"}},{"send":{"data":"send message"}},{"receive":{"data":"receive message"}}]' -u ws://websocket.server:9876

Type wsdirector --help to check all commands.

Receive order

By default, the receive action scans through all available or newly added message to find a matching one. If you want to check the order of incoming messages, add the ordered: true option to the receive action.

Connection configuration

You can specify client's headers, cookies or query string params via the connection_options directive:

- client:
    connection_options:
      headers:
        "X-API-KEY": "secret"
      query:
        token: "123"
      cookies:
        session_id: "2022"

NOTE: Query string params could also be passed as a part of the URL. Specifying them in the scenario allows you to provide values via the interpolation.

Using as a library

You can integrate WS Director into your library or application by using its APIs:

# Could be a file path or JSON-encoded string as well
scenario = [
  {
    send: {
      data: "ping"
    }
  },
  {
    receive: {
      data: "pong"
    }
  }
]

result = WSDirector.run(scenario, url: "ws://my.ws.server:4949/live")
result.success? #=> true of false
result.groups #=> result data for each client group

If you're using YAML-based scenarios, you can also pass local variables to be used with ERB via the locals option:

- client:
    connection_options:
      headers:
        "X-API-TOKEN": <%= token %>
token = UserToken.generate
WSDirector.run(scenario, url: "ws://my.ws.server:4949/live", locals: {token:})

Protocols

WSDirector uses protocols to handle provide convinient actions for some popular protocols.

ActionCable Example

Channel code:

class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_test"
  end

  def echo(data)
    transmit data
  end

  def broadcast(data)
    ActionCable.server.broadcast "chat_test", data
  end
end

Scenario:

- client:
    multiplier: ":scale"
    name: "publisher"
    protocol: "action_cable"
    actions:
      - subscribe:
          channel: "ChatChannel"
      - wait_all
      - perform:
          channel: "ChatChannel"
          action: "broadcast"
          data:
            text: "hello"
- client:
    name: "listener"
    protocol: "action_cable"
    actions:
      - subscribe:
          channel: "ChatChannel"
      - wait_all
      - receive:
          channel: "ChatChannel"
          data:
            text: "hello"

Phoenix Channels

With "phoenix" protocol, you can use communicate with a Phoenix Channels server:

- client:
    protocol: phoenix
    multiplier: ":scale"
    actions:
      - join:
          topic: room:lobby
      - wait_all
      - send:
          topic: room:lobby
          event: new_msg
          data:
            body: "Hey from WS director!"
      - receive:
          topic: room:lobby
          multiplier: ":scale"
          event: new_msg
          data:
            body: "Hey from WS director!"

IMPORTANT: We support only v2 version of the Channels protocol.

Custom protocols

You can define your own protocol and load it dynamically:

# It's important to put a custom protocol class under WSDirector::Protocols
module WSDirector::Protocols
  class CustomProtocol < Base
    def send_ping_and_receive_pong
      send("data" => {"type" => "ping"})
      receive("data" => {"type" => "pong"})
    end
  end
end

Now you can load it via the -r option:

$ wsdirector -u localhost:3232/ws -i '["send_ping_and_receive_pong"]' -r ./path/to/custom_protocol.rb -vv

hh:mm:ss client=default_1 Connecting
hh:mm:ss client=default_1 Connected (45ms)
hh:mm:ss client=default_1 Sent message: {"type":"ping"}
hh:mm:ss client=default_1 Receive message: {"type":"pong"}
hh:mm:ss client=default_1 Received message: {"type":"pong"} (21ms)

Testing frameworks integration

WSDirector does not provide any specific helpers for RSpec or Minitest. Instead, we provide an example setup, which you could adjust to your needs (and which is too small to be a part of the library).

The example below implies running tests against an Action Cable server with a token-based authentication

module WSDirectorTestHelper
  def run_websocket_scenario(path, token:, url: ActionCable.server.config.url, **options)
    url = "#{url}?jid=#{token}"
    scenario = Rails.root.join "spec" / "fixtures" / "wsdirector" / path

    WSDirector.run(scenario, url:, **options)
  end
end

# In RSpec, you can include this modules via the configuration
RSpec.configure do |config|
  # Here we only add this helper to system tests
  config.include WSDirectorTestHelper, type: :system
end

Future Ideas

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/wsdirector.

License

The gem is available as open source under the terms of the MIT License.