diff --git a/lib/valkey/commands/connection_commands.rb b/lib/valkey/commands/connection_commands.rb index bef1ed5..bd6ac5b 100644 --- a/lib/valkey/commands/connection_commands.rb +++ b/lib/valkey/commands/connection_commands.rb @@ -44,13 +44,234 @@ def select(db) # # @return [String] `OK` def quit - # TODO: Implement a proper quit command - # synchronize do |client| - # client.call_v([:quit]) - # rescue ConnectionError - # ensure - # client.close - # end + send_command(RequestType::QUIT) + end + + # Switch to a different protocol version and handshake with the server. + # + # @param [Integer] protover Protocol version (2 or 3) + # @param [Hash] options Optional parameters like AUTH, SETNAME + # @return [Hash] Server information and capabilities + def hello(protover = 3, **options) + args = [protover] + + if options[:auth] + args << "AUTH" + args.concat(Array(options[:auth])) + end + + if options[:setname] + args << "SETNAME" << options[:setname] + end + + send_command(RequestType::HELLO, args) + end + + # Reset the connection state. + # + # @return [String] `RESET` + def reset + send_command(RequestType::RESET) + end + + # Get the current client's ID. + # + # @return [Integer] Unique client ID + def client_id + send_command(RequestType::CLIENT_ID) + end + + # Get the current client's name. + # + # @return [String, nil] Client name or nil if not set + def client_get_name + send_command(RequestType::CLIENT_GET_NAME) + end + + # Set the current client's name. + # + # @param [String] name New name for the client connection + # @return [String] `OK` + def client_set_name(name) + send_command(RequestType::CLIENT_SET_NAME, [name]) + end + + # Get a list of client connections. + # + # @param [String] type Optional filter by client type (normal, master, slave, pubsub) + # @param [Array] ids Optional filter by client IDs + # @return [String] List of clients as a formatted string + def client_list(type: nil, ids: nil) + args = [] + + if type + args << "TYPE" << type + end + + if ids + args << "ID" + args.concat(Array(ids)) + end + + send_command(RequestType::CLIENT_LIST, args) + end + + # Get information about the current client connection. + # + # @return [String] Client connection information + def client_info + send_command(RequestType::CLIENT_INFO) + end + + # Kill client connections. + # + # @param [String] addr Client address (ip:port) + # @param [Hash] options Optional filters (id, type, user, addr, laddr, skipme) + # @return [Integer] Number of clients killed + def client_kill(addr = nil, **options) + if addr && options.empty? + # Simple form: CLIENT KILL ip:port + send_command(RequestType::CLIENT_KILL_SIMPLE, [addr]) + else + # Extended form with filters + args = [] + + if addr + args << "ADDR" << addr + end + + options.each do |key, value| + case key + when :id + args << "ID" << value.to_s + when :type + args << "TYPE" << value.to_s + when :user + args << "USER" << value.to_s + when :addr + args << "ADDR" << value.to_s + when :laddr + args << "LADDR" << value.to_s + when :skipme + args << "SKIPME" << (value ? "yes" : "no") + end + end + + send_command(RequestType::CLIENT_KILL, args) + end + end + + # Pause client processing. + # + # @param [Integer] timeout Pause duration in milliseconds + # @param [String] mode Optional mode (WRITE, ALL) + # @return [String] `OK` + def client_pause(timeout, mode = nil) + args = [timeout] + args << mode if mode + send_command(RequestType::CLIENT_PAUSE, args) + end + + # Unpause client processing. + # + # @return [String] `OK` + def client_unpause + send_command(RequestType::CLIENT_UNPAUSE) + end + + # Configure client reply mode. + # + # @param [String] mode Reply mode (ON, OFF, SKIP) + # @return [String] `OK` + def client_reply(mode) + send_command(RequestType::CLIENT_REPLY, [mode]) + end + + # Unblock a client blocked in a blocking operation. + # + # @param [Integer] client_id ID of the client to unblock + # @param [String] unblock_type Optional unblock type (TIMEOUT, ERROR) + # @return [Integer] 1 if client was unblocked, 0 otherwise + def client_unblock(client_id, unblock_type = nil) + args = [client_id] + args << unblock_type if unblock_type + send_command(RequestType::CLIENT_UNBLOCK, args) + end + + # Set client connection information. + # + # @param [String] attr Attribute to set (lib-name, lib-ver) + # @param [String] value Value to set for the attribute + # @return [String] `OK` + def client_set_info(attr, value) + send_command(RequestType::CLIENT_SET_INFO, [attr, value]) + end + + # Enable/disable client caching. + # + # @param [String] mode Caching mode (YES, NO) + # @return [String] `OK` + def client_caching(mode) + send_command(RequestType::CLIENT_CACHING, [mode]) + end + + # Configure client tracking. + # + # @param [String] status Tracking status (ON, OFF) + # @param [Hash] options Optional parameters + # @return [String] `OK` + def client_tracking(status, **options) + args = [status] + + options.each do |key, value| + case key + when :redirect + args << "REDIRECT" << value.to_s + when :prefix + args << "PREFIX" + Array(value).each { |prefix| args << prefix } + when :bcast + args << "BCAST" if value + when :optin + args << "OPTIN" if value + when :optout + args << "OPTOUT" if value + when :noloop + args << "NOLOOP" if value + end + end + + send_command(RequestType::CLIENT_TRACKING, args) + end + + # Get client tracking information. + # + # @return [Array] Tracking information + def client_tracking_info + send_command(RequestType::CLIENT_TRACKING_INFO) + end + + # Get the client ID used for tracking redirection. + # + # @return [Integer] Client ID for tracking redirection + def client_getredir + send_command(RequestType::CLIENT_GET_REDIR) + end + + # Enable/disable client no-evict mode. + # + # @param [String] mode Mode (ON, OFF) + # @return [String] `OK` + def client_no_evict(mode) + send_command(RequestType::CLIENT_NO_EVICT, [mode.to_s.upcase]) + end + + # Enable/disable client no-touch mode. + # + # @param [String] mode Mode (ON, OFF) + # @return [String] `OK` + def client_no_touch(mode) + send_command(RequestType::CLIENT_NO_TOUCH, [mode.to_s.upcase]) end end end diff --git a/test/cluster/cluster_commands_test.rb b/test/cluster/cluster_commands_test.rb index da87c55..e613124 100644 --- a/test/cluster/cluster_commands_test.rb +++ b/test/cluster/cluster_commands_test.rb @@ -5,5 +5,6 @@ class TestClusterCommandsOnClusters < Minitest::Test include Helper::Cluster # include Lint::StringCommands # Run string tests first (while cluster is healthy) + include Lint::ConnectionCommands # Run connection tests (cluster-aware) include Lint::ClusterCommands # Run cluster commands second (after string tests) end diff --git a/test/lint/connection_commands.rb b/test/lint/connection_commands.rb new file mode 100644 index 0000000..f18fa99 --- /dev/null +++ b/test/lint/connection_commands.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +module Lint + module ConnectionCommands + def test_ping_without_message + assert_equal "PONG", r.ping + end + + def test_ping_with_message + message = "Hello World" + assert_equal message, r.ping(message) + end + + def test_echo + message = "Hello Valkey" + assert_equal message, r.echo(message) + end + + def test_auth_no_password + # Test auth when no password is set - should raise error + assert_raises(Valkey::CommandError) do + r.auth("some_password") + end + end + + def test_select_database + assert_equal "OK", r.select(0) + + # In cluster mode, only database 0 is supported + if cluster_mode? + assert_raises(Valkey::CommandError) do + r.select(1) + end + else + assert_equal "OK", r.select(1) + assert_equal "OK", r.select(0) # Switch back to default + end + end + + def test_client_id + # Use the server commands interface that's known to work + assert_kind_of Integer, r.client(:id) + end + + def test_client_set_get_name + name = "lint_test_client" + + # Use the server commands interface that's known to work + r.client(:set_name, name) + assert_equal name, r.client(:get_name) + + # Clear client name + r.client(:set_name, "") + assert_nil r.client(:get_name) + end + + def test_client_list + # Use the server commands interface that's known to work + list = r.client(:list) + assert_kind_of Array, list + assert list.all? { |client| client.is_a?(Hash) }, "Expected all clients to be represented as Hashes" + end + + def test_client_info + # Use the server commands interface that's known to work + info = r.client(:info) + assert_kind_of String, info + assert info.include?("id="), "Client info should contain client ID" + end + + def test_client_pause_unpause + # Use the server commands interface that's known to work + assert_equal "OK", r.client(:pause, 50) # 50ms pause + sleep(0.1) # Wait for pause to take effect + assert_equal "OK", r.client(:unpause) + end + + def test_client_reply + # Use the server commands interface that's known to work + assert_equal "OK", r.client(:reply, "ON") + end + + def test_client_set_info + # Use the server commands interface that's known to work + assert_equal "OK", r.client(:set_info, "lib-name", "valkey-ruby") + + assert_raises(Valkey::CommandError) do + r.client(:set_info, "invalid-attr", "value") + end + end + + def test_client_unblock + # Use the server commands interface that's known to work + client_id = r.client(:id) + result = r.client(:unblock, client_id) + assert [0, 1].include?(result), "Unblock should return 0 or 1" + end + + def test_client_no_evict + # Use the server commands interface that's known to work + assert_equal "OK", r.client_no_evict(:on) + assert_equal "OK", r.client_no_evict(:off) + + assert_raises(Valkey::CommandError) do + r.client_no_evict(:invalid) + end + end + + def test_client_no_touch + # Use the server commands interface that's known to work + assert_equal "OK", r.client_no_touch(:on) + assert_equal "OK", r.client_no_touch(:off) + + assert_raises(Valkey::CommandError) do + r.client_no_touch(:invalid) + end + end + + def test_client_getredir + skip("CLIENT GETREDIR command not implemented in backend yet") + + redir = r.client_getredir + assert_kind_of Integer, redir + end + + def test_hello_default + skip("HELLO command not implemented in backend yet") + + result = r.hello + assert_kind_of Hash, result + assert result.key?("server"), "HELLO response should contain server info" + end + + def test_hello_with_version + skip("HELLO command not implemented in backend yet") + + result = r.hello(3) + assert_kind_of Hash, result + assert_equal 3, result["proto"] + end + + def test_hello_with_setname + skip("HELLO command not implemented in backend yet") + + client_name = "hello_lint_test" + result = r.hello(3, setname: client_name) + assert_kind_of Hash, result + assert_equal client_name, r.client_get_name + end + + def test_reset + skip("RESET command not implemented in backend yet") + + # Set some state + r.client_set_name("before_reset") + + # In cluster mode, we can only use database 0 + unless cluster_mode? + r.select(1) + end + + # Reset + result = r.reset + assert_equal "RESET", result + + # State should be reset + assert_nil r.client_get_name + end + + def test_client_caching + skip("CLIENT CACHING command not implemented in backend yet") + + assert_equal "OK", r.client_caching("YES") + assert_equal "OK", r.client_caching("NO") + end + + def test_client_tracking + skip("CLIENT TRACKING command not implemented in backend yet") + + assert_equal "OK", r.client_tracking("ON") + assert_equal "OK", r.client_tracking("OFF") + end + + def test_client_tracking_info + skip("CLIENT TRACKING command not implemented in backend yet") + + info = r.client_tracking_info + assert_kind_of Array, info + end + + def test_quit + # Note: This test is tricky because QUIT closes the connection + # We'll skip it in lint tests to avoid connection issues + skip("QUIT command closes connection - tested separately") + end + end +end diff --git a/test/support/helper/client.rb b/test/support/helper/client.rb index 6086424..727bd67 100644 --- a/test/support/helper/client.rb +++ b/test/support/helper/client.rb @@ -20,6 +20,10 @@ def init(valkey) exit 1 end + def cluster_mode? + false + end + private def _new_client(options = {}) diff --git a/test/support/helper/cluster.rb b/test/support/helper/cluster.rb index edc0d8c..cf23e64 100644 --- a/test/support/helper/cluster.rb +++ b/test/support/helper/cluster.rb @@ -21,6 +21,10 @@ def version '7.0' end + def cluster_mode? + true + end + private def _new_client(options = {}) diff --git a/test/valkey/connection_commands_test.rb b/test/valkey/connection_commands_test.rb new file mode 100644 index 0000000..a9d4312 --- /dev/null +++ b/test/valkey/connection_commands_test.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require "test_helper" + +class TestConnectionCommands < Minitest::Test + include Helper::Client + include Lint::ConnectionCommands +end