RedisClientTransactionIT.java

package redis.clients.jedis.commands.unified.client;

import io.redis.test.annotations.ConditionalOnEnv;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.MethodSource;
import redis.clients.jedis.AbstractTransaction;
import redis.clients.jedis.RedisProtocol;
import redis.clients.jedis.Response;
import redis.clients.jedis.UnifiedJedis;
import redis.clients.jedis.commands.unified.UnifiedJedisCommandsTestBase;
import redis.clients.jedis.exceptions.JedisDataException;
import redis.clients.jedis.util.TestEnvUtil;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

@ParameterizedClass
@MethodSource("redis.clients.jedis.commands.CommandsTestsParameters#respVersions")
public class RedisClientTransactionIT extends UnifiedJedisCommandsTestBase {

  public RedisClientTransactionIT(RedisProtocol protocol) {
    super(protocol);
  }

  @Override
  protected UnifiedJedis createTestClient() {
    return RedisClientCommandsTestHelper.getClient(protocol);
  }

  @BeforeEach
  public void setUp() {
    RedisClientCommandsTestHelper.clearData();
  }

  @Test
  @ConditionalOnEnv(value = TestEnvUtil.ENV_REDIS_ENTERPRISE, enabled = false)
  public void transaction() {
    final int count = 10;
    int totalCount = 0;
    for (int i = 0; i < count; i++) {
      jedis.set("foo" + i, "bar" + i);
    }
    totalCount += count;
    for (int i = 0; i < count; i++) {
      jedis.rpush("foobar" + i, "foo" + i, "bar" + i);
    }
    totalCount += count;

    List<Object> responses;
    List<Object> expected = new ArrayList<>(totalCount);

    try (AbstractTransaction transaction = jedis.multi()) {
      for (int i = 0; i < count; i++) {
        transaction.get("foo" + i);
        expected.add("bar" + i);
      }
      for (int i = 0; i < count; i++) {
        transaction.lrange("foobar" + i, 0, -1);
        expected.add(Arrays.asList("foo" + i, "bar" + i));
      }
      responses = transaction.exec();
    }

    for (int i = 0; i < totalCount; i++) {
      assertEquals(expected.get(i), responses.get(i));
    }
  }

  @Test
  @ConditionalOnEnv(value = TestEnvUtil.ENV_REDIS_ENTERPRISE, enabled = false)
  public void watch() {
    try (AbstractTransaction tx = jedis.transaction(false)) {
      assertEquals("OK", tx.watch("mykey", "somekey"));
      tx.multi();

      jedis.set("mykey", "bar");

      tx.set("mykey", "foo");
      assertNull(tx.exec());

      assertEquals("bar", jedis.get("mykey"));
    }
  }

  /**
   * Verify manual multi and commands sent before sending multi does not cause out of order
   * responses
   */
  @Test
  public void transactionManualWithCommandsBeforeMulti() {

    try (AbstractTransaction tx = jedis.transaction(false)) {

      // command before multi
      Response<String> txSetBeforeMulti = tx.set("mykey", "before_multi");
      Response<String> txGetBeforeMulti = tx.get("mykey");
      assertEquals("OK", txSetBeforeMulti.get());
      assertEquals("before_multi", txGetBeforeMulti.get());

      tx.multi();
      Response<String> txSet = tx.set("mykey", "foo");
      Response<String> txGet = tx.get("mykey");
      List<Object> txResp = tx.exec();

      assertEquals("OK", txSet.get());
      assertEquals("foo", txGet.get());
      assertEquals(2, txResp.size());
      assertEquals("OK", txResp.get(0));
      assertEquals("foo", txResp.get(1));
    }
  }

  @Test
  public void publishInTransaction() {
    try (AbstractTransaction tx = jedis.multi()) {
      Response<Long> p1 = tx.publish("foo", "bar");
      Response<Long> p2 = tx.publish("foo".getBytes(), "bar".getBytes());
      tx.exec();

      assertEquals(0, p1.get().longValue());
      assertEquals(0, p2.get().longValue());
    }
  }

  @Nested
  class ResponseHandlingIT {

    @BeforeEach
    public void setUp() {
      RedisClientCommandsTestHelper.clearData();
    }

    @Test
    public void notInTransactionResponseReturnsExpectedValue() {
      // Commands executed before multi() should return immediate responses
      try (AbstractTransaction tx = jedis.transaction(false)) {
        Response<String> setResponse = tx.set("key1", "value1");
        Response<String> getResponse = tx.get("key1");

        // Responses should be available immediately (not in transaction yet)
        assertEquals("OK", setResponse.get());
        assertEquals("value1", getResponse.get());
      }
    }

    @Test
    public void notInTransactionResponsePropagatesException() {
      // Commands executed before multi() that fail should propagate exceptions
      try (AbstractTransaction tx = jedis.transaction(false)) {
        Response<String> setResponse = tx.set("key1", "not_a_number");
        Response<Long> incrResponse = tx.incr("key1");

        // Set should succeed
        assertEquals("OK", setResponse.get());

        // Incr should fail and propagate exception immediately
        JedisDataException ex = assertThrows(JedisDataException.class, incrResponse::get);
        assertEquals("ERR value is not an integer or out of range", ex.getMessage());
      }
    }

    @Test
    public void inTransactionResponseThrowsBeforeExec() {
      // Calling response.get() before exec() should throw IllegalStateException
      try (AbstractTransaction tx = jedis.multi()) {
        Response<String> response = tx.set("key1", "value1");

        // Attempting to get response before exec() should throw
        IllegalStateException ex = assertThrows(IllegalStateException.class, response::get);
        assertTrue(ex.getMessage()
            .contains("Please close pipeline or multi block before calling this method"));

        // Now exec the transaction
        tx.exec();

        // After exec, response should be available
        assertEquals("OK", response.get());
      }
    }

    @Test
    public void afterExecResponseContainsActualResults() {
      // After exec(), Response objects should contain actual results from Redis
      try (AbstractTransaction tx = jedis.multi()) {
        Response<String> setResponse = tx.set("key1", "value1");
        Response<String> getResponse = tx.get("key1");

        tx.exec();

        // Verify Response objects contain correct values
        assertEquals("OK", setResponse.get());
        assertEquals("value1", getResponse.get());
      }
    }

    @Test
    public void execReturnsListWithAllResultsInOrder() {
      // exec() should return List<Object> with all command results in order
      try (AbstractTransaction tx = jedis.multi()) {
        tx.set("key1", "value1");
        tx.get("key1");
        tx.del("key1");

        List<Object> results = tx.exec();

        // Verify all results are in the correct order
        assertEquals(3, results.size());
        assertEquals("OK", results.get(0)); // set key1
        assertEquals("value1", results.get(1)); // get key1
        assertEquals(1L, results.get(2)); // del key1
      }
    }
  }
}