diff --git a/CHANGELOG.md b/CHANGELOG.md index 1577e75ef..14dbff8fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [7.0.16] - 2023-12-04 + +- Returns 400, instead of 500, for badly typed core config while creating CUD, App or Tenant + ## [7.0.15] - 2023-11-28 - Adds test for user pagination from old version diff --git a/build.gradle b/build.gradle index 7d494180f..9e3e4795c 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ compileTestJava { options.encoding = "UTF-8" } // } //} -version = "7.0.15" +version = "7.0.16" repositories { diff --git a/src/main/java/io/supertokens/config/Config.java b/src/main/java/io/supertokens/config/Config.java index 042af8c9f..038f36575 100644 --- a/src/main/java/io/supertokens/config/Config.java +++ b/src/main/java/io/supertokens/config/Config.java @@ -31,6 +31,7 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.utils.ConfigMapper; import org.jetbrains.annotations.TestOnly; import java.io.File; @@ -49,16 +50,17 @@ public class Config extends ResourceDistributor.SingletonResource { private Config(Main main, String configFilePath) throws InvalidConfigException, IOException { this.main = main; final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - CoreConfig config = mapper.readValue(new File(configFilePath), CoreConfig.class); - config.normalizeAndValidate(main); + Object configObj = mapper.readValue(new File(configFilePath), Object.class); + JsonObject jsonConfig = new Gson().toJsonTree(configObj).getAsJsonObject(); + CoreConfig config = ConfigMapper.mapConfig(jsonConfig, CoreConfig.class); + config.normalizeAndValidate(main, true); this.core = config; } private Config(Main main, JsonObject jsonConfig) throws IOException, InvalidConfigException { this.main = main; - final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - CoreConfig config = mapper.readValue(jsonConfig.toString(), CoreConfig.class); - config.normalizeAndValidate(main); + CoreConfig config = ConfigMapper.mapConfig(jsonConfig, CoreConfig.class); + config.normalizeAndValidate(main, false); this.core = config; } diff --git a/src/main/java/io/supertokens/config/CoreConfig.java b/src/main/java/io/supertokens/config/CoreConfig.java index e02fadb4d..ba291a4b2 100644 --- a/src/main/java/io/supertokens/config/CoreConfig.java +++ b/src/main/java/io/supertokens/config/CoreConfig.java @@ -394,7 +394,7 @@ private String getConfigFileLocation(Main main) { : CLIOptions.get(main).getConfigFilePath()).getAbsolutePath(); } - void normalizeAndValidate(Main main) throws InvalidConfigException { + void normalizeAndValidate(Main main, boolean includeConfigFilePath) throws InvalidConfigException { if (isNormalizedAndValid) { return; } @@ -407,8 +407,9 @@ void normalizeAndValidate(Main main) throws InvalidConfigException { } if (access_token_validity < 1 || access_token_validity > 86400000) { throw new InvalidConfigException( - "'access_token_validity' must be between 1 and 86400000 seconds inclusive. The config file can be" - + " found here: " + getConfigFileLocation(main)); + "'access_token_validity' must be between 1 and 86400000 seconds inclusive." + + (includeConfigFilePath ? " The config file can be" + + " found here: " + getConfigFileLocation(main) : "")); } Boolean validityTesting = CoreConfigTestContent.getInstance(main) .getValue(CoreConfigTestContent.VALIDITY_TESTING); @@ -417,16 +418,18 @@ void normalizeAndValidate(Main main) throws InvalidConfigException { if ((refresh_token_validity * 60) <= access_token_validity) { if (!Main.isTesting || validityTesting) { throw new InvalidConfigException( - "'refresh_token_validity' must be strictly greater than 'access_token_validity'. The config " - + "file can be found here: " + getConfigFileLocation(main)); + "'refresh_token_validity' must be strictly greater than 'access_token_validity'." + + (includeConfigFilePath ? " The config file can be" + + " found here: " + getConfigFileLocation(main) : "")); } } if (!Main.isTesting || validityTesting) { // since in testing we make this really small if (access_token_dynamic_signing_key_update_interval < 1) { throw new InvalidConfigException( - "'access_token_dynamic_signing_key_update_interval' must be greater than, equal to 1 hour. The " - + "config file can be found here: " + getConfigFileLocation(main)); + "'access_token_dynamic_signing_key_update_interval' must be greater than, equal to 1 hour." + + (includeConfigFilePath ? " The config file can be" + + " found here: " + getConfigFileLocation(main) : "")); } } @@ -456,8 +459,9 @@ void normalizeAndValidate(Main main) throws InvalidConfigException { if (max_server_pool_size <= 0) { throw new InvalidConfigException( - "'max_server_pool_size' must be >= 1. The config file can be found here: " - + getConfigFileLocation(main)); + "'max_server_pool_size' must be >= 1." + + (includeConfigFilePath ? " The config file can be" + + " found here: " + getConfigFileLocation(main) : "")); } if (api_keys != null) { diff --git a/src/main/java/io/supertokens/utils/ConfigMapper.java b/src/main/java/io/supertokens/utils/ConfigMapper.java new file mode 100644 index 000000000..6f636098a --- /dev/null +++ b/src/main/java/io/supertokens/utils/ConfigMapper.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * 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 io.supertokens.utils; + +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonAlias; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Map; + +public class ConfigMapper { + public static T mapConfig(JsonObject config, Class clazz) throws InvalidConfigException { + try { + T result = clazz.newInstance(); + for (Map.Entry entry : config.entrySet()) { + Field field = findField(clazz, entry.getKey()); + if (field != null) { + setValue(result, field, entry.getValue()); + } + } + return result; + } catch (InstantiationException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private static Field findField(Class clazz, String key) { + Field[] fields = clazz.getDeclaredFields(); + + for (Field field : fields) { + if (field.getName().equals(key)) { + return field; + } + + // Check for JsonProperty annotation + JsonProperty jsonProperty = field.getAnnotation(JsonProperty.class); + if (jsonProperty != null && jsonProperty.value().equals(key)) { + return field; + } + + // Check for JsonAlias annotation + JsonAlias jsonAlias = field.getAnnotation(JsonAlias.class); + if (jsonAlias != null) { + for (String alias : jsonAlias.value()) { + if (alias.equals(key)) { + return field; + } + } + } + } + + return null; // Field not found + } + + private static void setValue(T object, Field field, JsonElement value) throws InvalidConfigException { + boolean foundAnnotation = false; + for (Annotation a : field.getAnnotations()) { + if (a.toString().contains("JsonProperty")) { + foundAnnotation = true; + break; + } + } + + if (!foundAnnotation) { + return; + } + + field.setAccessible(true); + Object convertedValue = convertJsonElementToTargetType(value, field.getType(), field.getName()); + if (convertedValue != null) { + try { + field.set(object, convertedValue); + } catch (IllegalAccessException e) { + throw new IllegalStateException("should never happen"); + } + } + } + + private static Object convertJsonElementToTargetType(JsonElement value, Class targetType, String fieldName) + throws InvalidConfigException { + // If the value is JsonNull, return null for any type + if (value instanceof JsonNull || value == null) { + return null; + } + + try { + if (targetType == String.class) { + return value.getAsString(); + } else if (targetType == Integer.class || targetType == int.class) { + if (value.getAsDouble() == (double) value.getAsInt()) { + return value.getAsInt(); + } + } else if (targetType == Long.class || targetType == long.class) { + if (value.getAsDouble() == (double) value.getAsLong()) { + return value.getAsLong(); + } + } else if (targetType == Double.class || targetType == double.class) { + return value.getAsDouble(); + } else if (targetType == Float.class || targetType == float.class) { + return value.getAsFloat(); + } else if (targetType == Boolean.class || targetType == boolean.class) { + // Handle boolean conversion from strings like "true", "false" + return handleBooleanConversion(value, fieldName); + } + } catch (NumberFormatException e) { + // do nothing, will fall into InvalidConfigException + } + + // Throw an exception for unsupported conversions + throw new InvalidConfigException("'" + fieldName + "' must be of type " + targetType.getSimpleName()); + } + + private static Object handleBooleanConversion(JsonElement value, String fieldName) throws InvalidConfigException { + // Handle boolean conversion from strings like "true", "false" + if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) { + String stringValue = value.getAsString().toLowerCase(); + if (stringValue.equals("true")) { + return true; + } else if (stringValue.equals("false")) { + return false; + } + } else if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isBoolean()) { + return value.getAsBoolean(); + } + + // Throw an exception for unsupported conversions + throw new InvalidConfigException("'" + fieldName + "' must be of type boolean"); + } +} diff --git a/src/test/java/io/supertokens/test/ConfigMapperTest.java b/src/test/java/io/supertokens/test/ConfigMapperTest.java new file mode 100644 index 000000000..dc4b44c82 --- /dev/null +++ b/src/test/java/io/supertokens/test/ConfigMapperTest.java @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * 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 io.supertokens.test; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.JsonObject; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.utils.ConfigMapper; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class ConfigMapperTest { + + public static class DummyConfig { + @JsonProperty + int int_property; + + @JsonProperty + long long_property; + + @JsonProperty + float float_property; + + @JsonProperty + double double_property; + + @JsonProperty + String string_property; + + @JsonProperty + boolean bool_property; + } + + @Test + public void testAllValidConversions() throws Exception { + // valid for int + { + JsonObject config = new JsonObject(); + config.addProperty("int_property", "100"); + assertEquals(100, ConfigMapper.mapConfig(config, DummyConfig.class).int_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("int_property", 100); + assertEquals(100, ConfigMapper.mapConfig(config, DummyConfig.class).int_property); + } + + // valid for long + { + JsonObject config = new JsonObject(); + config.addProperty("long_property", "100"); + assertEquals(100, ConfigMapper.mapConfig(config, DummyConfig.class).long_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("long_property", 100); + assertEquals(100, ConfigMapper.mapConfig(config, DummyConfig.class).long_property); + } + + // valid for float + { + JsonObject config = new JsonObject(); + config.addProperty("float_property", 100); + System.out.println(ConfigMapper.mapConfig(config, DummyConfig.class).float_property); + assertEquals((float) 100, ConfigMapper.mapConfig(config, DummyConfig.class).float_property, 0.001); + } + { + JsonObject config = new JsonObject(); + config.addProperty("float_property", 3.14); + assertEquals((float) 3.14, ConfigMapper.mapConfig(config, DummyConfig.class).float_property, 0.001); + } + { + JsonObject config = new JsonObject(); + config.addProperty("float_property", "100"); + assertEquals((float) 100, ConfigMapper.mapConfig(config, DummyConfig.class).float_property, 0.001); + } + { + JsonObject config = new JsonObject(); + config.addProperty("float_property", "3.14"); + assertEquals((float) 3.14, ConfigMapper.mapConfig(config, DummyConfig.class).float_property, 0.001); + } + + // valid double + { + JsonObject config = new JsonObject(); + config.addProperty("double_property", 100); + assertEquals((double) 100, ConfigMapper.mapConfig(config, DummyConfig.class).double_property, 0.001); + } + { + JsonObject config = new JsonObject(); + config.addProperty("double_property", 3.14); + assertEquals((double) 3.14, ConfigMapper.mapConfig(config, DummyConfig.class).double_property, 0.001); + } + { + JsonObject config = new JsonObject(); + config.addProperty("double_property", "100"); + assertEquals((double) 100, ConfigMapper.mapConfig(config, DummyConfig.class).double_property, 0.001); + } + { + JsonObject config = new JsonObject(); + config.addProperty("double_property", "3.14"); + assertEquals((double) 3.14, ConfigMapper.mapConfig(config, DummyConfig.class).double_property, 0.001); + } + + // valid for bool + { + JsonObject config = new JsonObject(); + config.addProperty("bool_property", "true"); + assertEquals(true, ConfigMapper.mapConfig(config, DummyConfig.class).bool_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("bool_property", "TRUE"); + assertEquals(true, ConfigMapper.mapConfig(config, DummyConfig.class).bool_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("bool_property", "false"); + assertEquals(false, ConfigMapper.mapConfig(config, DummyConfig.class).bool_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("bool_property", true); + assertEquals(true, ConfigMapper.mapConfig(config, DummyConfig.class).bool_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("bool_property", false); + assertEquals(false, ConfigMapper.mapConfig(config, DummyConfig.class).bool_property); + } + + // valid for string + { + JsonObject config = new JsonObject(); + config.addProperty("string_property", "true"); + assertEquals("true", ConfigMapper.mapConfig(config, DummyConfig.class).string_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("string_property", true); + assertEquals("true", ConfigMapper.mapConfig(config, DummyConfig.class).string_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("string_property", 100); + assertEquals("100", ConfigMapper.mapConfig(config, DummyConfig.class).string_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("string_property", 3.14); + assertEquals("3.14", ConfigMapper.mapConfig(config, DummyConfig.class).string_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("string_property", "hello"); + assertEquals("hello", ConfigMapper.mapConfig(config, DummyConfig.class).string_property); + } + } + + @Test + public void testInvalidConversions() throws Exception { + String[] properties = new String[]{ + "int_property", + "int_property", + "int_property", + "int_property", + "int_property", + + "long_property", + "long_property", + "long_property", + "long_property", + + "float_property", + "float_property", + "float_property", + + "double_property", + "double_property", + "double_property", + }; + Object[] values = new Object[]{ + "abcd", // int + "", // int + true, // int + new Double(4.5), // int + new Long(1234567892342l), // int + + "abcd", // long + "", // long + true, // long + new Double(4.5), // long + + "abcd", // float + "", // float + true, // float + + "abcd", // double + "", // double + true, // double + }; + + String[] expectedErrorMessages = new String[]{ + "'int_property' must be of type int", // int + "'int_property' must be of type int", // int + "'int_property' must be of type int", // int + "'int_property' must be of type int", // int + "'int_property' must be of type int", // int + + "'long_property' must be of type long", // long + "'long_property' must be of type long", // long + "'long_property' must be of type long", // long + "'long_property' must be of type long", // long + + "'float_property' must be of type float", // float + "'float_property' must be of type float", // float + "'float_property' must be of type float", // float + + "'double_property' must be of type double", // double + "'double_property' must be of type double", // double + "'double_property' must be of type double", // double + }; + + for (int i = 0; i < properties.length; i++) { + try { + System.out.println("Test case " + i); + JsonObject config = new JsonObject(); + if (values[i] == null) { + config.add(properties[i], null); + } + else if (values[i] instanceof String) { + config.addProperty(properties[i], (String) values[i]); + } else if (values[i] instanceof Boolean) { + config.addProperty(properties[i], (Boolean) values[i]); + } else if (values[i] instanceof Number) { + config.addProperty(properties[i], (Number) values[i]); + } else { + throw new RuntimeException("Invalid type"); + } + DummyConfig dc = ConfigMapper.mapConfig(config, DummyConfig.class); + fail(); + } catch (InvalidConfigException e) { + assertEquals(expectedErrorMessages[i], e.getMessage()); + } + } + } +} diff --git a/src/test/java/io/supertokens/test/ConfigTest2_21.java b/src/test/java/io/supertokens/test/ConfigTest2_21.java index 73b886f6f..a4b28c060 100644 --- a/src/test/java/io/supertokens/test/ConfigTest2_21.java +++ b/src/test/java/io/supertokens/test/ConfigTest2_21.java @@ -24,6 +24,7 @@ import org.junit.*; import org.junit.rules.TestRule; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; public class ConfigTest2_21 { @@ -84,4 +85,22 @@ public void testThatNewConfigWorks() throws Exception { EventAndException stopEvent = process.checkOrWaitForEvent(PROCESS_STATE.STOPPED); assertNotNull(stopEvent); } + + @Test + public void testCoreConfigTypeValidationInConfigYaml() throws Exception { + Utils.setValueInConfig("access_token_validity", "abcd"); + + String[] args = { "../" }; + + TestingProcess process = TestingProcessManager.start(args); + + EventAndException startEvent = process.checkOrWaitForEvent(PROCESS_STATE.INIT_FAILURE); + assertNotNull(startEvent); + + assertEquals("io.supertokens.pluginInterface.exceptions.InvalidConfigException: 'access_token_validity' must be of type long", startEvent.exception.getMessage()); + + process.kill(); + EventAndException stopEvent = process.checkOrWaitForEvent(PROCESS_STATE.STOPPED); + assertNotNull(stopEvent); + } } diff --git a/src/test/java/io/supertokens/test/multitenant/api/TestApp.java b/src/test/java/io/supertokens/test/multitenant/api/TestApp.java index fee7b1ad8..b548ebb4c 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestApp.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestApp.java @@ -19,6 +19,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.ProcessState; +import io.supertokens.config.CoreConfigTestContent; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; @@ -31,13 +32,11 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storageLayer.StorageLayer; -import io.supertokens.test.HttpRequestTest; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; import io.supertokens.test.httpRequest.HttpRequestForTesting; import io.supertokens.test.httpRequest.HttpResponseException; import io.supertokens.thirdparty.InvalidProviderConfigException; -import io.supertokens.utils.SemVer; import io.supertokens.webserver.Webserver; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -49,7 +48,6 @@ import org.junit.Test; import java.io.IOException; -import java.rmi.ServerException; import static org.junit.Assert.*; @@ -507,4 +505,108 @@ public void testDefaultRecipesEnabledWhileCreatingApp() throws Exception { assertTrue(tenant.get("thirdParty").getAsJsonObject().get("enabled").getAsBoolean()); assertTrue(tenant.get("passwordless").getAsJsonObject().get("enabled").getAsBoolean()); } + + @Test + public void testInvalidTypedValueInCoreConfigWhileCreatingApp() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + if (StorageLayer.isInMemDb(process.getProcess())) { + return; + } + + String[] properties = new String[]{ + "access_token_validity", // long + "access_token_validity", // long + "access_token_validity", // long + "access_token_validity", // long + "disable_telemetry", // boolean + "postgresql_connection_pool_size", // int + "mysql_connection_pool_size", // int + }; + Object[] values = new Object[]{ + "abcd", // access_token_validity + "", + "null", + null, + "abcd", // disable_telemetry + "abcd", // postgresql_connection_pool_size + "abcd", // mysql_connection_pool_size + }; + + String[] expectedErrorMessages = new String[]{ + "Http error. Status Code: 400. Message: Invalid core config: 'access_token_validity' must be of type long", // access_token_validity + "Http error. Status Code: 400. Message: Invalid core config: 'access_token_validity' must be of type long", // access_token_validity + "Http error. Status Code: 400. Message: Invalid core config: 'access_token_validity' must be of type long", // access_token_validity + null, + "Http error. Status Code: 400. Message: Invalid core config: 'disable_telemetry' must be of type boolean", // disable_telemetry + "Http error. Status Code: 400. Message: Invalid core config: 'postgresql_connection_pool_size' must be of type int", // postgresql_connection_pool_size + "Http error. Status Code: 400. Message: Invalid core config: 'mysql_connection_pool_size' must be of type int", // mysql_connection_pool_size + }; + + System.out.println(StorageLayer.getStorage(process.getProcess()).getClass().getCanonicalName()); + + for (int i = 0; i < properties.length; i++) { + try { + System.out.println("Test case " + i); + JsonObject config = new JsonObject(); + if (values[i] == null) { + config.add(properties[i], null); + } + else if (values[i] instanceof String) { + config.addProperty(properties[i], (String) values[i]); + } else if (values[i] instanceof Boolean) { + config.addProperty(properties[i], (Boolean) values[i]); + } else if (values[i] instanceof Number) { + config.addProperty(properties[i], (Number) values[i]); + } else { + throw new RuntimeException("Invalid type"); + } + StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + + JsonObject response = TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + config); + if (expectedErrorMessages[i] != null) { + fail(); + } + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + if (!e.getMessage().contains("Invalid config key")) { + assertEquals(expectedErrorMessages[i], e.getMessage()); + } + } + } + } + + @Test + public void testInvalidCoreConfig() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + CoreConfigTestContent.getInstance(process.getProcess()).setKeyValue(CoreConfigTestContent.VALIDITY_TESTING, + true); + + { + JsonObject config = new JsonObject(); + config.addProperty("access_token_validity", 3600); + config.addProperty("refresh_token_validity", 3); + StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + + try { + JsonObject response = TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + config); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: Invalid core config: 'refresh_token_validity' must be strictly greater than 'access_token_validity'.", e.getMessage()); + } + } + } }