GEODE-8852: Add additional tests for Redis HVALS command (#5931)

* Also make error response assertions more exact

Co-authored-by: Ray Ingles <ringles@vmware.com>
diff --git a/geode-docs/tools_modules/redis_api_for_geode.html.md.erb b/geode-docs/tools_modules/redis_api_for_geode.html.md.erb
index dfb17e3..2887109 100644
--- a/geode-docs/tools_modules/redis_api_for_geode.html.md.erb
+++ b/geode-docs/tools_modules/redis_api_for_geode.html.md.erb
@@ -68,7 +68,7 @@
 **Note**: These commands are supported for Redis 5.
 
 -   **Connection**: AUTH, PING, QUIT
--   **Hashes**: HGETALL, HMSET, HSET
+-   **Hashes**: HGETALL, HMSET, HSET, HVALS
 -   **Keys**: DEL, EXISTS, EXPIRE, EXPIREAT, KEYS, PERSIST, PEXPIRE, PEXPIREAT, PTTL, RENAME, TTL,
   TYPE
 -   **Publish/Subscribe**: PUBLISH, PSUBSCRIBE, PUNSUBSCRIBE, SUBSCRIBE, UNSUBSCRIBE
@@ -81,7 +81,7 @@
 
 -   **Connection**: ECHO, SELECT
 -   **Hashes**: HDEL, HEXISTS, HGET, HINCRBY, HINCRBYFLOAT, HKEYS, HLEN, HMGET, HSCAN, HSETNX,
-  HSTRLEN, HVALS
+  HSTRLEN
 -   **Keys**: SCAN, UNLINK
 -   **Server**: DBSIZE, FLUSHALL (no async option), FLUSHDB (no async option), INFO, SHUTDOWN,
   SLOWLOG, TIME
diff --git a/geode-redis/README.md b/geode-redis/README.md
index c9b8b96..72d4a36 100644
--- a/geode-redis/README.md
+++ b/geode-redis/README.md
@@ -167,51 +167,51 @@
 | HGETALL            	| FLUSHALL                                           	| ACL LOG                       	|
 | HMSET              	| FLUSHDB                                            	| ACL SAVE                      	|
 | HSET               	| GETBIT                                             	| ACL SETUSER                   	|
-| KEYS               	| GETRANGE                                           	| ACL USERS                     	|
-| PERSIST            	| GETSET                                             	| ACL WHOAMI                    	|
-| PEXPIRE            	| HDEL                                               	| BGREWRITEAOF                  	|
-| PEXPIREAT          	| HEXISTS                                            	| BGSAVE                        	|
-| PING               	| HGET                                               	| BITFIELD                      	|
-| PSUBSCRIBE         	| HINCRBY                                            	| BLPOP                         	|
-| PTTL               	| HINCRBYFLOAT                                       	| BRPOP                         	|
-| PUBLISH            	| HKEYS                                              	| BRPOPLPUSH                    	|
-| PUNSUBSCRIBE       	| HLEN                                               	| BZPOPMAX                      	|
-| QUIT               	| HMGET                                              	| BZPOPMIN                      	|
-| RENAME             	| HSCAN                                              	| CLIENT CACHING                	|
-| SADD               	| HSETNX                                             	| CLIENT GETNAME                	|
-| SET                	| HSTRLEN                                           	| CLIENT GETREDIR               	|
-| SMEMBERS           	| HVALS                                              	| CLIENT ID                     	|
-| SREM               	| INCR                                               	| CLIENT KILL                   	|
-| SUBSCRIBE          	| INCRBY                                             	| CLIENT LIST                   	|
-| TTL                	| INCRBYFLOAT                                        	| CLIENT PAUSE                  	|
-| TYPE               	| INFO                                                  | CLIENT REPLY                  	|
-| UNSUBSCRIBE       	| MGET                                               	| CLIENT SETNAME                	|
-|                    	| MSET                                               	| CLIENT TRACKING               	|
-|                    	| MSETNX                                             	| CLIENT UNBLOCK                	|
-|                    	| PSETEX                                             	| CLUSTER ADDSLOTS              	|
-|                    	| SCAN                                               	| CLUSTER BUMPEPOCH             	|
-|                    	| SCARD                                              	| CLUSTER COUNT-FAILURE-REPORTS 	|
-|                    	| SDIFF                                              	| CLUSTER COUNTKEYSINSLOT       	|
-|                    	| SDIFFSTORE                                         	| CLUSTER DELSLOTS     	            |
-|                    	| SELECT                                             	| CLUSTER FAILOVER              	|
-|                    	| SETBIT                                             	| CLUSTER FLUSHSLOTS                |
-|                    	| SETEX                                              	| CLUSTER FORGET                    |
-|                    	| SETNX                                              	| CLUSTER GETKEYSINSLOT             |
-|                    	| SETRANGE                                           	| CLUSTER INFO                  	|
-|                    	| SHUTDOWN                                           	| CLUSTER KEYSLOT                   |
-|                    	| SINTER                                             	| CLUSTER MEET                      |
-|                    	| SINTERSTORE                                        	| CLUSTER MYID                   	|
-|                    	| SISMEMBER                                          	| CLUSTER NODES                 	|
-|                    	| SLOWLOG                                            	| CLUSTER REPLICAS                  |
-|                    	| SMOVE                                              	| CLUSTER REPLICATE              	|
-|                    	| SPOP                                               	| CLUSTER RESET                  	|
-|                    	| SRANDMEMBER                                        	| CLUSTER SAVECONFIG                |
-|                    	| SSCAN                                              	| CLUSTER SET-CONFIG-EPOCH          |
-|                    	| STRLEN                                             	| CLUSTER SETSLOT                   |
-|                    	| SUNION                                             	| CLUSTER SLAVES                    |
-|                    	| SUNIONSTORE                                        	| CLUSTER SLOTS                 	|
-|                    	| TIME                                                  | COMMAND                       	|
-|                    	| UNLINK [1]                                         	| COMMAND COUNT                 	|
+| HVALS          	| GETRANGE                                           	| ACL USERS                     	|
+| KEYS               	| GETSET                                             	| ACL WHOAMI                    	|
+| PERSIST            	| HDEL                                               	| BGREWRITEAOF                  	|
+| PEXPIRE            	| HEXISTS                                            	| BGSAVE                        	|
+| PEXPIREAT          	| HGET                                               	| BITFIELD                      	|
+| PING               	| HINCRBY                                            	| BLPOP                         	|
+| PSUBSCRIBE         	| HINCRBYFLOAT                                       	| BRPOP                         	|
+| PTTL               	| HKEYS                                              	| BRPOPLPUSH                    	|
+| PUBLISH            	| HLEN                                               	| BZPOPMAX                      	|
+| PUNSUBSCRIBE       	| HMGET                                              	| BZPOPMIN                      	|
+| QUIT               	| HSCAN                                              	| CLIENT CACHING                	|
+| RENAME             	| HSETNX                                             	| CLIENT GETNAME                	|
+| SADD               	| HSTRLEN                                           	| CLIENT GETREDIR               	|
+| SET                	| INCR                                                 	| CLIENT ID                     	|
+| SMEMBERS           	| INCRBY                                             	| CLIENT KILL                   	|
+| SREM               	| INCRBYFLOAT                                        	| CLIENT LIST                   	|
+| SUBSCRIBE          	| INFO                                               	| CLIENT PAUSE                  	|
+| TTL                	| MGET                                                  | CLIENT REPLY                  	|
+| TYPE              	| MSET                                               	| CLIENT SETNAME                	|
+| UNSUBSCRIBE           | MSETNX                                             	| CLIENT TRACKING               	|
+|                    	| PSETEX                                             	| CLIENT UNBLOCK                	|
+|                    	| SCAN                                               	| CLUSTER ADDSLOTS              	|
+|                    	| SCARD                                              	| CLUSTER BUMPEPOCH             	|
+|                    	| SDIFF                                              	| CLUSTER COUNT-FAILURE-REPORTS 	|
+|                    	| SDIFFSTORE                                         	| CLUSTER COUNTKEYSINSLOT       	|
+|                    	| SELECT                                             	| CLUSTER DELSLOTS     	            |
+|                    	| SETBIT                                             	| CLUSTER FAILOVER              	|
+|                    	| SETEX                                              	| CLUSTER FLUSHSLOTS                |
+|                    	| SETNX                                              	| CLUSTER FORGET                    |
+|                    	| SETRANGE                                           	| CLUSTER GETKEYSINSLOT             |
+|                    	| SHUTDOWN                                           	| CLUSTER INFO                  	|
+|                    	| SINTER                                             	| CLUSTER KEYSLOT                   |
+|                    	| SINTERSTORE                                        	| CLUSTER MEET                      |
+|                    	| SISMEMBER                                          	| CLUSTER MYID                   	|
+|                    	| SLOWLOG                                            	| CLUSTER NODES                 	|
+|                    	| SMOVE                                              	| CLUSTER REPLICAS                  |
+|                    	| SPOP                                               	| CLUSTER REPLICATE              	|
+|                    	| SRANDMEMBER                                        	| CLUSTER RESET                  	|
+|                    	| SSCAN                                              	| CLUSTER SAVECONFIG                |
+|                    	| STRLEN                                             	| CLUSTER SET-CONFIG-EPOCH          |
+|                    	| SUNION                                             	| CLUSTER SETSLOT                   |
+|                    	| SUNIONSTORE                                        	| CLUSTER SLAVES                    |
+|                    	| TIME                                               	| CLUSTER SLOTS                 	|
+|                    	| UNLINK [1]                                            | COMMAND                       	|
+|                    	|                                             	        | COMMAND COUNT                 	|
 |                    	|                                                    	| COMMAND GETKEYS               	|
 |                    	|                                                    	| COMMAND INFO                  	|
 |                    	|                                                    	| CONFIG GET                    	|
diff --git a/geode-redis/src/distributedTest/java/org/apache/geode/redis/internal/executor/hash/HvalsDUnitTest.java b/geode-redis/src/distributedTest/java/org/apache/geode/redis/internal/executor/hash/HvalsDUnitTest.java
new file mode 100644
index 0000000..60ea735
--- /dev/null
+++ b/geode-redis/src/distributedTest/java/org/apache/geode/redis/internal/executor/hash/HvalsDUnitTest.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.geode.redis.internal.executor.hash;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import java.util.Random;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import redis.clients.jedis.Jedis;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class HvalsDUnitTest {
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(4);
+
+  private static final String LOCAL_HOST = "127.0.0.1";
+  private static final int JEDIS_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private static Jedis jedis1;
+  private static Jedis jedis2;
+  private static Jedis jedis3;
+
+  @BeforeClass
+  public static void classSetup() {
+    MemberVM locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.startRedisVM(2, locator.getPort());
+
+    int redisServerPort1 = clusterStartUp.getRedisPort(1);
+    int redisServerPort2 = clusterStartUp.getRedisPort(2);
+
+    jedis1 = new Jedis(LOCAL_HOST, redisServerPort1, JEDIS_TIMEOUT);
+    jedis2 = new Jedis(LOCAL_HOST, redisServerPort2, JEDIS_TIMEOUT);
+    jedis3 = new Jedis(LOCAL_HOST, redisServerPort1, JEDIS_TIMEOUT);
+  }
+
+  @Before
+  public void testSetup() {
+    jedis1.flushAll();
+  }
+
+  @Test
+  public void hvalsWorks_whileAlsoUpdatingHash() {
+    String key = "key";
+    int fieldCount = 100;
+    int iterations = 10000;
+    Random rand = new Random();
+
+    for (int i = 0; i < fieldCount; i++) {
+      jedis1.hset(key, "field-" + i, "" + i);
+    }
+
+    new ConcurrentLoopingThreads(iterations,
+        (i) -> {
+          int x = rand.nextInt(fieldCount);
+          String field = "field-" + x;
+          String currentValue = jedis1.hget(key, field);
+          jedis1.hset(key, field, "" + (Long.parseLong(currentValue) + i));
+        },
+        (i) -> assertThat(jedis2.hvals(key)).hasSize(fieldCount),
+        (i) -> assertThat(jedis3.hvals(key)).hasSize(fieldCount))
+            .run();
+
+    List<String> values = jedis1.hvals(key);
+    long finalTotal = values.stream().mapToLong(Long::valueOf).sum();
+
+    // Spell out the formula for a sum of an arithmetic sequence which is: (n / 2) * (start + end)
+    long sumOfBothSequenceSums = (fieldCount / 2) * ((fieldCount - 1) + 0) +
+        (iterations / 2) * ((iterations - 1) + 0);
+    assertThat(finalTotal).isEqualTo(sumOfBothSequenceSums);
+  }
+
+}
diff --git a/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/hash/AbstractHashesIntegrationTest.java b/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/hash/AbstractHashesIntegrationTest.java
index 7212416..3b66ecf 100755
--- a/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/hash/AbstractHashesIntegrationTest.java
+++ b/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/hash/AbstractHashesIntegrationTest.java
@@ -39,6 +39,7 @@
 import redis.clients.jedis.exceptions.JedisDataException;
 
 import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.internal.RedisConstants;
 import org.apache.geode.test.dunit.rules.RedisPortSupplier;
 
 public abstract class AbstractHashesIntegrationTest implements RedisPortSupplier {
@@ -69,11 +70,12 @@
   @Test
   public void testHMSet_givenWrongNumberOfArguments() {
     assertThatThrownBy(() -> jedis.sendCommand(Protocol.Command.HMSET))
-        .hasMessageContaining("wrong number of arguments");
+        .hasMessage("ERR wrong number of arguments for 'hmset' command");
     assertThatThrownBy(() -> jedis.sendCommand(Protocol.Command.HMSET, "1"))
-        .hasMessageContaining("wrong number of arguments");
+        .hasMessage("ERR wrong number of arguments for 'hmset' command");
     assertThatThrownBy(() -> jedis.sendCommand(Protocol.Command.HMSET, "1", "2"))
-        .hasMessageContaining("wrong number of arguments");
+        .hasMessage("ERR wrong number of arguments for 'hmset' command");
+    // Redis is somewhat inconsistent with the error response here
     assertThatThrownBy(() -> jedis.sendCommand(Protocol.Command.HMSET, "1", "2", "3", "4"))
         .hasMessageContaining("wrong number of arguments");
   }
@@ -81,11 +83,12 @@
   @Test
   public void testHSet_givenWrongNumberOfArguments() {
     assertThatThrownBy(() -> jedis.sendCommand(Protocol.Command.HSET))
-        .hasMessageContaining("wrong number of arguments");
+        .hasMessage("ERR wrong number of arguments for 'hset' command");
     assertThatThrownBy(() -> jedis.sendCommand(Protocol.Command.HSET, "1"))
-        .hasMessageContaining("wrong number of arguments");
+        .hasMessage("ERR wrong number of arguments for 'hset' command");
     assertThatThrownBy(() -> jedis.sendCommand(Protocol.Command.HSET, "1", "2"))
-        .hasMessageContaining("wrong number of arguments");
+        .hasMessage("ERR wrong number of arguments for 'hset' command");
+    // Redis is somewhat inconsistent with the error response here
     assertThatThrownBy(() -> jedis.sendCommand(Protocol.Command.HSET, "1", "2", "3", "4"))
         .hasMessageContaining("wrong number of arguments");
   }
@@ -93,9 +96,9 @@
   @Test
   public void testHGetall_givenWrongNumberOfArguments() {
     assertThatThrownBy(() -> jedis.sendCommand(Protocol.Command.HGETALL))
-        .hasMessageContaining("wrong number of arguments");
+        .hasMessage("ERR wrong number of arguments for 'hgetall' command");
     assertThatThrownBy(() -> jedis.sendCommand(Protocol.Command.HMSET, "1", "2"))
-        .hasMessageContaining("wrong number of arguments");
+        .hasMessage("ERR wrong number of arguments for 'hmset' command");
   }
 
   @Test
@@ -167,7 +170,7 @@
     assertThatThrownBy(() -> {
       jedis.hdel("farm", "chicken");
     }).isInstanceOf(JedisDataException.class)
-        .hasMessageContaining("WRONGTYPE Operation against a key holding the wrong kind of value");
+        .hasMessage("WRONGTYPE " + RedisConstants.ERROR_WRONG_TYPE);
   }
 
   @Test
@@ -192,11 +195,11 @@
   public void testHStrLen_failsForNonHashes() {
     jedis.sadd("farm", "chicken");
     assertThatThrownBy(() -> jedis.hstrlen("farm", "chicken"))
-        .hasMessageContaining("WRONGTYPE");
+        .hasMessage("WRONGTYPE " + RedisConstants.ERROR_WRONG_TYPE);
 
     jedis.set("tractor", "John Deere");
     assertThatThrownBy(() -> jedis.hstrlen("tractor", "chicken"))
-        .hasMessageContaining("WRONGTYPE");
+        .hasMessage("WRONGTYPE " + RedisConstants.ERROR_WRONG_TYPE);
   }
 
   @Test
@@ -373,23 +376,41 @@
     String key = "HVals_key";
     String field1 = "field_1";
     String field2 = "field_2";
-    String value = "value";
+    String value1 = "value_1";
+    String value2 = "value_2";
 
-    List<String> list = jedis.hvals(key);
-    assertThat(list == null || list.isEmpty()).isTrue();
+    List<String> list = jedis.hvals("non-existent-key");
+    assertThat(list).isEmpty();
 
-    Long result = jedis.hset(key, field1, value);
+    Long result = jedis.hset(key, field1, value1);
     assertThat(result).isEqualTo(1);
 
-    result = jedis.hset(key, field2, value);
+    result = jedis.hset(key, field2, value2);
     assertThat(result).isEqualTo(1);
     list = jedis.hvals(key);
 
-    assertThat(list).isNotNull();
-    assertThat(list).isNotEmpty();
     assertThat(list).hasSize(2);
+    assertThat(list).contains(value1, value2);
+  }
 
-    assertThat(list).contains(value);
+  @Test
+  public void hvalsFailsForNonHash() {
+    jedis.sadd("farm", "chicken");
+    assertThatThrownBy(() -> jedis.hvals("farm"))
+        .hasMessage("WRONGTYPE " + RedisConstants.ERROR_WRONG_TYPE);
+
+    jedis.set("tractor", "John Deere");
+    assertThatThrownBy(() -> jedis.hvals("tractor"))
+        .hasMessage("WRONGTYPE " + RedisConstants.ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void hvalsFails_withIncorrectParameters() {
+    assertThatThrownBy(() -> jedis.sendCommand(Protocol.Command.HVALS))
+        .hasMessage("ERR wrong number of arguments for 'hvals' command");
+
+    assertThatThrownBy(() -> jedis.sendCommand(Protocol.Command.HVALS, "1", "too-many"))
+        .hasMessage("ERR wrong number of arguments for 'hvals' command");
   }
 
   /**
@@ -637,7 +658,7 @@
 
     assertThatThrownBy(
         () -> jedis.hset("key", "field", "something else")).isInstanceOf(JedisDataException.class)
-            .hasMessageContaining("WRONGTYPE");
+            .hasMessage("WRONGTYPE " + RedisConstants.ERROR_WRONG_TYPE);
   }
 
   @Test
diff --git a/geode-redis/src/main/java/org/apache/geode/redis/internal/RedisCommandType.java b/geode-redis/src/main/java/org/apache/geode/redis/internal/RedisCommandType.java
index beba759..044a1d1 100755
--- a/geode-redis/src/main/java/org/apache/geode/redis/internal/RedisCommandType.java
+++ b/geode-redis/src/main/java/org/apache/geode/redis/internal/RedisCommandType.java
@@ -160,6 +160,7 @@
       new MinimumParameterRequirements(4).and(new EvenParameterRequirements())),
   HSET(new HSetExecutor(), SUPPORTED,
       new MinimumParameterRequirements(4).and(new EvenParameterRequirements())),
+  HVALS(new HValsExecutor(), SUPPORTED, new ExactParameterRequirements(2)),
 
   /************* Sets *****************/
 
@@ -243,7 +244,6 @@
       new OddParameterRequirements(ERROR_SYNTAX)),
   HSETNX(new HSetNXExecutor(), UNSUPPORTED, new ExactParameterRequirements(4)),
   HSTRLEN(new HStrLenExecutor(), UNSUPPORTED, new ExactParameterRequirements(3)),
-  HVALS(new HValsExecutor(), UNSUPPORTED, new ExactParameterRequirements(2)),
 
   /***************************************
    **************** Sets *****************
diff --git a/geode-redis/src/test/java/org/apache/geode/redis/internal/SupportedCommandsJUnitTest.java b/geode-redis/src/test/java/org/apache/geode/redis/internal/SupportedCommandsJUnitTest.java
index 4474201..e0b05be 100644
--- a/geode-redis/src/test/java/org/apache/geode/redis/internal/SupportedCommandsJUnitTest.java
+++ b/geode-redis/src/test/java/org/apache/geode/redis/internal/SupportedCommandsJUnitTest.java
@@ -40,6 +40,7 @@
       "HGETALL",
       "HMSET",
       "HSET",
+      "HVALS",
       "KEYS",
       "PERSIST",
       "PEXPIRE",
@@ -85,7 +86,6 @@
       "HSCAN",
       "HSETNX",
       "HSTRLEN",
-      "HVALS",
       "INCR",
       "INCRBY",
       "INCRBYFLOAT",