Repository: fppt/jedis-mock (java)

Issue:
Unable to put data into Redis hash with Redisson client
When I use Redisson to put data into a Redis hash, the following error occurs.
The following code works when I use Jedis instead of Redisson or a local Redis docker image instead of the jedis-mock.

jedis-mock version: 1.0.10

**Code snippet:**
```
public void test() throws IOException {
	RedisServer redisServer = RedisServer.newRedisServer(6379);
	redisServer.start();

	RedissonClient client = Redisson.create();

	RMap<String, Integer> map = client.getMap("myMap");
	map.put("a", 1);
}
```

**Error:**
```
00:32:49,621       redisson-netty-21-2            ERROR CommandDecoder:208 - Unable to decode data. channel: [id: 0x45d70998, L:/127.0.0.1:59675 - R:127.0.0.1/127.0.0.1:6379], reply: ReplayingDecoderByteBuf(ridx=6, widx=6), command: (EVAL), promise: java.util.concurrent.CompletableFuture@653249f6[Not completed, 1 dependents], params: [local v = redis.call('hget', KEYS[1], ARGV[1]); redis.call('hset', KEYS[1], ARGV[1], ARGV[2]); return v, 1, myMap, PooledUnsafeDirectByteBuf(ridx: 0, widx: 3, cap: 256), PooledUnsafeDirectByteBuf(ridx: 0, widx: 2, cap: 256)]
com.esotericsoftware.kryo.io.KryoBufferUnderflowException: Buffer underflow.
	at com.esotericsoftware.kryo.io.Input.require(Input.java:221) ~[kryo-5.5.0.jar:?]
	at com.esotericsoftware.kryo.io.Input.readVarInt(Input.java:409) ~[kryo-5.5.0.jar:?]
	at com.esotericsoftware.kryo.util.DefaultClassResolver.readClass(DefaultClassResolver.java:145) ~[kryo-5.5.0.jar:?]
	at com.esotericsoftware.kryo.Kryo.readClass(Kryo.java:758) ~[kryo-5.5.0.jar:?]
	at com.esotericsoftware.kryo.Kryo.readClassAndObject(Kryo.java:869) ~[kryo-5.5.0.jar:?]
	at org.redisson.codec.Kryo5Codec$4.decode(Kryo5Codec.java:144) ~[redisson-3.23.4.jar:3.23.4]
	at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:394) ~[redisson-3.23.4.jar:3.23.4]
	at org.redisson.client.handler.CommandDecoder.decodeCommand(CommandDecoder.java:205) [redisson-3.23.4.jar:3.23.4]
	at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:144) [redisson-3.23.4.jar:3.23.4]
	at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:120) [redisson-3.23.4.jar:3.23.4]
	at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:529) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:366) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:290) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at java.lang.Thread.run(Thread.java:750) [?:1.8.0_321]
00:32:49,647       redisson-netty-21-2            ERROR ErrorsLoggingHandler:47 - Exception occured. Channel: [id: 0x45d70998, L:/127.0.0.1:59675 - R:127.0.0.1/127.0.0.1:6379]
io.netty.handler.codec.DecoderException: com.esotericsoftware.kryo.io.KryoBufferUnderflowException: Buffer underflow.
	at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:421) ~[com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:290) ~[com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) [com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at java.lang.Thread.run(Thread.java:750) [?:1.8.0_321]
Caused by: com.esotericsoftware.kryo.io.KryoBufferUnderflowException: Buffer underflow.
	at com.esotericsoftware.kryo.io.Input.require(Input.java:221) ~[kryo-5.5.0.jar:?]
	at com.esotericsoftware.kryo.io.Input.readVarInt(Input.java:409) ~[kryo-5.5.0.jar:?]
	at com.esotericsoftware.kryo.util.DefaultClassResolver.readClass(DefaultClassResolver.java:145) ~[kryo-5.5.0.jar:?]
	at com.esotericsoftware.kryo.Kryo.readClass(Kryo.java:758) ~[kryo-5.5.0.jar:?]
	at com.esotericsoftware.kryo.Kryo.readClassAndObject(Kryo.java:869) ~[kryo-5.5.0.jar:?]
	at org.redisson.codec.Kryo5Codec$4.decode(Kryo5Codec.java:144) ~[redisson-3.23.4.jar:3.23.4]
	at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:394) ~[redisson-3.23.4.jar:3.23.4]
	at org.redisson.client.handler.CommandDecoder.decodeCommand(CommandDecoder.java:205) ~[redisson-3.23.4.jar:3.23.4]
	at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:144) ~[redisson-3.23.4.jar:3.23.4]
	at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:120) ~[redisson-3.23.4.jar:3.23.4]
	at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:529) ~[com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:366) ~[com.sap.cloud.connectivity.netty-all-1.103.0.jar:?]
	... 17 more
```

Pull request:
fix lua scripting for Redisson
fix #354

Patch:
diff --git a/README.md b/README.md
index c6f8a27..e5e7b63 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
 
 Jedis-Mock is a simple in-memory mock of Redis for Java testing, which can also work as test proxy. 
 Despite its name, it works on network protocol level and can be used with any Redis client 
-(be it `Jedis`, `Lettuce` or others).
+(be it [Jedis](https://github.com/redis/jedis), [Lettuce](https://github.com/lettuce-io/lettuce-core), [Redisson](https://github.com/redisson/redisson) or others).
 
 When used as a mock, it allows you to test behaviour dependent on Redis without having to deploy an instance of Redis.
 
@@ -60,6 +60,13 @@ Jedis jedis = new Jedis(server.getHost(), server.getBindPort());
 RedisClient redisClient = RedisClient
         .create(String.format("redis://%s:%s",
         server.getHost(), server.getBindPort()));
+
+//Redisson connection:
+Config config = new Config();
+config.useSingleServer().setAddress(
+        String.format("redis://%s:%d",
+        redisServer.getHost(), redisServer.getBindPort()));
+RedissonClient client = Redisson.create(config);
 ```
 
 From here test as needed.
diff --git a/pom.xml b/pom.xml
index 8d1c546..94aac8a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -90,6 +90,12 @@
             <version>${junit.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-all</artifactId>
+            <version>4.1.100.Final</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>io.lettuce</groupId>
             <artifactId>lettuce-core</artifactId>
@@ -127,6 +133,13 @@
             <version>3.15.2</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson</artifactId>
+            <version>3.23.1</version>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 
     <build>
diff --git a/src/main/java/com/github/fppt/jedismock/operations/scripting/Eval.java b/src/main/java/com/github/fppt/jedismock/operations/scripting/Eval.java
index 154844e..bf6864f 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/scripting/Eval.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/scripting/Eval.java
@@ -8,6 +8,7 @@ import com.github.fppt.jedismock.storage.OperationExecutorState;
 import com.github.fppt.jedismock.storage.RedisBase;
 import org.luaj.vm2.Globals;
 import org.luaj.vm2.LuaError;
+import org.luaj.vm2.LuaString;
 import org.luaj.vm2.LuaTable;
 import org.luaj.vm2.LuaValue;
 import org.luaj.vm2.lib.jse.CoerceJavaToLua;
@@ -69,7 +70,7 @@ public class Eval extends AbstractRedisOperation {
 
     private static List<LuaValue> getLuaValues(List<Slice> slices) {
         return slices.stream()
-                .map(Slice::toString)
+                .map(Slice::data)
                 .map(LuaValue::valueOf)
                 .collect(Collectors.toList());
     }
@@ -85,7 +86,7 @@ public class Eval extends AbstractRedisOperation {
 
         switch (result.typename()) {
             case "string":
-                return Response.bulkString(Slice.create(result.tojstring()));
+                return Response.bulkString(Slice.create(((LuaString) result).m_bytes));
             case "number":
                 return Response.integer(result.tolong());
             case "table":
diff --git a/src/main/java/com/github/fppt/jedismock/operations/scripting/LuaRedisCallback.java b/src/main/java/com/github/fppt/jedismock/operations/scripting/LuaRedisCallback.java
index f1cfa6b..0167401 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/scripting/LuaRedisCallback.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/scripting/LuaRedisCallback.java
@@ -4,7 +4,9 @@ import com.github.fppt.jedismock.datastructures.Slice;
 import com.github.fppt.jedismock.operations.CommandFactory;
 import com.github.fppt.jedismock.operations.RedisOperation;
 import com.github.fppt.jedismock.operations.connection.Select;
+import com.github.fppt.jedismock.server.Response;
 import com.github.fppt.jedismock.storage.OperationExecutorState;
+import org.luaj.vm2.LuaString;
 import org.luaj.vm2.LuaTable;
 import org.luaj.vm2.LuaValue;
 import org.slf4j.LoggerFactory;
@@ -34,7 +36,12 @@ public class LuaRedisCallback {
         String operationName = args.get(1).tojstring();
         List<Slice> a = new ArrayList<>();
         for (int i = 2; i <= args.length(); i++) {
-            a.add(Slice.create(args.get(i).tojstring()));
+            LuaValue arg = args.get(i);
+            if (arg instanceof LuaString) {
+                a.add(Slice.create(((LuaString) arg).m_bytes));
+            } else {
+                a.add(Slice.create(args.get(i).tojstring()));
+            }
         }
         return execute(operationName, a);
     }
@@ -67,7 +74,7 @@ public class LuaRedisCallback {
         if (operation != null) {
             throwOnUnsupported(operation);
             Slice result = operation.execute();
-            if (result == null) {
+            if (Response.NULL.equals(result)) {
                 return LuaValue.NONE;
             } else {
                 byte[] data = result.data();


Test patch:
diff --git a/src/test/java/com/github/fppt/jedismock/TestRedissonConnection.java b/src/test/java/com/github/fppt/jedismock/TestRedissonConnection.java
new file mode 100644
index 0000000..7b681b0
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/TestRedissonConnection.java
@@ -0,0 +1,62 @@
+package com.github.fppt.jedismock;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.redisson.Redisson;
+import org.redisson.api.RList;
+import org.redisson.api.RMap;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+public class TestRedissonConnection {
+    private static RedisServer redisServer;
+    private static Config config;
+
+    @BeforeAll
+    static void setUp() throws IOException {
+        redisServer = RedisServer.newRedisServer();
+        redisServer.start();
+        config = new Config();
+        config.useSingleServer().setAddress(
+                String.format("redis://%s:%d",
+                        redisServer.getHost(), redisServer.getBindPort()));
+    }
+
+    @AfterAll
+    static void tearDown() throws IOException {
+        redisServer.stop();
+    }
+
+    @Test
+    public void testStringMap() {
+        RedissonClient client = Redisson.create(config);
+        RMap<String, String> map = client.getMap("stringMap");
+        Assertions.assertNull(map.put("foo", "bar"));
+        Assertions.assertEquals("bar", map.get("foo"));
+        Assertions.assertEquals("bar", map.put("foo", "baz"));
+    }
+
+    @Test
+    public void testIntegerMap() {
+        RedissonClient client = Redisson.create(config);
+        RMap<String, Integer> map = client.getMap("intMap");
+        Assertions.assertNull(map.put("foo", 42));
+        Assertions.assertEquals(42, map.get("foo"));
+        Assertions.assertEquals(42, map.put("foo", 43));
+    }
+    @Test
+    public void testIntList() {
+        RedissonClient client = Redisson.create(config);
+        RList<Integer> list = client.getList("intList");
+        Assertions.assertTrue(list.add(11));
+        Assertions.assertTrue(list.add(15));
+        Assertions.assertEquals(2, list.size());
+        Assertions.assertEquals(Arrays.asList(11, 15), list.readAll());
+    }
+
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/scripting/EvalTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/scripting/EvalTest.java
index 2c82c74..1f86e75 100644
--- a/src/test/java/com/github/fppt/jedismock/comparisontests/scripting/EvalTest.java
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/scripting/EvalTest.java
@@ -7,9 +7,15 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import redis.clients.jedis.Jedis;
 import redis.clients.jedis.exceptions.JedisDataException;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 @ExtendWith(ComparisonBase.class)
 public class EvalTest {
@@ -171,7 +177,7 @@ public class EvalTest {
 
     @TestTemplate
     void evalRedisPCallCanHandleExceptionTest(Jedis jedis) {
-        assertEquals("Handled error from pcall", jedis.eval("" +
+        assertEquals("Handled error from pcall", jedis.eval(
                         "local reply = redis.pcall('RENAME','A','B')\n" +
                         "if reply['err'] ~= nil then\n" +
                         "  return 'Handled error from pcall'" +
@@ -251,5 +257,20 @@ public class EvalTest {
         assertEquals("DB6", jedis.get("foo"));
     }
 
+    @TestTemplate
+    public void luaReturnsNullFromEmptyMap(Jedis jedis) {
+        String s = "return redis.call('hget', KEYS[1], ARGV[1])";
+        Object res = jedis.eval(s, Collections.singletonList("foo"),
+                Collections.singletonList("bar"));
+        assertNull(res);
+    }
+
+    @TestTemplate
+    public void luaReturnsNullFromNonExistentKey(Jedis jedis) {
+        String s = "return redis.call('get', KEYS[1])";
+        Object res = jedis.eval(s, Collections.singletonList("foo"),
+                Collections.emptyList());
+        assertNull(res);
+    }
 
 }