summaryrefslogtreecommitdiffstats
path: root/tests/unittests/test_client.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 03:01:24 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 03:01:24 +0000
commitbbbba88cf6eb914c53ace70a32ed8874d5e600c2 (patch)
tree97210524cb84512c477421e1950f2c6f7e1a3089 /tests/unittests/test_client.py
parentInitial commit. (diff)
downloadiredis-bbbba88cf6eb914c53ace70a32ed8874d5e600c2.tar.xz
iredis-bbbba88cf6eb914c53ace70a32ed8874d5e600c2.zip
Adding upstream version 1.14.1.upstream/1.14.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tests/unittests/test_client.py')
-rw-r--r--tests/unittests/test_client.py615
1 files changed, 615 insertions, 0 deletions
diff --git a/tests/unittests/test_client.py b/tests/unittests/test_client.py
new file mode 100644
index 0000000..c0f4c07
--- /dev/null
+++ b/tests/unittests/test_client.py
@@ -0,0 +1,615 @@
+import os
+import re
+from textwrap import dedent
+from unittest.mock import MagicMock, patch
+
+from packaging.version import parse as version_parse
+from prompt_toolkit.formatted_text import FormattedText
+import pytest
+import redis
+
+from iredis.client import Client
+from iredis.commands import command2syntax
+from iredis.completers import IRedisCompleter
+from iredis.config import config, load_config_files
+from iredis.entry import Rainbow, prompt_message
+from iredis.exceptions import NotSupport
+
+from ..helpers import formatted_text_rematch
+
+
+@pytest.fixture
+def completer():
+ return IRedisCompleter()
+
+
+zset_type = "ziplist"
+hash_type = "hashtable"
+list_type = "quicklist"
+if version_parse(os.environ["REDIS_VERSION"]) >= version_parse("7"):
+ zset_type = "listpack"
+ hash_type = "listpack"
+ list_type = "listpack"
+
+
+@pytest.mark.parametrize(
+ "_input, command_name, expect_args",
+ [
+ ("keys *", "keys", ["*"]),
+ ("DEL abc foo bar", "DEL", ["abc", "foo", "bar"]),
+ ("cluster info", "cluster info", []),
+ ("CLUSTER failover FORCE", "CLUSTER failover", ["FORCE"]),
+ ],
+)
+def test_send_command(_input, command_name, expect_args):
+ client = Client("127.0.0.1", 6379, None)
+ client.execute = MagicMock()
+ next(client.send_command(_input, None))
+ args, _ = client.execute.call_args
+ assert args == (command_name, *expect_args)
+
+
+def test_client_not_support_hello_command(iredis_client):
+ with pytest.raises(NotSupport):
+ iredis_client.pre_hook("hello 3", "hello", "3", None)
+
+
+def test_patch_completer():
+ client = Client("127.0.0.1", "6379", None)
+ completer = IRedisCompleter()
+ client.pre_hook(
+ "MGET foo bar hello world", "MGET", "foo bar hello world", completer
+ )
+ assert completer.key_completer.words == ["world", "hello", "bar", "foo"]
+ assert completer.key_completer.words == ["world", "hello", "bar", "foo"]
+
+ client.pre_hook("GET bar", "GET", "bar", completer)
+ assert completer.key_completer.words == ["bar", "world", "hello", "foo"]
+
+
+def test_get_server_verison_after_client(config):
+ Client("127.0.0.1", "6379", None)
+ assert re.match(r"\d+\..*", config.version)
+
+ config.version = "Unknown"
+ config.no_info = True
+ Client("127.0.0.1", "6379", None)
+ assert config.version == "Unknown"
+
+
+def test_do_help(config):
+ client = Client("127.0.0.1", "6379", None)
+ config.version = "5.0.0"
+ resp = client.do_help("SET")
+ assert resp[10] == ("", "1.0.0 (Available on your redis-server: 5.0.0)")
+ config.version = "2.0.0"
+ resp = client.do_help("cluster", "addslots")
+ assert resp[10] == ("", "3.0.0 (Not available on your redis-server: 2.0.0)")
+
+
+def test_rainbow_iterator():
+ "test color infinite iterator"
+ original_color = Rainbow.color
+ Rainbow.color = list(range(0, 3))
+ assert list(zip(range(10), Rainbow())) == [
+ (0, 0),
+ (1, 1),
+ (2, 2),
+ (3, 1),
+ (4, 0),
+ (5, 1),
+ (6, 2),
+ (7, 1),
+ (8, 0),
+ (9, 1),
+ ]
+ Rainbow.color = original_color
+
+
+def test_prompt_message(iredis_client, config):
+ config.rainbow = False
+ assert prompt_message(iredis_client) == "127.0.0.1:6379[15]> "
+
+ config.rainbow = True
+ assert prompt_message(iredis_client)[:3] == [
+ ("#cc2244", "1"),
+ ("#bb4444", "2"),
+ ("#996644", "7"),
+ ]
+
+
+def test_on_connection_error_retry(iredis_client, config):
+ config.retry_times = 1
+ mock_connection = MagicMock()
+ mock_connection.read_response.side_effect = [
+ redis.exceptions.ConnectionError(
+ "Error 61 connecting to 127.0.0.1:7788. Connection refused."
+ ),
+ "hello",
+ ]
+ original_connection = iredis_client.connection
+ iredis_client.connection = mock_connection
+ value = iredis_client.execute("None", "GET", ["foo"])
+ assert value == "hello" # be rendered
+
+ mock_connection.disconnect.assert_called_once()
+ mock_connection.connect.assert_called_once()
+
+ iredis_client.connection = original_connection
+
+
+def test_on_connection_error_retry_without_retrytimes(iredis_client, config):
+ config.retry_times = 0
+ mock_connection = MagicMock()
+ mock_connection.read_response.side_effect = [
+ redis.exceptions.ConnectionError(
+ "Error 61 connecting to 127.0.0.1:7788. Connection refused."
+ ),
+ "hello",
+ ]
+ iredis_client.connection = mock_connection
+ with pytest.raises(redis.exceptions.ConnectionError):
+ iredis_client.execute("None", "GET", ["foo"])
+
+ mock_connection.disconnect.assert_not_called()
+ mock_connection.connect.assert_not_called()
+
+
+def test_socket_keepalive(config):
+ config.socket_keepalive = True
+ from iredis.client import Client
+
+ newclient = Client("127.0.0.1", "6379", 0)
+ assert newclient.connection.socket_keepalive
+
+ # keepalive off
+ config.socket_keepalive = False
+
+ newclient = Client("127.0.0.1", "6379", 0)
+ assert not newclient.connection.socket_keepalive
+
+
+def test_not_retry_on_authentication_error(iredis_client, config):
+ config.retry_times = 2
+ mock_connection = MagicMock()
+ mock_connection.read_response.side_effect = [
+ redis.exceptions.AuthenticationError("Authentication required."),
+ "hello",
+ ]
+ iredis_client.connection = mock_connection
+ with pytest.raises(redis.exceptions.ConnectionError):
+ iredis_client.execute("None", "GET", ["foo"])
+
+
+@pytest.mark.skipif(
+ "version_parse(os.environ['REDIS_VERSION']) != version_parse('6')",
+ reason="""
+in redis7, it will not work if you:
+1. connect redis without password
+2. set a password
+3. auth
+
+the auth will fail""",
+)
+def test_auto_select_db_and_auth_for_reconnect_only_6(iredis_client, config):
+ config.retry_times = 2
+ config.raw = True
+ next(iredis_client.send_command("select 2"))
+ assert iredis_client.connection.db == 2
+
+ resp = next(iredis_client.send_command("auth 123"))
+
+ assert (
+ b"ERROR AUTH <password> called without any "
+ b"password configured for the default user. "
+ b"Are you sure your configuration is correct?" in resp
+ )
+ assert iredis_client.connection.password is None
+
+ next(iredis_client.send_command("config set requirepass 'abc'"))
+ next(iredis_client.send_command("auth abc"))
+ assert iredis_client.connection.password == "abc"
+ assert (
+ iredis_client.execute("ACL SETUSER", "default", "on", "nopass", "~*", "+@all")
+ == b"OK"
+ )
+
+
+@pytest.mark.skipif("version_parse(os.environ['REDIS_VERSION']) > version_parse('5')")
+def test_auto_select_db_and_auth_for_reconnect_only_5(iredis_client, config):
+ config.retry_times = 2
+ config.raw = True
+ next(iredis_client.send_command("select 2"))
+ assert iredis_client.connection.db == 2
+
+ resp = next(iredis_client.send_command("auth 123"))
+
+ assert b"Client sent AUTH, but no password is set" in resp
+ assert iredis_client.connection.password is None
+
+ next(iredis_client.send_command("config set requirepass 'abc'"))
+ next(iredis_client.send_command("auth abc"))
+ assert iredis_client.connection.password == "abc"
+ next(iredis_client.send_command("config set requirepass ''"))
+
+
+def test_split_shell_command(iredis_client, completer):
+ assert iredis_client.split_command_and_pipeline(" get json | rg . ", completer) == (
+ " get json ",
+ "rg . ",
+ )
+
+ assert iredis_client.split_command_and_pipeline(
+ """ get "json | \\" hello" | rg . """, completer
+ ) == (""" get "json | \\" hello" """, "rg . ")
+
+
+def test_running_with_pipeline(clean_redis, iredis_client, capfd, completer):
+ config.shell = True
+ clean_redis.set("foo", "hello \n world")
+ with pytest.raises(StopIteration):
+ next(iredis_client.send_command("get foo | grep w", completer))
+ out, err = capfd.readouterr()
+ assert out == " world\n"
+
+
+def test_running_with_multiple_pipeline(clean_redis, iredis_client, capfd, completer):
+ config.shell = True
+ clean_redis.set("foo", "hello world\nhello iredis")
+ with pytest.raises(StopIteration):
+ next(
+ iredis_client.send_command("get foo | grep hello | grep iredis", completer)
+ )
+ out, err = capfd.readouterr()
+ assert out == "hello iredis\n"
+
+
+def test_can_not_connect_on_startup(capfd):
+ with pytest.raises(SystemExit):
+ Client("localhost", "16111", 15)
+ out, err = capfd.readouterr()
+ assert "connecting to localhost:16111." in err
+
+
+def test_peek_key_not_exist(iredis_client, clean_redis, config):
+ config.raw = False
+ peek_result = list(iredis_client.do_peek("non-exist-key"))
+ assert peek_result == ["non-exist-key doesn't exist."]
+
+
+def test_iredis_with_username():
+ with patch("redis.connection.Connection.connect"):
+ c = Client("127.0.0.1", "6379", username="abc", password="abc1")
+ assert c.connection.username == "abc"
+ assert c.connection.password == "abc1"
+
+
+def test_peek_string(iredis_client, clean_redis):
+ clean_redis.set("foo", "bar")
+ peek_result = list(iredis_client.do_peek("foo"))
+
+ assert peek_result[0][0] == ("class:dockey", "key: ")
+ assert re.match(r"string \(embstr\) mem: \d+ bytes, ttl: -1", peek_result[0][1][1])
+ assert peek_result[0][2:] == [
+ ("", "\n"),
+ ("class:dockey", "strlen: "),
+ ("", "3"),
+ ("", "\n"),
+ ("class:dockey", "value: "),
+ ("", '"bar"'),
+ ]
+
+
+def test_peek_list_fetch_all(iredis_client, clean_redis):
+ clean_redis.lpush("mylist", *[f"hello-{index}" for index in range(5)])
+ peek_result = list(iredis_client.do_peek("mylist"))
+
+ formatted_text_rematch(
+ peek_result[0],
+ FormattedText(
+ [
+ ("class:dockey", "key: "),
+ ("", rf"list \({list_type}\) mem: \d+ bytes, ttl: -1"),
+ ("", "\n"),
+ ("class:dockey", "llen: "),
+ ("", "5"),
+ ("", "\n"),
+ ("class:dockey", "elements: "),
+ ("", "\n"),
+ ("", r"1\)"),
+ ("", " "),
+ ("class:string", '"hello-4"'),
+ ("", "\n"),
+ ("", r"2\)"),
+ ("", " "),
+ ("class:string", '"hello-3"'),
+ ("", "\n"),
+ ("", r"3\)"),
+ ("", " "),
+ ("class:string", '"hello-2"'),
+ ("", "\n"),
+ ("", r"4\)"),
+ ("", " "),
+ ("class:string", '"hello-1"'),
+ ("", "\n"),
+ ("", r"5\)"),
+ ("", " "),
+ ("class:string", '"hello-0"'),
+ ]
+ ),
+ )
+
+
+def test_peek_list_fetch_part(iredis_client, clean_redis):
+ clean_redis.lpush("mylist", *[f"hello-{index}" for index in range(40)])
+ peek_result = list(iredis_client.do_peek("mylist"))
+ assert len(peek_result[0]) == 91
+
+
+def test_peek_set_fetch_all(iredis_client, clean_redis):
+ clean_redis.sadd("myset", *[f"hello-{index}" for index in range(5)])
+ peek_result = list(iredis_client.do_peek("myset"))
+ assert len(peek_result[0]) == 27
+
+
+def test_peek_set_fetch_part(iredis_client, clean_redis):
+ clean_redis.sadd("myset", *[f"hello-{index}" for index in range(40)])
+ peek_result = list(iredis_client.do_peek("myset"))
+
+ assert peek_result[0][0] == ("class:dockey", "key: ")
+ assert peek_result[0][1][1].startswith(f"set ({hash_type}) mem: ")
+
+
+def test_peek_zset_fetch_all(iredis_client, clean_redis):
+ clean_redis.zadd(
+ "myzset", dict(zip([f"hello-{index}" for index in range(3)], range(3)))
+ )
+ peek_result = list(iredis_client.do_peek("myzset"))
+
+ formatted_text_rematch(
+ peek_result[0][0:9],
+ FormattedText(
+ [
+ ("class:dockey", "key: "),
+ ("", rf"zset \({zset_type}\) mem: \d+ bytes, ttl: -1"),
+ ("", "\n"),
+ ("class:dockey", "zcount: "),
+ ("", "3"),
+ ("", "\n"),
+ ("class:dockey", "members: "),
+ ("", "\n"),
+ ("", r"1\)"),
+ ]
+ ),
+ )
+
+
+def test_peek_zset_fetch_part(iredis_client, clean_redis):
+ clean_redis.zadd(
+ "myzset", dict(zip([f"hello-{index}" for index in range(40)], range(40)))
+ )
+ peek_result = list(iredis_client.do_peek("myzset"))
+ formatted_text_rematch(
+ peek_result[0][0:8],
+ FormattedText(
+ [
+ ("class:dockey", "key: "),
+ ("", rf"zset \({zset_type}\) mem: \d+ bytes, ttl: -1"),
+ ("", "\n"),
+ ("class:dockey", "zcount: "),
+ ("", "40"),
+ ("", "\n"),
+ ("class:dockey", r"members \(first 40\): "),
+ ("", "\n"),
+ ]
+ ),
+ )
+
+
+def test_peek_hash_fetch_all(iredis_client, clean_redis):
+ for key, value in zip(
+ [f"hello-{index}" for index in range(3)], [f"hi-{index}" for index in range(3)]
+ ):
+ clean_redis.hset("myhash", key, value)
+ peek_result = list(iredis_client.do_peek("myhash"))
+ assert len(peek_result[0]) == 28
+
+
+def test_peek_hash_fetch_part(iredis_client, clean_redis):
+ for key, value in zip(
+ [f"hello-{index}" for index in range(100)],
+ [f"hi-{index}" for index in range(100)],
+ ):
+ clean_redis.hset("myhash", key, value)
+ peek_result = list(iredis_client.do_peek("myhash"))
+ assert len(peek_result[0]) == 707
+
+
+def test_peek_stream(iredis_client, clean_redis):
+ clean_redis.xadd("mystream", {"foo": "bar", "hello": "world"})
+ peek_result = list(iredis_client.do_peek("mystream"))
+
+ assert peek_result[0][0] == ("class:dockey", "key: ")
+ assert re.match(
+ r"stream \((stream|unknown)\) mem: \d+ bytes, ttl: -1", peek_result[0][1][1]
+ )
+ assert peek_result[0][2:18] == FormattedText(
+ [
+ ("", "\n"),
+ ("class:dockey", "XINFO: "),
+ ("", "\n"),
+ ("", " 1)"),
+ ("", " "),
+ ("class:string", '"length"'),
+ ("", "\n"),
+ ("", " 2)"),
+ ("", " "),
+ ("class:string", '"1"'),
+ ("", "\n"),
+ ("", " 3)"),
+ ("", " "),
+ ("class:string", '"radix-tree-keys"'),
+ ("", "\n"),
+ ("", " 4)"),
+ ]
+ )
+
+
+def test_mem_not_called_before_redis_4(config, iredis_client, clean_redis):
+ config.version = "3.2.9"
+
+ def wrapper(func):
+ def execute(command_name, *args):
+ print(command_name)
+ if command_name.upper() == "MEMORY USAGE":
+ raise Exception("MEMORY USAGE not supported!")
+ return func(command_name, *args)
+
+ return execute
+
+ iredis_client.execute = wrapper(iredis_client.execute)
+ clean_redis.set("foo", "bar")
+ result = list(iredis_client.do_peek("foo"))
+ assert result[0][1] == ("", "string (embstr), ttl: -1")
+
+
+def test_mem_not_called_when_cant_get_server_version(
+ config, iredis_client, clean_redis
+):
+ config.version = None
+
+ def wrapper(func):
+ def execute(command_name, *args):
+ print(command_name)
+ if command_name.upper() == "MEMORY USAGE":
+ raise Exception("MEMORY USAGE not supported!")
+ return func(command_name, *args)
+
+ return execute
+
+ iredis_client.execute = wrapper(iredis_client.execute)
+ clean_redis.set("foo", "bar")
+ result = list(iredis_client.do_peek("foo"))
+ assert result[0][1] == ("", "string (embstr), ttl: -1")
+
+
+def test_reissue_command_on_redis_cluster(iredis_client, clean_redis):
+ mock_response = iredis_client.connection = MagicMock()
+ mock_response.read_response.side_effect = redis.exceptions.ResponseError(
+ "MOVED 12182 127.0.0.1:7002"
+ )
+ iredis_client.reissue_with_redirect = MagicMock()
+ iredis_client.execute("set", "foo", "bar")
+ assert iredis_client.reissue_with_redirect.call_args == (
+ (
+ "MOVED 12182 127.0.0.1:7002",
+ "set",
+ "foo",
+ "bar",
+ ),
+ )
+
+
+def test_reissue_command_on_redis_cluster_with_password_in_dsn(
+ iredis_client, clean_redis
+):
+ config_content = dedent(
+ """
+ [main]
+ log_location = /tmp/iredis1.log
+ no_info=True
+ [alias_dsn]
+ cluster-7003=redis://foo:bar@127.0.0.1:7003
+ """
+ )
+ with open("/tmp/iredisrc", "w+") as etc_config:
+ etc_config.write(config_content)
+
+ config_obj = load_config_files("/tmp/iredisrc")
+ config.alias_dsn = config_obj["alias_dsn"]
+
+ mock_execute_by_connection = iredis_client.execute_by_connection = MagicMock()
+ with patch("redis.connection.Connection.connect"):
+ iredis_client.reissue_with_redirect(
+ "MOVED 12182 127.0.0.1:7003", "set", "foo", "bar"
+ )
+
+ call_args = mock_execute_by_connection.call_args[0]
+ print(call_args)
+ assert list(call_args[1:]) == ["set", "foo", "bar"]
+ assert call_args[0].password == "bar"
+
+
+def test_version_parse_for_auth(iredis_client):
+ """
+ fix: https://github.com/laixintao/iredis/issues/418
+ """
+ iredis_client.auth_compat("6.1.0")
+ assert command2syntax["AUTH"] == "command_usernamex_password"
+ iredis_client.auth_compat("5.0")
+ assert command2syntax["AUTH"] == "command_password"
+ iredis_client.auth_compat("5.0.14.1")
+ assert command2syntax["AUTH"] == "command_password"
+
+
+@pytest.mark.parametrize(
+ "info, version",
+ [
+ (
+ (
+ "# Server\r\nredis_version:df--128-NOTFOUND\r\n"
+ "redis_mode:standalone\r\narch_bits:64"
+ ),
+ "df--128-NOTFOUND",
+ ),
+ (
+ (
+ "# Server\r\nredis_version:6.2.5\r\n"
+ "redis_git_sha1:00000000\r\n"
+ "redis_git_dirty:0\r\n"
+ "redis_build_id:915e5480613bc9b6\r\n"
+ "redis_mode:standalone "
+ ),
+ "6.2.5",
+ ),
+ (
+ (
+ "# Server\r\nredis_version:5.0.14.1\r\n"
+ "redis_git_sha1:00000000\r\nredis_git_dirty:0\r\n"
+ "redis_build_id:915e5480613bc9b6\r\n"
+ "redis_mode:standalone "
+ ),
+ "5.0.14.1",
+ ),
+ ],
+)
+def test_version_path(info, version):
+ with patch("iredis.client.config") as mock_config:
+ mock_config.no_info = True
+ mock_config.pager = "less"
+ mock_config.version = "5.0.0"
+ mock_config.decode = "utf-8"
+ with patch("iredis.client.Client.execute") as mock_execute:
+ mock_execute.return_value = info
+ client = Client("127.0.0.1", 6379)
+ client.get_server_info()
+ assert mock_config.version == version
+
+
+def test_prompt():
+ c = Client()
+ assert str(c) == "127.0.0.1:6379> "
+
+ c = Client(prompt="{host} {port} {db}")
+ assert str(c) == "127.0.0.1 6379 0"
+
+ c = Client(prompt="{host} {port} {db} {username}")
+ assert str(c) == "127.0.0.1 6379 0 None"
+
+ c = Client(prompt="{host} {port} {db} {username}", username="foo1")
+ assert str(c) == "127.0.0.1 6379 0 foo1"
+
+ c = Client(prompt="{client_id} aabc")
+ assert re.match(r"^\d+ aabc$", str(c))
+ c = Client(prompt="{client_addr} >")
+ assert re.match(r"^127.0.0.1:\d+ >$", str(c))