MaintenancePushCodecTest.java

package redis.clients.jedis;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static redis.clients.jedis.MaintenancePushCodec.build;
import static redis.clients.jedis.MaintenancePushCodec.PushType.resolve;

import java.util.Arrays;
import java.util.Collections;

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import redis.clients.jedis.MaintenancePushCodec.PushType;
import redis.clients.jedis.util.SafeEncoder;

/**
 * Unit coverage for {@link MaintenancePushCodec}: {@link PushType#resolve} token classification and
 * {@link MaintenancePushCodec#build} field extraction / malformed-frame rejection.
 */
@Tag("unit")
public class MaintenancePushCodecTest {

  private static final byte[] SHARDS = bytes("[\"2\",\"4\"]");

  // resolve(): token -> PushType

  @Test
  public void resolveClassifiesMaintenanceTokens() {
    assertSame(PushType.MOVING, resolve(type("MOVING")));
    assertSame(PushType.MIGRATING, resolve(type("MIGRATING")));
    assertSame(PushType.MIGRATED, resolve(type("MIGRATED")));
    assertSame(PushType.FAILING_OVER, resolve(type("FAILING_OVER")));
    assertSame(PushType.FAILED_OVER, resolve(type("FAILED_OVER")));
  }

  @Test
  public void resolveReturnsNullForNonMaintenanceTokens() {
    assertNull(resolve(null));
    assertNull(resolve(type("message")));
    assertNull(resolve(type("invalidate")));
    assertNull(resolve(type("foobar"))); // length 6, not MOVING
    assertNull(resolve(new PushMessage(Collections.emptyList()).getType())); // empty frame
  }

  // build(): frame -> event field extraction

  @Test
  public void buildMoving() {
    MovingEvent m = assertInstanceOf(MovingEvent.class,
      build(PushType.MOVING, push(type("MOVING"), 30L, 15L, bytes("new-host:6380"))));
    assertEquals(30L, m.seq);
    assertEquals(15L, m.gracePeriodSeconds);
    assertEquals(new HostAndPort("new-host", 6380), m.target);
  }

  @Test
  public void buildMovingNoneTarget() { // null endpoint => 'none' type, no remap
    MovingEvent m = assertInstanceOf(MovingEvent.class,
      build(PushType.MOVING, push(type("MOVING"), 30L, 15L, null)));
    assertEquals(30L, m.seq);
    assertEquals(15L, m.gracePeriodSeconds);
    assertNull(m.target);
  }

  @Test
  public void buildMigrating() {
    MigratingEvent e = assertInstanceOf(MigratingEvent.class,
      build(PushType.MIGRATING, push(type("MIGRATING"), 6L, 2L, SHARDS)));
    assertEquals(6L, e.seq);
    assertEquals(2L, e.startsInSeconds);
    assertEquals("[\"2\",\"4\"]", e.shardIds);
  }

  @Test
  public void buildFailingOver() {
    FailingOverEvent e = assertInstanceOf(FailingOverEvent.class,
      build(PushType.FAILING_OVER, push(type("FAILING_OVER"), 6L, 2L, SHARDS)));
    assertEquals(6L, e.seq);
    assertEquals(2L, e.startsInSeconds);
    assertEquals("[\"2\",\"4\"]", e.shardIds);
  }

  @Test
  public void buildMigrated() {
    MigratedEvent e = assertInstanceOf(MigratedEvent.class,
      build(PushType.MIGRATED, push(type("MIGRATED"), 7L, SHARDS)));
    assertEquals(7L, e.seq);
    assertEquals("[\"2\",\"4\"]", e.shardIds);
  }

  @Test
  public void buildFailedOver() {
    FailedOverEvent e = assertInstanceOf(FailedOverEvent.class,
      build(PushType.FAILED_OVER, push(type("FAILED_OVER"), 7L, SHARDS)));
    assertEquals(7L, e.seq);
    assertEquals("[\"2\",\"4\"]", e.shardIds);
  }

  // build(): malformed frame -> throws MalformedMaintenanceEventException (logged by the consumer)

  @Test
  public void buildRejectsMalformedMoving() {
    // bad seq
    assertMalformed(PushType.MOVING, push(type("MOVING"), bytes("x"), 15L, bytes("h:1")));
    // bad time
    assertMalformed(PushType.MOVING, push(type("MOVING"), 30L, bytes("x"), bytes("h:1")));
    // missing time/target
    assertMalformed(PushType.MOVING, push(type("MOVING"), 30L));
    // missing target
    assertMalformed(PushType.MOVING, push(type("MOVING"), 30L, 15L));
    // target not byte[]
    assertMalformed(PushType.MOVING, push(type("MOVING"), 30L, 15L, 6379L));
    // unparseable host:port
    assertMalformed(PushType.MOVING, push(type("MOVING"), 30L, 15L, bytes("no-port")));
  }

  @Test
  public void buildRejectsMalformedMigrating() {
    assertMalformed(PushType.MIGRATING, push(type("MIGRATING"))); // no seq/time
    assertMalformed(PushType.MIGRATING, push(type("MIGRATING"), 6L)); // missing time
    assertMalformed(PushType.MIGRATING, push(type("MIGRATING"), 6L, bytes("x"))); // bad time
    assertMalformed(PushType.MIGRATING, push(type("MIGRATING"), bytes("x"), 2L)); // bad seq
    assertMalformed(PushType.MIGRATING, push(type("MIGRATING"), 6L, 2L)); // missing shards
  }

  @Test
  public void buildRejectsMalformedFailingOver() {
    assertMalformed(PushType.FAILING_OVER, push(type("FAILING_OVER"), bytes("x"), 2L)); // bad seq
    assertMalformed(PushType.FAILING_OVER, push(type("FAILING_OVER"), 6L)); // missing time
    assertMalformed(PushType.FAILING_OVER, push(type("FAILING_OVER"), 6L, 2L)); // missing shards
  }

  @Test
  public void buildRejectsMalformedMigrated() {
    assertMalformed(PushType.MIGRATED, push(type("MIGRATED"))); // no seq
    assertMalformed(PushType.MIGRATED, push(type("MIGRATED"), bytes("x"))); // bad seq
    assertMalformed(PushType.MIGRATED, push(type("MIGRATED"), 7L)); // missing shards
  }

  @Test
  public void buildRejectsMalformedFailedOver() {
    assertMalformed(PushType.FAILED_OVER, push(type("FAILED_OVER"))); // no seq
    assertMalformed(PushType.FAILED_OVER, push(type("FAILED_OVER"), bytes("x"))); // bad seq
    assertMalformed(PushType.FAILED_OVER, push(type("FAILED_OVER"), 7L)); // missing shards
  }

  private static void assertMalformed(PushType type, PushMessage msg) {
    assertThrows(MalformedMaintenanceEventException.class, () -> build(type, msg));
  }

  private static PushMessage push(Object... content) {
    return new PushMessage(Arrays.asList(content));
  }

  private static byte[] type(String t) {
    return SafeEncoder.encode(t);
  }

  private static byte[] bytes(String s) {
    return SafeEncoder.encode(s);
  }
}