content: /*
   * 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.cassandra.db.guardrails;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.HashSet;
  import java.util.LinkedHashSet;
  import java.util.List;
  import java.util.Set;
  import java.util.function.Consumer;
  
  import org.junit.Assert;
  import org.junit.Test;
  
  import static java.lang.String.format;
  import static org.junit.Assert.assertEquals;
  import static org.junit.Assert.assertFalse;
  import static org.junit.Assert.assertTrue;
  
  public class GuardrailsTest extends GuardrailTester
  {
      public static final int DISABLED = -1;
      public static final String REASON = "Testing";
      public static final String FEATURE = "Feature name";
  
      private void testDisabledThreshold(Threshold guard) throws Throwable
      {
          assertFalse(guard.enabled(userClientState));
  
          for (boolean containsUserData : Arrays.asList(true, false))
          {
              assertValid(() -> guard.guard(5, "Z", containsUserData, null));
              assertValid(() -> guard.guard(25, "A", containsUserData, userClientState));
              assertValid(() -> guard.guard(100, "B", containsUserData, userClientState));
              assertValid(() -> guard.guard(101, "X", containsUserData, userClientState));
              assertValid(() -> guard.guard(200, "Y", containsUserData, userClientState));
          }
      }
  
      @Test
      public void testDisabledMaxThreshold() throws Throwable
      {
          Threshold.ErrorMessageProvider errorMessageProvider = (isWarn, what, v, t) -> "Should never trigger";
          testDisabledThreshold(new MaxThreshold("x", REASON, state -> DISABLED, state -> DISABLED, errorMessageProvider));
      }
  
      @Test
      public void testMaxThreshold() throws Throwable
      {
          MaxThreshold guard = new MaxThreshold("x",
                                                REASON,
                                                state -> 10,
                                                state -> 100,
                                                (isWarn, featureName, v, t) -> format("%s: for %s, %s > %s",
                                                                               isWarn ? "Warning" : "Aborting", featureName, v, t));
  
          assertTrue(guard.enabled(userClientState));
  
          assertValid(() -> guard.guard(5, "Z", false, userClientState));
          assertWarns(() -> guard.guard(25, "A", false, userClientState), "Warning: for A, 25 > 10");
          assertWarns(() -> guard.guard(100, "B", false, userClientState), "Warning: for B, 100 > 10");
          assertFails(() -> guard.guard(101, "X", false, userClientState), "Aborting: for X, 101 > 100");
          assertFails(() -> guard.guard(200, "Y", false, userClientState), "Aborting: for Y, 200 > 100");
          assertValid(() -> guard.guard(5, "Z", false, userClientState));
  
          assertValid(() -> guard.guard(5, "Z", true, userClientState));
          assertWarns(() -> guard.guard(25, "A", true, userClientState), "Warning: for A, 25 > 10", "Warning: for <redacted>, 25 > 10");
          assertWarns(() -> guard.guard(100, "B", true, userClientState), "Warning: for B, 100 > 10", "Warning: for <redacted>, 100 > 10");
          assertFails(() -> guard.guard(101, "X", true, userClientState), "Aborting: for X, 101 > 100", "Aborting: for <redacted>, 101 > 100");
          assertFails(() -> guard.guard(200, "Y", true, userClientState), "Aborting: for Y, 200 > 100", "Aborting: for <redacted>, 200 > 100");
          assertValid(() -> guard.guard(5, "Z", true, userClientState));
      }
  
      @Test
      public void testWarnOnlyMaxThreshold() throws Throwable
      {
          MaxThreshold guard = new MaxThreshold("x",
                                                REASON,
                                                state -> 10,
                                                state -> DISABLED,
                                                (isWarn, featureName, v, t) -> format("%s: for %s, %s > %s",
                                                                               isWarn ? "Warning" : "Aborting", featureName, v, t));
  
          assertTrue(guard.enabled(userClientState));
  
          assertValid(() -> guard.guard(5, "Z", false, userClientState));
          assertWarns(() -> guard.guard(11, "A", false, userClientState), "Warning: for A, 11 > 10");
  
          assertValid(() -> guard.guard(5, "Z", true, userClientState));
          assertWarns(() -> guard.guard(11, "A", true, userClientState), "Warning: for A, 11 > 10", "Warning: for <redacted>, 11 > 10");
      }
  
      @Test
      public void testFailOnlyMaxThreshold() throws Throwable
      {
          MaxThreshold guard = new MaxThreshold("x",
                                                REASON,
                                                state -> DISABLED,
                                                state -> 10,
                                                (isWarn, featureName, v, t) -> format("%s: for %s, %s > %s",
                                                                               isWarn ? "Warning" : "Aborting", featureName, v, t));
  
          assertTrue(guard.enabled(userClientState));
  
          assertValid(() -> guard.guard(5, "Z", false, userClientState));
          assertFails(() -> guard.guard(11, "A", false, userClientState), "Aborting: for A, 11 > 10");
  
          assertValid(() -> guard.guard(5, "Z", true, userClientState));
          assertFails(() -> guard.guard(11, "A", true, userClientState), "Aborting: for A, 11 > 10", "Aborting: for <redacted>, 11 > 10");
      }
  
      @Test
      public void testMaxThresholdUsers() throws Throwable
      {
          MaxThreshold guard = new MaxThreshold("x",
                                                REASON,
                                                state -> 10,
                                                state -> 100,
                                                (isWarn, featureName, v, t) -> format("%s: for %s, %s > %s",
                                                                               isWarn ? "Warning" : "Failure", featureName, v, t));
  
          // value under both thresholds
          assertValid(() -> guard.guard(5, "x", false, null));
          assertValid(() -> guard.guard(5, "x", false, userClientState));
          assertValid(() -> guard.guard(5, "x", false, systemClientState));
          assertValid(() -> guard.guard(5, "x", false, superClientState));
  
          // value over warning threshold
          assertWarns(() -> guard.guard(100, "y", false, null), "Warning: for y, 100 > 10");
          assertWarns(() -> guard.guard(100, "y", false, userClientState), "Warning: for y, 100 > 10");
          assertValid(() -> guard.guard(100, "y", false, systemClientState));
          assertValid(() -> guard.guard(100, "y", false, superClientState));
  
          // value over fail threshold. An undefined user means that the check comes from a background process, so we
          // still emit failure messages and events, but we don't throw an exception to prevent interrupting that process.
          assertFails(() -> guard.guard(101, "z", false, null), false, "Failure: for z, 101 > 100");
          assertFails(() -> guard.guard(101, "z", false, userClientState), "Failure: for z, 101 > 100");
          assertValid(() -> guard.guard(101, "z", false, systemClientState));
          assertValid(() -> guard.guard(101, "z", false, superClientState));
      }
  
      @Test
      public void testDisabledMinThreshold() throws Throwable
      {
          Threshold.ErrorMessageProvider errorMessageProvider = (isWarn, what, v, t) -> "Should never trigger";
          testDisabledThreshold(new MinThreshold("x", REASON, state -> DISABLED, state -> DISABLED, errorMessageProvider));
      }
  
      @Test
      public void testMinThreshold() throws Throwable
      {
          MinThreshold guard = new MinThreshold("x",
                                                REASON,
                                                state -> 100,
                                                state -> 10,
                                                (isWarn, what, v, t) -> format("%s: for %s, %s < %s",
                                                                               isWarn ? "Warning" : "Aborting", what, v, t));
  
          assertTrue(guard.enabled(userClientState));
  
          assertValid(() -> guard.guard(200, "Z", false, userClientState));
          assertWarns(() -> guard.guard(25, "A", false, userClientState), "Warning: for A, 25 < 100");
          assertWarns(() -> guard.guard(10, "B", false, userClientState), "Warning: for B, 10 < 100");
          assertFails(() -> guard.guard(9, "X", false, userClientState), "Aborting: for X, 9 < 10");
          assertFails(() -> guard.guard(1, "Y", false, userClientState), "Aborting: for Y, 1 < 10");
          assertValid(() -> guard.guard(200, "Z", false, userClientState));
  
          assertValid(() -> guard.guard(200, "Z", true, userClientState));
          assertWarns(() -> guard.guard(25, "A", true, userClientState), "Warning: for A, 25 < 100", "Warning: for <redacted>, 25 < 100");
          assertWarns(() -> guard.guard(10, "B", true, userClientState), "Warning: for B, 10 < 100", "Warning: for <redacted>, 10 < 100");
          assertFails(() -> guard.guard(9, "X", true, userClientState), "Aborting: for X, 9 < 10", "Aborting: for <redacted>, 9 < 10");
          assertFails(() -> guard.guard(1, "Y", true, userClientState), "Aborting: for Y, 1 < 10", "Aborting: for <redacted>, 1 < 10");
          assertValid(() -> guard.guard(200, "Z", true, userClientState));
      }
  
      @Test
      public void testWarnOnlyMinThreshold() throws Throwable
      {
          MinThreshold guard = new MinThreshold("x",
                                                REASON,
                                                state -> 10,
                                                state -> DISABLED,
                                                (isWarn, what, v, t) -> format("%s: for %s, %s < %s",
                                                                               isWarn ? "Warning" : "Aborting", what, v, t));
  
          assertTrue(guard.enabled(userClientState));
  
          assertValid(() -> guard.guard(11, "Z", false, userClientState));
          assertWarns(() -> guard.guard(5, "A", false, userClientState), "Warning: for A, 5 < 10");
  
          assertValid(() -> guard.guard(11, "Z", true, userClientState));
          assertWarns(() -> guard.guard(5, "A", true, userClientState), "Warning: for A, 5 < 10", "Warning: for <redacted>, 5 < 10");
      }
  
      @Test
      public void testFailOnlyMinThreshold() throws Throwable
      {
          MinThreshold guard = new MinThreshold("x",
                                                REASON,
                                                state -> DISABLED,
                                                state -> 10,
                                                (isWarn, what, v, t) -> format("%s: for %s, %s < %s",
                                                                               isWarn ? "Warning" : "Aborting", what, v, t));
  
          assertTrue(guard.enabled(userClientState));
  
          assertValid(() -> guard.guard(11, "Z", false, userClientState));
          assertFails(() -> guard.guard(5, "A", false, userClientState), "Aborting: for A, 5 < 10");
  
          assertValid(() -> guard.guard(11, "Z", true, userClientState));
          assertFails(() -> guard.guard(5, "A", true, userClientState), "Aborting: for A, 5 < 10", "Aborting: for <redacted>, 5 < 10");
      }
  
      @Test
      public void testMinThresholdUsers() throws Throwable
      {
          MinThreshold guard = new MinThreshold("x",
                                                REASON,
                                                state -> 100,
                                                state -> 10,
                                                (isWarn, what, v, t) -> format("%s: for %s, %s < %s",
                                                                               isWarn ? "Warning" : "Failure", what, v, t));
  
          // value above both thresholds
          assertValid(() -> guard.guard(200, "x", false, null));
          assertValid(() -> guard.guard(200, "x", false, userClientState));
          assertValid(() -> guard.guard(200, "x", false, systemClientState));
          assertValid(() -> guard.guard(200, "x", false, superClientState));
  
          // value under warning threshold
          assertWarns(() -> guard.guard(10, "y", false, null), "Warning: for y, 10 < 100");
          assertWarns(() -> guard.guard(10, "y", false, userClientState), "Warning: for y, 10 < 100");
          assertValid(() -> guard.guard(10, "y", false, systemClientState));
          assertValid(() -> guard.guard(10, "y", false, superClientState));
  
          // value under fail threshold. An undefined user means that the check comes from a background process, so we
          // still emit failure messages and events, but we don't throw an exception to prevent interrupting that process.
          assertFails(() -> guard.guard(9, "z", false, null), false, "Failure: for z, 9 < 10");
          assertFails(() -> guard.guard(9, "z", false, userClientState), "Failure: for z, 9 < 10");
          assertValid(() -> guard.guard(9, "z", false, systemClientState));
          assertValid(() -> guard.guard(9, "z", false, superClientState));
      }
  
      @Test
      public void testEnableFlag() throws Throwable
      {
          assertFails(() -> new EnableFlag("x", REASON, state -> false, "X").ensureEnabled(userClientState), "X is not allowed");
          assertValid(() -> new EnableFlag("x", REASON, state -> true, "X").ensureEnabled(userClientState));
  
          assertFails(() -> new EnableFlag("x", REASON, state -> false, "X").ensureEnabled("Y", userClientState), "Y is not allowed");
          assertValid(() -> new EnableFlag("x", REASON, state -> true, "X").ensureEnabled("Y", userClientState));
      }
  
      @Test
      public void testEnableFlagUsers() throws Throwable
      {
          EnableFlag enabled = new EnableFlag("x", REASON, state -> true, "X");
          assertValid(() -> enabled.ensureEnabled(null));
          assertValid(() -> enabled.ensureEnabled(userClientState));
          assertValid(() -> enabled.ensureEnabled(systemClientState));
          assertValid(() -> enabled.ensureEnabled(superClientState));
  
          EnableFlag disabled = new EnableFlag("x", REASON, state -> false, "X");
          assertFails(() -> disabled.ensureEnabled(userClientState), "X is not allowed");
          assertValid(() -> disabled.ensureEnabled(systemClientState));
          assertValid(() -> disabled.ensureEnabled(superClientState));
      }
  
      @Test
      public void testEnableFlagWarn() throws Throwable
      {
          EnableFlag disabledGuard = new EnableFlag("x", null, state -> true, state -> false, FEATURE);
  
          assertFails(() -> disabledGuard.ensureEnabled(null), false, FEATURE + " is not allowed");
          assertFails(() -> disabledGuard.ensureEnabled(userClientState), FEATURE + " is not allowed");
          assertValid(() -> disabledGuard.ensureEnabled(systemClientState));
          assertValid(() -> disabledGuard.ensureEnabled(superClientState));
  
          EnableFlag enabledGuard = new EnableFlag("x", null, state -> true, state -> true, FEATURE);
          assertWarns(() -> enabledGuard.ensureEnabled(null), FEATURE + " is not recommended");
          assertWarns(() -> enabledGuard.ensureEnabled(userClientState), FEATURE + " is not recommended");
          assertValid(() -> enabledGuard.ensureEnabled(systemClientState));
          assertValid(() -> enabledGuard.ensureEnabled(superClientState));
      }
  
      @Test
      public void testValuesWarned() throws Throwable
      {
          // Using a sorted set below to ensure the order in the warning message checked below is not random
          Values<Integer> warned = new Values<>("x",
                                                REASON,
                                                state -> insertionOrderedSet(4, 6, 20),
                                                state -> Collections.emptySet(),
                                                state -> Collections.emptySet(),
                                                "integer");
  
          Consumer<Integer> action = i -> Assert.fail("The ignore action shouldn't have been triggered");
          assertValid(() -> warned.guard(set(3), action, userClientState));
          assertWarns(() -> warned.guard(set(4), action, userClientState),
                      "Provided values [4] are not recommended for integer (warned values are: [4, 6, 20])");
          assertWarns(() -> warned.guard(set(4, 6), action, null),
                      "Provided values [4, 6] are not recommended for integer (warned values are: [4, 6, 20])");
          assertWarns(() -> warned.guard(set(4, 5, 6, 7), action, null),
                      "Provided values [4, 6] are not recommended for integer (warned values are: [4, 6, 20])");
      }
  
      @Test
      public void testValuesIgnored() throws Throwable
      {
          // Using a sorted set below to ensure the order in the error message checked below are not random
          Values<Integer> ignored = new Values<>("x",
                                                 REASON,
                                                 state -> Collections.emptySet(),
                                                 state -> insertionOrderedSet(4, 6, 20),
                                                 state -> Collections.emptySet(),
                                                 "integer");
  
          Set<Integer> triggeredOn = set();
          assertValid(() -> ignored.guard(set(3), triggeredOn::add, userClientState));
          assertEquals(set(), triggeredOn);
  
          assertWarns(() -> ignored.guard(set(4), triggeredOn::add, userClientState),
                      "Ignoring provided values [4] as they are not supported for integer (ignored values are: [4, 6, 20])");
          assertEquals(set(4), triggeredOn);
          triggeredOn.clear();
  
          assertWarns(() -> ignored.guard(set(4, 6), triggeredOn::add, null),
                      "Ignoring provided values [4, 6] as they are not supported for integer (ignored values are: [4, 6, 20])");
          assertEquals(set(4, 6), triggeredOn);
          triggeredOn.clear();
  
          assertWarns(() -> ignored.guard(set(4, 5, 6, 7), triggeredOn::add, null),
                      "Ignoring provided values [4, 6] as they are not supported for integer (ignored values are: [4, 6, 20])");
          assertEquals(set(4, 6), triggeredOn);
          triggeredOn.clear();
  
          assertThrows(() -> ignored.guard(set(4), userClientState),
                       AssertionError.class,
                       "There isn't an ignore action for integer, but value 4 is setup to be ignored");
      }
  
      @Test
      public void testValuesDisallowed() throws Throwable
      {
          // Using a sorted set below to ensure the order in the error message checked below are not random
          Values<Integer> disallowed = new Values<>("x",
                                                    REASON,
                                                    state -> Collections.emptySet(),
                                                    state -> Collections.emptySet(),
                                                    state -> insertionOrderedSet(4, 6, 20),
                                                    "integer");
  
          Consumer<Integer> action = i -> Assert.fail("The ignore action shouldn't have been triggered");
          assertValid(() -> disallowed.guard(set(3), action, userClientState));
          assertFails(() -> disallowed.guard(set(4), action, userClientState),
                      "Provided values [4] are not allowed for integer (disallowed values are: [4, 6, 20])");
          assertValid(() -> disallowed.guard(set(10), action, userClientState));
          assertFails(() -> disallowed.guard(set(20), action, userClientState),
                      "Provided values [20] are not allowed for integer (disallowed values are: [4, 6, 20])");
          assertValid(() -> disallowed.guard(set(200), action, userClientState));
          assertValid(() -> disallowed.guard(set(1, 2, 3), action, userClientState));
  
          assertFails(() -> disallowed.guard(set(4, 6), action, null), false,
                      "Provided values [4, 6] are not allowed for integer (disallowed values are: [4, 6, 20])");
          assertFails(() -> disallowed.guard(set(4, 5, 6, 7), action, null), false,
                      "Provided values [4, 6] are not allowed for integer (disallowed values are: [4, 6, 20])");
      }
  
      @Test
      public void testValuesUsers() throws Throwable
      {
          Values<Integer> disallowed = new Values<>("x",
                                                    REASON,
                                                    state -> Collections.singleton(2),
                                                    state -> Collections.singleton(3),
                                                    state -> Collections.singleton(4),
                                                    "integer");
  
          Consumer<Integer> action = i -> Assert.fail("The ignore action shouldn't have been triggered");
  
          assertValid(() -> disallowed.guard(set(1), action, null));
          assertValid(() -> disallowed.guard(set(1), action, userClientState));
          assertValid(() -> disallowed.guard(set(1), action, systemClientState));
          assertValid(() -> disallowed.guard(set(1), action, superClientState));
  
          String message = "Provided values [2] are not recommended for integer (warned values are: [2])";
          assertWarns(() -> disallowed.guard(set(2), action, null), message);
          assertWarns(() -> disallowed.guard(set(2), action, userClientState), message);
          assertValid(() -> disallowed.guard(set(2), action, systemClientState));
          assertValid(() -> disallowed.guard(set(2), action, superClientState));
  
          message = "Ignoring provided values [3] as they are not supported for integer (ignored values are: [3])";
          List<Integer> triggeredOn = new ArrayList<>();
          assertWarns(() -> disallowed.guard(set(3), triggeredOn::add, null), message);
          assertWarns(() -> disallowed.guard(set(3), triggeredOn::add, userClientState), message);
          assertValid(() -> disallowed.guard(set(3), triggeredOn::add, systemClientState));
          assertValid(() -> disallowed.guard(set(3), triggeredOn::add, superClientState));
          Assert.assertEquals(list(3, 3), triggeredOn);
  
          message = "Provided values [4] are not allowed for integer (disallowed values are: [4])";
          assertFails(() -> disallowed.guard(set(4), action, null), false, message);
          assertFails(() -> disallowed.guard(set(4), action, userClientState), message);
          assertValid(() -> disallowed.guard(set(4), action, systemClientState));
          assertValid(() -> disallowed.guard(set(4), action, superClientState));
      }
  
      @Test
      public void testPredicates() throws Throwable
      {
          Predicates<Integer> guard = new Predicates<>("x",
                                                       REASON,
                                                       state -> x -> x > 10,
                                                       state -> x -> x > 100,
                                                       (isWarn, value) -> format("%s: %s", isWarn ? "Warning" : "Aborting", value));
  
          assertValid(() -> guard.guard(5, userClientState));
          assertWarns(() -> guard.guard(25, userClientState), "Warning: 25");
          assertWarns(() -> guard.guard(100,  userClientState), "Warning: 100");
          assertFails(() -> guard.guard(101,  userClientState), "Aborting: 101");
          assertFails(() -> guard.guard(200,  userClientState), "Aborting: 200");
          assertValid(() -> guard.guard(5,  userClientState));
      }
  
      @Test
      public void testPredicatesUsers() throws Throwable
      {
          Predicates<Integer> guard = new Predicates<>("x",
                                                       REASON,
                                                       state -> x -> x > 10,
                                                       state -> x -> x > 100,
                                                       (isWarn, value) -> format("%s: %s", isWarn ? "Warning" : "Aborting", value));
  
          assertTrue(guard.enabled());
          assertTrue(guard.enabled(null));
          assertTrue(guard.enabled(userClientState));
          assertFalse(guard.enabled(systemClientState));
          assertFalse(guard.enabled(superClientState));
  
          assertValid(() -> guard.guard(5, null));
          assertValid(() -> guard.guard(5, userClientState));
          assertValid(() -> guard.guard(5, systemClientState));
          assertValid(() -> guard.guard(5, superClientState));
  
          assertWarns(() -> guard.guard(25, null), "Warning: 25");
          assertWarns(() -> guard.guard(25, userClientState), "Warning: 25");
          assertValid(() -> guard.guard(25, systemClientState));
          assertValid(() -> guard.guard(25, superClientState));
  
          assertFails(() -> guard.guard(101,  null), false, "Aborting: 101");
          assertFails(() -> guard.guard(101,  userClientState), "Aborting: 101");
          assertValid(() -> guard.guard(101, systemClientState));
          assertValid(() -> guard.guard(101, superClientState));
      }
  
      private static Set<Integer> set(Integer value)
      {
          return Collections.singleton(value);
      }
  
      private static Set<Integer> set(Integer... values)
      {
          return new HashSet<>(Arrays.asList(values));
      }
  
      @SafeVarargs
      private static <T> Set<T> insertionOrderedSet(T... values)
      {
          return new LinkedHashSet<>(Arrays.asList(values));
      }
  }
  