MaintNotificationsConfigIT.java

package redis.clients.jedis.scenario;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import com.redis.test.fi.Database;
import com.redis.test.fi.FaultInjectorClient;
import com.redis.test.fi.StandaloneEffect;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import redis.clients.jedis.Builder;
import redis.clients.jedis.BuilderFactory;
import redis.clients.jedis.CommandArguments;
import redis.clients.jedis.CommandObject;
import redis.clients.jedis.Connection;
import redis.clients.jedis.MaintenanceNotificationsConfig;
import redis.clients.jedis.MaintenanceNotificationsConfig.EndpointType;
import redis.clients.jedis.MaintenanceNotificationsConfig.Mode;
import redis.clients.jedis.Protocol;
import redis.clients.jedis.RedisClient;
import redis.clients.jedis.util.KeyValue;

/**
 * T.3 Configuration & Handshake ��� verifies the maintenance-notifications handshake against the
 * server via CLIENT MAINT_NOTIFICATIONS_INFO: enablement (T.3.1) and the requested endpoint type
 * (T.3.2). These tests fire no fault-injector effect and mutate nothing, so one database serves the
 * whole class and only the client is rebuilt per configuration.
 */
@Tag("scenario")
public class MaintNotificationsConfigIT {

  // Referencing the scenario base's static field also runs its DNS-cache TTL static block before
  // the first FI/endpoint name lookup.
  private static final FaultInjectorClient faultInjector = FaultInjectorScenarioBase.faultInjector;

  private static final String STATUS_ON = "on";
  private static final String STATUS_OFF = "off";

  /** The moving-endpoint-type value the server reports for each requested {@link EndpointType}. */
  private static final Map<EndpointType, String> EXPECTED_ENDPOINT_TYPE = new LinkedHashMap<>();
  static {
    EXPECTED_ENDPOINT_TYPE.put(EndpointType.EXTERNAL_IP, "external-ip");
    EXPECTED_ENDPOINT_TYPE.put(EndpointType.INTERNAL_IP, "internal-ip");
    EXPECTED_ENDPOINT_TYPE.put(EndpointType.EXTERNAL_FQDN, "external-fqdn");
    EXPECTED_ENDPOINT_TYPE.put(EndpointType.INTERNAL_FQDN, "internal-fqdn");
    EXPECTED_ENDPOINT_TYPE.put(EndpointType.NONE, "none");
  }

  private static Database shared;

  /**
   * Creates the single shared database. The trigger is arbitrary ��� no effect fires ��� but the
   * dbconfig comes from a fresh discovery so the database gets its own endpoint name, as elsewhere.
   */
  @BeforeAll
  static void createSharedDatabase() {
    Map<String, Object> dbConfig = faultInjector.getStandaloneTriggers(StandaloneEffect.CONN_DROP)
        .trigger("endpoint_rebind").requirement("single").dbConfig();
    // assign before any further step so @AfterAll cleans up even on a later failure
    shared = faultInjector.createDatabase(dbConfig);
    FaultInjectorScenarioBase.awaitEndpointConnectable(shared.endpoint());
  }

  @AfterAll
  static void deleteSharedDatabase() {
    if (shared != null) {
      faultInjector.deleteDatabase(shared.id());
      shared = null;
    }
  }

  /** T.3.1 ��� the default (AUTO) handshake enables notifications; the server reports them active. */
  @Test
  @Timeout(30)
  void enabledActivatesNotificationsOnServer() {
    MaintenanceNotificationsConfig config = MaintenanceNotificationsConfig.builder().mode(Mode.AUTO)
        .build();
    try (RedisClient client = buildClient(config);
        Connection connection = client.getPool().getResource()) {
      MaintNotificationsInfo info = connection.executeCommand(maintNotificationsInfo());
      assertEquals(STATUS_ON, info.status(),
        "AUTO handshake must enable maintenance notifications on the server");
    }
  }

  /** T.3.1 ��� DISABLED skips the handshake; the server reports notifications inactive. */
  @Test
  @Timeout(30)
  void disabledDoesNotActivateNotificationsOnServer() {
    try (RedisClient client = buildClient(MaintenanceNotificationsConfig.DISABLED);
        Connection connection = client.getPool().getResource()) {
      MaintNotificationsInfo info = connection.executeCommand(maintNotificationsInfo());
      assertEquals(STATUS_OFF, info.status(),
        "DISABLED must not enable maintenance notifications on the server");
    }
  }

  static Stream<EndpointType> configuredEndpointTypes() {
    return EXPECTED_ENDPOINT_TYPE.keySet().stream();
  }

  /** T.3.2 ��� the requested endpoint type is the one the server records for the connection. */
  @ParameterizedTest(name = "endpoint-type {0}")
  @MethodSource("configuredEndpointTypes")
  @Timeout(30)
  void endpointTypeAcknowledgedByServer(EndpointType endpointType) {
    MaintenanceNotificationsConfig config = MaintenanceNotificationsConfig.builder()
        .mode(Mode.ENABLED).endpointType(endpointType).build();
    try (RedisClient client = buildClient(config);
        Connection connection = client.getPool().getResource()) {
      MaintNotificationsInfo info = connection.executeCommand(maintNotificationsInfo());
      assertEquals(STATUS_ON, info.status(),
        "requesting an endpoint type must enable notifications");
      assertEquals(EXPECTED_ENDPOINT_TYPE.get(endpointType), info.movingEndpointType(),
        "server must record the requested moving-endpoint-type");
    }
  }

  private static RedisClient buildClient(MaintenanceNotificationsConfig maintenance) {
    return RedisClient.builder().hostAndPort(shared.host(), shared.port())
        .clientConfig(shared.clientConfig().build()).maintenanceNotifications(maintenance).build();
  }

  /**
   * CLIENT MAINT_NOTIFICATIONS_INFO as a typed command, mirroring the client's own CommandObjects:
   * the reply is decoded by a dedicated builder into a {@link MaintNotificationsInfo}.
   */
  static CommandObject<MaintNotificationsInfo> maintNotificationsInfo() {
    return new CommandObject<>(
        new CommandArguments(Protocol.Command.CLIENT).add("MAINT_NOTIFICATIONS_INFO"),
        MaintNotificationsInfo.BUILDER);
  }

  /**
   * Parsed CLIENT MAINT_NOTIFICATIONS_INFO reply: the enablement status and the negotiated
   * moving-endpoint-type. The RESP3 reply is a map whose {@code parameters} entry is a nested map.
   */
  static final class MaintNotificationsInfo {

    private static final String FIELD_STATUS = "status";
    private static final String FIELD_PARAMETERS = "parameters";
    private static final String PARAM_MOVING_ENDPOINT_TYPE = "moving-endpoint-type";

    static final Builder<MaintNotificationsInfo> BUILDER = new Builder<MaintNotificationsInfo>() {
      @Override
      public MaintNotificationsInfo build(Object data) {
        return new MaintNotificationsInfo(BuilderFactory.ENCODED_OBJECT_MAP.build(data));
      }
    };

    private final String status;
    private final String movingEndpointType;

    private MaintNotificationsInfo(Map<String, Object> reply) {
      this.status = (String) reply.get(FIELD_STATUS);
      this.movingEndpointType = movingEndpointType(reply);
    }

    /** {@code on} or {@code off}. */
    String status() {
      return status;
    }

    /** The negotiated moving-endpoint-type, or null when notifications are disabled. */
    String movingEndpointType() {
      return movingEndpointType;
    }

    private static String movingEndpointType(Map<String, Object> reply) {
      Object parameters = reply.get(FIELD_PARAMETERS);
      if (parameters instanceof List) {
        for (Object entry : (List<?>) parameters) {
          KeyValue<?, ?> kv = (KeyValue<?, ?>) entry;
          if (PARAM_MOVING_ENDPOINT_TYPE.equals(kv.getKey())) {
            return (String) kv.getValue();
          }
        }
      }
      return null;
    }

    @Override
    public String toString() {
      return "status=" + status + ", moving-endpoint-type=" + movingEndpointType;
    }
  }
}