diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datatypes/MySQL_Spec.ts b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datatypes/MySQL_Spec.ts index 0b6f24ed42..aaef244cf4 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datatypes/MySQL_Spec.ts +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datatypes/MySQL_Spec.ts @@ -36,7 +36,9 @@ describe("MySQL Datatype tests", function() { dataSources.RunQuery(); ee.ActionContextMenuByEntityName(dsName, "Refresh"); - agHelper.AssertElementVisible(ee._entityNameInExplorer(inputData.tableName)); + agHelper.AssertElementVisible( + ee._entityNameInExplorer(inputData.tableName), + ); }); it("3. Creating SELECT query", () => { @@ -54,32 +56,14 @@ describe("MySQL Datatype tests", function() { agHelper.RenameWithInPane("insertRecord"); dataSources.EnterQuery(query); - query = inputData.query.deleteAllRecords; - ee.ActionTemplateMenuByEntityName(inputData.tableName, "DELETE"); - agHelper.RenameWithInPane("deleteAllRecords"); - dataSources.EnterQuery(query); - query = inputData.query.dropTable; ee.ActionTemplateMenuByEntityName(inputData.tableName, "DELETE"); agHelper.RenameWithInPane("dropTable"); dataSources.EnterQuery(query); }); - //Insert false values to each column and check for the error status of the request. - it("5. False Cases", () => { - ee.ActionTemplateMenuByEntityName(inputData.tableName, "INSERT"); - agHelper.RenameWithInPane("falseCases"); - inputData.falseResult.forEach((res_array, i) => { - res_array.forEach((value) => { - query = `INSERT INTO ${inputData.tableName} (${inputData.inputFieldName[i]}) VALUES (${value})`; - dataSources.EnterQuery(query); - dataSources.RunQuery(false); - }); - }); - }); - //Insert valid/true values into datasource - it("6. Inserting record", () => { + it("5. Inserting record", () => { ee.SelectEntityByName("Page1"); deployMode.DeployApp(); table.WaitForTableEmpty(); //asserting table is empty before inserting! @@ -87,7 +71,8 @@ describe("MySQL Datatype tests", function() { inputData.input.forEach((valueArr, i) => { agHelper.ClickButton("Run InsertQuery"); valueArr.forEach((value, index) => { - agHelper.EnterInputText(inputData.inputFieldName[index], value); + if (value !== "") + agHelper.EnterInputText(inputData.inputFieldName[index], value); }); i % 2 && agHelper.ToggleSwitch("Bool_column"); agHelper.ClickButton("insertRecord"); @@ -98,26 +83,41 @@ describe("MySQL Datatype tests", function() { //Verify weather expected value is present in each cell //i.e. weather right data is pushed and fetched from datasource. - it("7. Validating values in each cell", () => { + it("6. Validating values in each cell", () => { cy.wait(2000); inputData.result.forEach((res_array, i) => { res_array.forEach((value, j) => { table.ReadTableRowColumnData(j, i, 0).then(($cellData) => { - expect($cellData).to.eq(value); + if(i === inputData.result.length-1){ + let obj = JSON.parse($cellData) + expect(JSON.stringify(obj)).to.eq(JSON.stringify(value)); + }else{ + expect($cellData).to.eq(value); + } }); }); }); }); - it("8. Deleting all records from table ", () => { - agHelper.GetNClick(locator._deleteIcon); - agHelper.Sleep(2000); - table.WaitForTableEmpty(); - }); - - it("9. Validate Drop of the Newly Created - mysqlDTs - Table from MySQL datasource", () => { + //null will be displayed as empty string in tables + //So test null we have to intercept execute request. + //And check response payload. + it("7. Testing null value", () => { deployMode.NavigateBacktoEditor(); ee.ExpandCollapseEntity("Queries/JS"); + ee.SelectEntityByName("selectRecords"); + dataSources.RunQuery(true, false); + cy.wait("@postExecute").then((intercept) => { + expect( + typeof intercept.response?.body.data.body[5].varchar_column, + ).to.be.equal("object"); + expect(intercept.response?.body.data.body[5].varchar_column).to.be.equal( + null, + ); + }); + }); + + it("8. Validate drop of mysqlDTs - Table from MySQL datasource", () => { ee.SelectEntityByName("dropTable"); dataSources.RunQuery(); dataSources.ReadQueryTableResponse(0).then(($cellData) => { @@ -134,15 +134,20 @@ describe("MySQL Datatype tests", function() { ee.ExpandCollapseEntity("Datasources", false); }); - it("10. Verify Deletion of the datasource after all created queries are Deleted", () => { + it("9. Verify Deletion of the datasource after all created queries are Deleted", () => { dataSources.DeleteDatasouceFromWinthinDS(dsName, 409); //Since all queries exists ee.ExpandCollapseEntity("Queries/JS"); - ["falseCases", "createTable", "deleteAllRecords", "dropTable", "insertRecord", "selectRecords"].forEach(type => { + [ + "createTable", + "dropTable", + "insertRecord", + "selectRecords", + ].forEach((type) => { ee.ActionContextMenuByEntityName(type, "Delete", "Are you sure?"); - }) + }); deployMode.DeployApp(); deployMode.NavigateBacktoEditor(); ee.ExpandCollapseEntity("Queries/JS"); - dataSources.DeleteDatasouceFromWinthinDS(dsName, 200); + dataSources.DeleteDatasouceFromWinthinDS(dsName, 200); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datatypes/MySQL_false_Spec.ts b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datatypes/MySQL_false_Spec.ts new file mode 100644 index 0000000000..1cb430433d --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datatypes/MySQL_false_Spec.ts @@ -0,0 +1,79 @@ +import { ObjectsRegistry } from "../../../../support/Objects/Registry"; +import inputData from "../../../../support/Objects/mySqlData"; + +let dsName: any, query: string; +const agHelper = ObjectsRegistry.AggregateHelper, + ee = ObjectsRegistry.EntityExplorer, + dataSources = ObjectsRegistry.DataSources, + propPane = ObjectsRegistry.PropertyPane, + table = ObjectsRegistry.Table, + locator = ObjectsRegistry.CommonLocators, + deployMode = ObjectsRegistry.DeployMode; + +describe("MySQL Datatype tests", function() { + it("1. Create Mysql DS", function() { + dataSources.CreateDataSource("MySql"); + cy.get("@dsName").then(($dsName) => { + dsName = $dsName; + }); + }); + + it("2. Creating mysqlDTs table", () => { + //IF NOT EXISTS can be used - which creates tabel if it does not exist and donot throw any error if table exists. + //But if we add this option then next case could fail inn that case. + query = inputData.query.createTable; + ee.CreateNewDsQuery(dsName); + agHelper.RenameWithInPane("createTable"); + agHelper.GetNClick(dataSources._templateMenu); + dataSources.EnterQuery(query); + dataSources.RunQuery(); + + ee.ActionContextMenuByEntityName(dsName, "Refresh"); + agHelper.AssertElementVisible( + ee._entityNameInExplorer(inputData.tableName), + ); + }); + + //Insert false values to each column and check for the error status of the request. + it("3. False Cases", () => { + ee.ActionTemplateMenuByEntityName(inputData.tableName, "INSERT"); + agHelper.RenameWithInPane("falseCases"); + inputData.falseResult.forEach((res_array, i) => { + res_array.forEach((value) => { + query = + typeof value === "string" + ? `INSERT INTO ${inputData.tableName} (${inputData.inputFieldName[i]}) VALUES ({{"${value}"}})` + : `INSERT INTO ${inputData.tableName} (${inputData.inputFieldName[i]}) VALUES ({{${value}}})`; + dataSources.EnterQuery(query); + dataSources.RunQuery(false); + }); + }); + agHelper.Sleep(2000); + agHelper.WaitUntilAllToastsDisappear(); + }); + + //This is a special case. + //Added due to server side checks, which was handled in Datatype handling. + it("4. Long Integer as query param", () => { + query = `SELECT * FROM ${inputData.tableName} LIMIT {{2147483648}}`; + dataSources.EnterQuery(query); + dataSources.RunQuery(); + }); + + it("5. Drop Table", () => { + query = inputData.query.dropTable; + dataSources.EnterQuery(query); + dataSources.RunQuery(); + }); + + it("6. Verify Deletion of the datasource after all created queries are Deleted", () => { + ee.ExpandCollapseEntity("Queries/JS"); + ["falseCases", "createTable"].forEach((type) => { + ee.ActionContextMenuByEntityName(type, "Delete", "Are you sure?"); + }); + deployMode.DeployApp(); + deployMode.NavigateBacktoEditor(); + ee.ExpandCollapseEntity("Queries/JS"); + dataSources.DeleteDatasouceFromWinthinDS(dsName, 200); + }); +}); diff --git a/app/client/cypress/support/Objects/mySqlData.ts b/app/client/cypress/support/Objects/mySqlData.ts index 9fa866775a..96e1e0d986 100644 --- a/app/client/cypress/support/Objects/mySqlData.ts +++ b/app/client/cypress/support/Objects/mySqlData.ts @@ -1,6 +1,6 @@ const mySqlData = { tableName: "mysqlDTs", - inputFieldName : [ + inputFieldName: [ "Stinyint_column", "Utinyint_column", "Ssmallint_column", @@ -23,7 +23,7 @@ const mySqlData = { "Enum_column", "Json_column", ], - input : [ + input: [ [ "-128", "0", @@ -88,11 +88,34 @@ const mySqlData = { "2012/12/31", "-838:59:59", "1901'", - "null", - "null", + "12345678912345", + "012345", "c", "[1, 2, 3, 4]", ], + [ + "0", + "0", + "0", + "0", + "0", + "0", + "0123", + "0", + "0", + "0", + "0", + "0", + "20121231113045", + "121231113045", + "121231", + "11:12", + "2155", + "true", + "false", + "c", + "[]", + ], [ "0", "0", @@ -109,33 +132,10 @@ const mySqlData = { "20121231113045", "121231113045", "121231", - "11:12", - "2155", - "null", - "null", - "c", - "[]", - ], - [ - "0", - "0", - "0", - "0", - "0", - "0", - "0", - "0", - "0", - "0", - "0", - "null", - "121231113045", - "null", - "null", "1112", + "2022", "null", - "null", - "null", + "NulL", "c", '["a",true,0,12.34]', ], @@ -151,19 +151,19 @@ const mySqlData = { "0", "0", "0", - "null", - "null", - "null", - "null", + "0", + "20121231113045", + "121231113045", + "121231", "12", - "null", - "null", - "null", + "2022", + "", + "abc", "c", - "null", + '{}', ], ], - falseResult : [ + falseResult: [ [-129, 128], [-1, 256], [-32769, 32768], @@ -172,9 +172,9 @@ const mySqlData = { [-1, 16777216], [-2147483649, 2147483648], [-1, 4294967296], - [-9223372036854775808, 9223372036854775807], + [], [123456789.45], - ['a'], + ["a"], [123456789.45], ["2012/12/31 11:30:451"], ["2012/12/311 11:30:45"], @@ -184,9 +184,9 @@ const mySqlData = { ["abcdefghijklmnopqrstu"], ["abcdefghijk"], ["d"], - ["abc", "{", "["], + [], ], - result : [ + result: [ ["1", "2", "3", "4", "5"], ["-128", "0", "127"], ["0", "255"], @@ -194,7 +194,7 @@ const mySqlData = { ["0", "65535"], ["-8388608", "0", "8388607"], ["0", "16777215"], - ["-2147483648", "0", "2147483647"], + ["-2147483648", "0", "2147483647", "123"], ["0", "4294967295"], ["123456", "456789", "123456789"], ["123.45", "123.46", "123.45"], @@ -216,30 +216,28 @@ const mySqlData = { ["2012-12-31", "2012-12-31", "2012-12-31", "2012-12-31"], ["22:59:59", "00:00:00", "01:00:01", "11:12:00", "00:11:12", "00:00:12"], ["1901", "2155", "1901", "2155"], - ["a", "abcdefghijklmnopqrst"], - ["a", "abcdefghij"], + ["a", "abcdefghijklmnopqrst", "12345678912345", "true", "null"], + ["a", "abcdefghij", "012345", "false", "NulL"], ["a", "b", "c"], ["0", "1"], - ['{"abc": "123"}', "{}", "[1, 2, 3, 4]", "", '["a",true,0,12.34]'], + [{"abc": "123"}, {}, [1, 2, 3, 4], [], ["a",true,0,12.34]], ], - query :{ - createTable : `CREATE TABLE mysqlDTs (serialId SERIAL not null primary key, stinyint_column TINYINT, utinyint_column TINYINT UNSIGNED, + query: { + createTable: `CREATE TABLE mysqlDTs (serialId SERIAL not null primary key, stinyint_column TINYINT, utinyint_column TINYINT UNSIGNED, ssmallint_column SMALLINT, usmallint_column SMALLINT UNSIGNED, smediumint_column MEDIUMINT, umediumint_column MEDIUMINT UNSIGNED, sint_column INT, uint_column INT UNSIGNED, bigint_column BIGINT, float_column FLOAT( 10, 2 ), double_column DOUBLE, decimal_column DECIMAL( 10, 2 ), datetime_column DATETIME, timestamp_column TIMESTAMP, date_column DATE, time_column TIME, year_column YEAR, varchar_column VARCHAR( 20 ), char_column CHAR( 10 ), enum_column ENUM( 'a', 'b', 'c' ), bool_column BOOL, json_column JSON);`, - insertRecord : `INSERT INTO mysqlDTs (stinyint_column, utinyint_column, ssmallint_column, usmallint_column, smediumint_column, umediumint_column, + insertRecord: `INSERT INTO mysqlDTs (stinyint_column, utinyint_column, ssmallint_column, usmallint_column, smediumint_column, umediumint_column, sint_column, uint_column, bigint_column, float_column, double_column, decimal_column, datetime_column, timestamp_column, date_column, time_column, year_column, varchar_column, char_column, enum_column, bool_column, json_column ) VALUES ({{InsertStinyint.text}}, {{InsertUtinyint.text}}, {{InsertSsmallint.text}}, {{InsertUsmallint.text}}, {{InsertSmediumint.text}}, {{InsertUmediumint.text}}, {{InsertSint.text}}, {{InsertUint.text}}, {{InsertBigint.text}}, {{InsertFloat.text}}, {{InsertDouble.text}}, {{InsertDecimal.text}}, {{InsertDatetime.text}}, {{InsertTimestamp.text}}, {{InsertDate.text}}, {{InsertTime.text}}, - {{InsertYear.text}}, {{InsertVarchar.text}}, {{InsertChar.text}}, {{InsertEnum.text}}, {{InsertBoolean.isSwitchedOn}}, {{InputJson.text}});`, - deleteRecord : `DELETE FROM mysqlDTs WHERE serialId ={{Table1.selectedRow.serialid}}`, - deleteAllRecords : `DELETE FROM mysqlDTs`, + {{InsertYear.text}}, {{InsertVarchar.text ? InsertVarchar.text : null}}, {{InsertChar.text}}, {{InsertEnum.text}}, {{InsertBoolean.isSwitchedOn}}, {{InputJson.text}});`, dropTable: `drop table mysqlDTs`, - } + }, }; -export default mySqlData; \ No newline at end of file +export default mySqlData; diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/SmartSubstitutionInterface.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/SmartSubstitutionInterface.java index f8f82293be..55cb5b5491 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/SmartSubstitutionInterface.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/SmartSubstitutionInterface.java @@ -5,6 +5,7 @@ import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; import com.appsmith.external.models.Param; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; @@ -39,7 +40,7 @@ public interface SmartSubstitutionInterface { String value = matchingParam.get().getValue(); input = substituteValueInInput(i + 1, key, - value, input, insertedParams, args); + value, input, insertedParams, append(args, matchingParam.get().getClientDataType())); } else { throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Uh oh! This is unexpected. " + "Did not receive any information for the binding " @@ -71,4 +72,11 @@ public interface SmartSubstitutionInterface { default String sanitizeReplacement(String replacementValue, DataType dataType) { return replacementValue; } + + static T[] append(T[] arr, T lastElement) { + final int N = arr.length; + arr = Arrays.copyOf(arr, N+1); + arr[N] = lastElement; + return arr; + } } diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java index c3323d3a9f..eb9e60d40c 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java @@ -1,11 +1,12 @@ package com.external.plugins; -import com.appsmith.external.constants.DataType; +import com.appsmith.external.datatypes.AppsmithType; +import com.appsmith.external.datatypes.ClientDataType; import com.appsmith.external.dtos.ExecuteActionDTO; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; -import com.appsmith.external.helpers.DataTypeStringUtils; +import com.appsmith.external.helpers.DataTypeServiceUtils; import com.appsmith.external.helpers.MustacheHelper; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionRequest; @@ -22,6 +23,7 @@ import com.appsmith.external.models.SSLDetails; import com.appsmith.external.plugins.BasePlugin; import com.appsmith.external.plugins.PluginExecutor; import com.appsmith.external.plugins.SmartSubstitutionInterface; +import com.external.plugins.datatypes.MySQLSpecificDataTypes; import com.external.utils.QueryUtils; import io.r2dbc.spi.ColumnMetadata; import io.r2dbc.spi.Connection; @@ -340,7 +342,7 @@ public class MySqlPlugin extends BasePlugin { } - private boolean isIsOperatorUsed(String query) { + boolean isIsOperatorUsed(String query) { String queryKeyWordsOnly = query.replaceAll(MATCH_QUOTED_WORDS_REGEX, ""); return Arrays.stream(queryKeyWordsOnly.split("\\s")) .anyMatch(word -> IS_KEY.equalsIgnoreCase(word.trim())); @@ -394,29 +396,36 @@ public class MySqlPlugin extends BasePlugin { Object... args) { Statement connectionStatement = (Statement) input; - DataType valueType = DataTypeStringUtils.stringToKnownDataTypeConverter(value); + ClientDataType clientDataType = (ClientDataType) args[0]; + AppsmithType appsmithType = DataTypeServiceUtils.getAppsmithType(clientDataType, value, MySQLSpecificDataTypes.pluginSpecificTypes); - Map.Entry parameter = new SimpleEntry<>(value, valueType.toString()); + Map.Entry parameter = new SimpleEntry<>(value, appsmithType.type().toString()); insertedParams.add(parameter); - if (DataType.NULL.equals(valueType)) { - try { - connectionStatement.bindNull((index - 1), Object.class); - } catch (UnsupportedOperationException e) { - // Do nothing. Move on - } - } else if (DataType.INTEGER.equals(valueType)) { - /** - * - NumberFormatException is NOT expected here since stringToKnownDataTypeConverter uses parseInt - * method to detect INTEGER type. - */ - connectionStatement.bind((index - 1), Integer.parseInt(value)); - } else if (DataType.BOOLEAN.equals(valueType)) { - connectionStatement.bind((index - 1), Boolean.parseBoolean(value) == TRUE ? 1 : 0); - } else { - connectionStatement.bind((index - 1), value); + switch (appsmithType.type()) { + case NULL: + try { + connectionStatement.bindNull((index - 1), Object.class); + } catch (UnsupportedOperationException e) { + // Do nothing. Move on + } + break; + case BOOLEAN: + connectionStatement.bind((index - 1), appsmithType.performSmartSubstitution(value)); + break; + case INTEGER: + connectionStatement.bind((index - 1), Integer.parseInt(value)); + break; + case LONG: + connectionStatement.bind((index - 1), Long.parseLong(value)); + break; + case DOUBLE: + connectionStatement.bind((index - 1), Double.parseDouble(value)); + break; + default: + connectionStatement.bind((index - 1), value); + break; } - return connectionStatement; } diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/datatypes/MySQLSpecificDataTypes.java b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/datatypes/MySQLSpecificDataTypes.java new file mode 100644 index 0000000000..8d25ec4778 --- /dev/null +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/datatypes/MySQLSpecificDataTypes.java @@ -0,0 +1,44 @@ +package com.external.plugins.datatypes; + +import com.appsmith.external.datatypes.AppsmithType; +import com.appsmith.external.datatypes.BigDecimalType; +import com.appsmith.external.datatypes.ClientDataType; +import com.appsmith.external.datatypes.DoubleType; +import com.appsmith.external.datatypes.IntegerType; +import com.appsmith.external.datatypes.JsonObjectType; +import com.appsmith.external.datatypes.LongType; +import com.appsmith.external.datatypes.NullType; +import com.appsmith.external.datatypes.StringType; +import com.appsmith.external.datatypes.TimeType; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MySQLSpecificDataTypes { + public static final Map> pluginSpecificTypes; + + static { + pluginSpecificTypes = new HashMap<>(); + pluginSpecificTypes.put(ClientDataType.NULL, List.of(new NullType())); + + pluginSpecificTypes.put(ClientDataType.BOOLEAN, List.of(new MySQLBooleanType())); + + pluginSpecificTypes.put(ClientDataType.NUMBER, List.of( + new IntegerType(), + new LongType(), + new DoubleType(), + new BigDecimalType() + )); + + pluginSpecificTypes.put(ClientDataType.OBJECT, List.of(new JsonObjectType())); + + pluginSpecificTypes.put(ClientDataType.STRING, List.of( + new TimeType(), + new MySQLDateType(), + new MySQLDateTimeType(), + new StringType() + )); + } + +} diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java index abc57f187a..23b9e3cdbf 100755 --- a/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java +++ b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java @@ -1,5 +1,6 @@ package com.external.plugins; +import com.appsmith.external.datatypes.ClientDataType; import com.appsmith.external.dtos.ExecuteActionDTO; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; @@ -17,6 +18,7 @@ import com.appsmith.external.models.SSLDetails; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactories; import io.r2dbc.spi.ConnectionFactoryOptions; @@ -39,6 +41,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -49,1125 +52,1428 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; @Slf4j @Testcontainers public class MySqlPluginTest { - MySqlPlugin.MySqlPluginExecutor pluginExecutor = new MySqlPlugin.MySqlPluginExecutor(); - - @SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is pseudo-optional. - @Container - public static MySQLContainer mySQLContainer = new MySQLContainer( - DockerImageName.parse("mysql/mysql-server:8.0.25").asCompatibleSubstituteFor("mysql")) - .withUsername("mysql") - .withPassword("password") - .withDatabaseName("test_db"); - - @SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is pseudo-optional. - @Container - public static MySQLContainer mySQLContainerWithInvalidTimezone = (MySQLContainer) new MySQLContainer( - DockerImageName.parse("mysql/mysql-server:8.0.25").asCompatibleSubstituteFor("mysql")) - .withUsername("root") - .withPassword("") - .withDatabaseName("test_db") - .withEnv("TZ", "PDT") - .withEnv("MYSQL_ROOT_HOST", "%"); - - private static String address; - private static Integer port; - private static String username; - private static String password; - private static String database; - private static DatasourceConfiguration dsConfig; - - @BeforeAll - public static void setUp() { - address = mySQLContainer.getContainerIpAddress(); - port = mySQLContainer.getFirstMappedPort(); - username = mySQLContainer.getUsername(); - password = mySQLContainer.getPassword(); - database = mySQLContainer.getDatabaseName(); - dsConfig = createDatasourceConfiguration(); - - ConnectionFactoryOptions baseOptions = MySQLR2DBCDatabaseContainer.getOptions(mySQLContainer); - ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); - - Mono.from(ConnectionFactories.get(ob.build()).create()) - .map(connection -> { - return connection.createBatch() - .add("create table users (\n" + - " id int auto_increment primary key,\n" + - " username varchar (250) unique not null,\n" + - " password varchar (250) not null,\n" + - " email varchar (250) unique not null,\n" + - " spouse_dob date,\n" + - " dob date not null,\n" + - " yob year not null,\n" + - " time1 time not null,\n" + - " created_on timestamp not null,\n" + - " updated_on datetime not null,\n" + - " constraint unique index (username, email)\n" + - ")" - ) - .add("create table possessions (\n" + - " id int primary key,\n" + - " title varchar (250) not null,\n" + - " user_id int not null,\n" + - " username varchar (250) not null,\n" + - " email varchar (250) not null\n" + - ")" - ) - .add("alter table possessions add foreign key (username, email) \n" + - "references users (username, email)" - ) - .add("SET SESSION sql_mode = '';\n") - .add("INSERT INTO users VALUES (" + - "1, 'Jack', 'jill', 'jack@exemplars.com', NULL, '2018-12-31', 2018," + - " '18:32:45'," + - " '2018-11-30 20:45:15', '0000-00-00 00:00:00'" + - ")" - ) - .add("INSERT INTO users VALUES (" + - "2, 'Jill', 'jack', 'jill@exemplars.com', NULL, '2019-12-31', 2019," + - " '15:45:30'," + - " '2019-11-30 23:59:59', '2019-11-30 23:59:59'" + - ")" - ); - }) - .flatMapMany(batch -> Flux.from(batch.execute())) - .blockLast(); //wait until completion of all the queries - - return; - } - - private static DatasourceConfiguration createDatasourceConfiguration() { - DBAuth authDTO = new DBAuth(); - authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD); - authDTO.setUsername(username); - authDTO.setPassword(password); - authDTO.setDatabaseName(database); - - Endpoint endpoint = new Endpoint(); - endpoint.setHost(address); - endpoint.setPort(port.longValue()); - - DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); - - /* set endpoint */ - datasourceConfiguration.setAuthentication(authDTO); - datasourceConfiguration.setEndpoints(List.of(endpoint)); - - /* set ssl mode */ - datasourceConfiguration.setConnection(new com.appsmith.external.models.Connection()); - datasourceConfiguration.getConnection().setSsl(new SSLDetails()); - datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.DEFAULT); - - return datasourceConfiguration; - } - - @Test - public void testConnectMySQLContainer() { - - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - StepVerifier.create(dsConnectionMono) - .assertNext(Assertions::assertNotNull) - .verifyComplete(); - } - - @Test - public void testConnectMySQLContainerWithInvalidTimezone() { - - final DatasourceConfiguration dsConfig = createDatasourceConfigForContainerWithInvalidTZ(); - dsConfig.setProperties(List.of( - new Property("serverTimezone", "UTC") - )); - - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - StepVerifier.create(dsConnectionMono) - .assertNext(Assertions::assertNotNull) - .verifyComplete(); - } - - @Test - public void testTestDatasource() { - dsConfig = createDatasourceConfiguration(); - - /* Expect no error */ - StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) - .assertNext(datasourceTestResult -> { - assertEquals(0, datasourceTestResult.getInvalids().size()); - }) - .verifyComplete(); - - /* Create bad datasource configuration and expect error */ - dsConfig.getEndpoints().get(0).setHost("badHost"); - StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) - .assertNext(datasourceTestResult -> { - assertNotEquals(0, datasourceTestResult.getInvalids().size()); - }) - .verifyComplete(); - - /* Reset dsConfig */ - dsConfig = createDatasourceConfiguration(); - } - - - public DatasourceConfiguration createDatasourceConfigForContainerWithInvalidTZ() { - final DBAuth authDTO = new DBAuth(); - authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD); - authDTO.setUsername(mySQLContainerWithInvalidTimezone.getUsername()); - authDTO.setPassword(mySQLContainerWithInvalidTimezone.getPassword()); - authDTO.setDatabaseName(mySQLContainerWithInvalidTimezone.getDatabaseName()); - - final Endpoint endpoint = new Endpoint(); - endpoint.setHost(mySQLContainerWithInvalidTimezone.getContainerIpAddress()); - endpoint.setPort(mySQLContainerWithInvalidTimezone.getFirstMappedPort().longValue()); - - final DatasourceConfiguration dsConfig = new DatasourceConfiguration(); - - /* set endpoint */ - dsConfig.setAuthentication(authDTO); - dsConfig.setEndpoints(List.of(endpoint)); - - /* set ssl mode */ - - dsConfig.setConnection(new com.appsmith.external.models.Connection()); - dsConfig.getConnection().setMode(com.appsmith.external.models.Connection.Mode.READ_WRITE); - dsConfig.getConnection().setSsl(new SSLDetails()); - dsConfig.getConnection().getSsl().setAuthType(SSLDetails.AuthType.DEFAULT); - - return dsConfig; - } - - @Test - public void testDatasourceWithNullPassword() { - final DatasourceConfiguration dsConfig = createDatasourceConfigForContainerWithInvalidTZ(); - - // adding a user with empty password - ConnectionFactoryOptions baseOptions = MySQLR2DBCDatabaseContainer.getOptions(mySQLContainerWithInvalidTimezone); - ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); - - Mono.from(ConnectionFactories.get(ob.build()).create()) - .map(connection -> connection.createBatch() - // adding a new user called 'mysql' with empty password - .add("CREATE USER 'mysql'@'%';\n" + - "GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'%' WITH GRANT OPTION;\n" + - "FLUSH PRIVILEGES;") - ) - .flatMapMany(batch -> Flux.from(batch.execute())) - .blockLast(); //wait until completion of all the queries - - - // change to ordinary user - DBAuth auth = ((DBAuth) dsConfig.getAuthentication()); - auth.setPassword(""); - auth.setUsername("mysql"); - - // check user pass - assertEquals("mysql", auth.getUsername()); - assertEquals("", auth.getPassword()); - - - // Validate datastore - Set output = pluginExecutor.validateDatasource(dsConfig); - assertTrue(output.isEmpty()); - // test connect - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - StepVerifier.create(dsConnectionMono) - .assertNext(Assertions::assertNotNull) - .verifyComplete(); - - /* Expect no error */ - StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) - .assertNext(datasourceTestResult -> assertEquals(0, datasourceTestResult.getInvalids().size())) - .verifyComplete(); - } - - @Test - public void testDatasourceWithRootUserAndNullPassword() { - - final DatasourceConfiguration dsConfig = createDatasourceConfigForContainerWithInvalidTZ(); - - // check user pass - assertEquals("root", mySQLContainerWithInvalidTimezone.getUsername()); - assertEquals("", mySQLContainerWithInvalidTimezone.getPassword()); - - - // Validate datastore - Set output = pluginExecutor.validateDatasource(dsConfig); - assertTrue(output.isEmpty()); - // test connect - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - StepVerifier.create(dsConnectionMono) - .assertNext(Assertions::assertNotNull) - .verifyComplete(); - - /* Expect no error */ - StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) - .assertNext(datasourceTestResult -> assertEquals(0, datasourceTestResult.getInvalids().size())) - .verifyComplete(); - - } - - @Test - public void testExecute() { - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("show databases"); - - Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - assertNotNull(result.getBody()); - }) - .verifyComplete(); - } - - @Test - public void testExecuteWithFormattingWithShowCmd() { - dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("show\n\tdatabases"); - - Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - assertNotNull(result.getBody()); - String expectedBody = "[{\"Database\":\"information_schema\"},{\"Database\":\"test_db\"}]"; - assertEquals(expectedBody, result.getBody().toString()); - }) - .verifyComplete(); - } - - @Test - public void testExecuteWithFormattingWithSelectCmd() { - dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("select\n\t*\nfrom\nusers where id=1"); - - Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - assertNotNull(result.getBody()); - final JsonNode node = ((ArrayNode) result.getBody()).get(0); - assertEquals("2018-12-31", node.get("dob").asText()); - assertEquals("2018", node.get("yob").asText()); - assertEquals("Jack", node.get("username").asText()); - assertEquals("jill", node.get("password").asText()); - assertEquals("1", node.get("id").asText()); - assertEquals("jack@exemplars.com", node.get("email").asText()); - assertEquals("18:32:45", node.get("time1").asText()); - assertEquals("2018-11-30T20:45:15Z", node.get("created_on").asText()); - - /* - * - RequestParamDTO object only have attributes configProperty and value at this point. - * - The other two RequestParamDTO attributes - label and type are null at this point. - */ - List expectedRequestParams = new ArrayList<>(); - expectedRequestParams.add(new RequestParamDTO(ACTION_CONFIGURATION_BODY, - actionConfiguration.getBody(), null, null, new HashMap<>())); - assertEquals(result.getRequest().getRequestParams().toString(), expectedRequestParams.toString()); - }) - .verifyComplete(); - } - - @Test - public void testStaleConnectionCheck() { - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("show databases"); - Connection connection = pluginExecutor.datasourceCreate(dsConfig).block(); - - Flux resultFlux = Mono.from(connection.close()) - .thenMany(pluginExecutor.executeParameterized(connection, new ExecuteActionDTO(), dsConfig, actionConfiguration)); - - StepVerifier.create(resultFlux) - .expectErrorMatches(throwable -> throwable instanceof StaleConnectionException) - .verify(); - } - - @Test - public void testValidateDatasourceNullCredentials() { - dsConfig.setConnection(new com.appsmith.external.models.Connection()); - DBAuth auth = (DBAuth) dsConfig.getAuthentication(); - auth.setUsername(null); - auth.setPassword(null); - auth.setDatabaseName("someDbName"); - Set output = pluginExecutor.validateDatasource(dsConfig); - assertTrue(output.contains("Missing username for authentication.")); - assertTrue(output.contains("Missing password for authentication.")); - } - - @Test - public void testValidateDatasourceMissingDBName() { - ((DBAuth) dsConfig.getAuthentication()).setDatabaseName(""); - Set output = pluginExecutor.validateDatasource(dsConfig); - assertTrue(output - .stream() - .anyMatch(error -> error.contains("Missing database name.")) - ); - } - - @Test - public void testValidateDatasourceNullEndpoint() { - dsConfig.setEndpoints(null); - Set output = pluginExecutor.validateDatasource(dsConfig); - assertTrue(output - .stream() - .anyMatch(error -> error.contains("Missing endpoint and url")) - ); - } - - @Test - public void testValidateDatasource_NullHost() { - dsConfig.setEndpoints(List.of(new Endpoint())); - Set output = pluginExecutor.validateDatasource(dsConfig); - assertTrue(output - .stream() - .anyMatch(error -> error.contains("Host value cannot be empty")) - ); - - Endpoint endpoint = new Endpoint(); - endpoint.setHost(address); - endpoint.setPort(port.longValue()); - dsConfig.setEndpoints(List.of(endpoint)); - } - - @Test - public void testValidateDatasourceInvalidEndpoint() { - String hostname = "r2dbc:mysql://localhost"; - dsConfig.getEndpoints().get(0).setHost(hostname); - Set output = pluginExecutor.validateDatasource(dsConfig); - assertTrue(output.contains("Host value cannot contain `/` or `:` characters. Found `" + hostname + "`.")); - } - - @Test - public void testAliasColumnNames() { - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("SELECT id as user_id FROM users WHERE id = 1"); - - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); - - StepVerifier.create(executeMono) - .assertNext(result -> { - final JsonNode node = ((ArrayNode) result.getBody()).get(0); - assertArrayEquals( - new String[]{ - "user_id" - }, - new ObjectMapper() - .convertValue(node, LinkedHashMap.class) - .keySet() - .toArray() - ); - }) - .verifyComplete(); - - return; - } - - @Test - public void testPreparedStatementErrorWithIsKeyword() { - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); + MySqlPlugin.MySqlPluginExecutor pluginExecutor = new MySqlPlugin.MySqlPluginExecutor(); + + @SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is + // pseudo-optional. + @Container + public static MySQLContainer mySQLContainer = new MySQLContainer( + DockerImageName.parse("mysql/mysql-server:8.0.25").asCompatibleSubstituteFor("mysql")) + .withUsername("mysql") + .withPassword("password") + .withDatabaseName("test_db"); + + @SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is + // pseudo-optional. + @Container + public static MySQLContainer mySQLContainerWithInvalidTimezone = (MySQLContainer) new MySQLContainer( + DockerImageName.parse("mysql/mysql-server:8.0.25").asCompatibleSubstituteFor("mysql")) + .withUsername("root") + .withPassword("") + .withDatabaseName("test_db") + .withEnv("TZ", "PDT") + .withEnv("MYSQL_ROOT_HOST", "%"); + + private static String address; + private static Integer port; + private static String username; + private static String password; + private static String database; + private static DatasourceConfiguration dsConfig; + + @BeforeAll + public static void setUp() { + address = mySQLContainer.getContainerIpAddress(); + port = mySQLContainer.getFirstMappedPort(); + username = mySQLContainer.getUsername(); + password = mySQLContainer.getPassword(); + database = mySQLContainer.getDatabaseName(); + dsConfig = createDatasourceConfiguration(); + + ConnectionFactoryOptions baseOptions = MySQLR2DBCDatabaseContainer.getOptions(mySQLContainer); + ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); + + Mono.from(ConnectionFactories.get(ob.build()).create()) + .map(connection -> { + return connection.createBatch() + .add("DROP TABLE IF EXISTS possessions") + .add("DROP TABLE IF EXISTS users") + .add("create table users (\n" + + " id int auto_increment primary key,\n" + + " username varchar (250) unique not null,\n" + + + " password varchar (250) not null,\n" + + " email varchar (250) unique not null,\n" + + " spouse_dob date,\n" + + " dob date not null,\n" + + " yob year not null,\n" + + " time1 time not null,\n" + + " created_on timestamp not null,\n" + + " updated_on datetime not null,\n" + + " constraint unique index (username, email)\n" + + + ")") + .add("create table possessions (\n" + + " id int primary key,\n" + + " title varchar (250) not null,\n" + + " user_id int not null,\n" + + " username varchar (250) not null,\n" + + " email varchar (250) not null\n" + + ")") + .add("alter table possessions add foreign key (username, email) \n" + + + "references users (username, email)") + .add("SET SESSION sql_mode = '';\n") + .add("INSERT INTO users VALUES (" + + "1, 'Jack', 'jill', 'jack@exemplars.com', NULL, '2018-12-31', 2018," + + + " '18:32:45'," + + " '2018-11-30 20:45:15', '0000-00-00 00:00:00'" + + + ")") + .add("INSERT INTO users VALUES (" + + "2, 'Jill', 'jack', 'jill@exemplars.com', NULL, '2019-12-31', 2019," + + + " '15:45:30'," + + " '2019-11-30 23:59:59', '2019-11-30 23:59:59'" + + + ")"); + }) + .flatMapMany(batch -> Flux.from(batch.execute())) + .blockLast(); // wait until completion of all the queries + + return; + } + + private static DatasourceConfiguration createDatasourceConfiguration() { + DBAuth authDTO = new DBAuth(); + authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD); + authDTO.setUsername(username); + authDTO.setPassword(password); + authDTO.setDatabaseName(database); + + Endpoint endpoint = new Endpoint(); + endpoint.setHost(address); + endpoint.setPort(port.longValue()); + + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + + /* set endpoint */ + datasourceConfiguration.setAuthentication(authDTO); + datasourceConfiguration.setEndpoints(List.of(endpoint)); + + /* set ssl mode */ + datasourceConfiguration.setConnection(new com.appsmith.external.models.Connection()); + datasourceConfiguration.getConnection().setSsl(new SSLDetails()); + datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.DEFAULT); + + return datasourceConfiguration; + } + + @Test + public void testConnectMySQLContainer() { + + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + StepVerifier.create(dsConnectionMono) + .assertNext(Assertions::assertNotNull) + .verifyComplete(); + } + + @Test + public void testConnectMySQLContainerWithInvalidTimezone() { + + final DatasourceConfiguration dsConfig = createDatasourceConfigForContainerWithInvalidTZ(); + dsConfig.setProperties(List.of( + new Property("serverTimezone", "UTC"))); + + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + StepVerifier.create(dsConnectionMono) + .assertNext(Assertions::assertNotNull) + .verifyComplete(); + } + + @Test + public void testTestDatasource() { + dsConfig = createDatasourceConfiguration(); + + /* Expect no error */ + StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) + .assertNext(datasourceTestResult -> { + assertEquals(0, datasourceTestResult.getInvalids().size()); + }) + .verifyComplete(); + + /* Create bad datasource configuration and expect error */ + dsConfig.getEndpoints().get(0).setHost("badHost"); + StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) + .assertNext(datasourceTestResult -> { + assertNotEquals(0, datasourceTestResult.getInvalids().size()); + }) + .verifyComplete(); + + /* Reset dsConfig */ + dsConfig = createDatasourceConfiguration(); + } + + public DatasourceConfiguration createDatasourceConfigForContainerWithInvalidTZ() { + final DBAuth authDTO = new DBAuth(); + authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD); + authDTO.setUsername(mySQLContainerWithInvalidTimezone.getUsername()); + authDTO.setPassword(mySQLContainerWithInvalidTimezone.getPassword()); + authDTO.setDatabaseName(mySQLContainerWithInvalidTimezone.getDatabaseName()); + + final Endpoint endpoint = new Endpoint(); + endpoint.setHost(mySQLContainerWithInvalidTimezone.getContainerIpAddress()); + endpoint.setPort(mySQLContainerWithInvalidTimezone.getFirstMappedPort().longValue()); + + final DatasourceConfiguration dsConfig = new DatasourceConfiguration(); + + /* set endpoint */ + dsConfig.setAuthentication(authDTO); + dsConfig.setEndpoints(List.of(endpoint)); + + /* set ssl mode */ + + dsConfig.setConnection(new com.appsmith.external.models.Connection()); + dsConfig.getConnection().setMode(com.appsmith.external.models.Connection.Mode.READ_WRITE); + dsConfig.getConnection().setSsl(new SSLDetails()); + dsConfig.getConnection().getSsl().setAuthType(SSLDetails.AuthType.DEFAULT); + + return dsConfig; + } + + @Test + public void testDatasourceWithNullPassword() { + final DatasourceConfiguration dsConfig = createDatasourceConfigForContainerWithInvalidTZ(); + + // adding a user with empty password + ConnectionFactoryOptions baseOptions = MySQLR2DBCDatabaseContainer + .getOptions(mySQLContainerWithInvalidTimezone); + ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); + + Mono.from(ConnectionFactories.get(ob.build()).create()) + .map(connection -> connection.createBatch() + // adding a new user called 'mysql' with empty password + .add("CREATE USER 'mysql'@'%';\n" + + "GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'%' WITH GRANT OPTION;\n" + + + "FLUSH PRIVILEGES;")) + .flatMapMany(batch -> Flux.from(batch.execute())) + .blockLast(); // wait until completion of all the queries + + // change to ordinary user + DBAuth auth = ((DBAuth) dsConfig.getAuthentication()); + auth.setPassword(""); + auth.setUsername("mysql"); + + // check user pass + assertEquals("mysql", auth.getUsername()); + assertEquals("", auth.getPassword()); + + // Validate datastore + Set output = pluginExecutor.validateDatasource(dsConfig); + assertTrue(output.isEmpty()); + // test connect + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + StepVerifier.create(dsConnectionMono) + .assertNext(Assertions::assertNotNull) + .verifyComplete(); + + /* Expect no error */ + StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) + .assertNext(datasourceTestResult -> assertEquals(0, + datasourceTestResult.getInvalids().size())) + .verifyComplete(); + } + + @Test + public void testDatasourceWithRootUserAndNullPassword() { + + final DatasourceConfiguration dsConfig = createDatasourceConfigForContainerWithInvalidTZ(); + + // check user pass + assertEquals("root", mySQLContainerWithInvalidTimezone.getUsername()); + assertEquals("", mySQLContainerWithInvalidTimezone.getPassword()); + + // Validate datastore + Set output = pluginExecutor.validateDatasource(dsConfig); + assertTrue(output.isEmpty()); + // test connect + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + StepVerifier.create(dsConnectionMono) + .assertNext(Assertions::assertNotNull) + .verifyComplete(); + + /* Expect no error */ + StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) + .assertNext(datasourceTestResult -> assertEquals(0, + datasourceTestResult.getInvalids().size())) + .verifyComplete(); + + } + + @Test + public void testExecute() { + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("show databases"); + + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, + new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + }) + .verifyComplete(); + } + + @Test + public void testExecuteWithFormattingWithShowCmd() { + dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("show\n\tdatabases"); + + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, + new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + String expectedBody = "[{\"Database\":\"information_schema\"},{\"Database\":\"test_db\"}]"; + assertEquals(expectedBody, result.getBody().toString()); + }) + .verifyComplete(); + } + + @Test + public void testExecuteWithFormattingWithSelectCmd() { + dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("select\n\t*\nfrom\nusers where id=1"); + + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, + new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertEquals("2018-12-31", node.get("dob").asText()); + assertEquals("2018", node.get("yob").asText()); + assertEquals("Jack", node.get("username").asText()); + assertEquals("jill", node.get("password").asText()); + assertEquals("1", node.get("id").asText()); + assertEquals("jack@exemplars.com", node.get("email").asText()); + assertEquals("18:32:45", node.get("time1").asText()); + assertEquals("2018-11-30T20:45:15Z", node.get("created_on").asText()); + + /* + * - RequestParamDTO object only have attributes configProperty and value at + * this point. + * - The other two RequestParamDTO attributes - label and type are null at this + * point. + */ + List expectedRequestParams = new ArrayList<>(); + expectedRequestParams.add(new RequestParamDTO(ACTION_CONFIGURATION_BODY, + actionConfiguration.getBody(), null, null, new HashMap<>())); + assertEquals(result.getRequest().getRequestParams().toString(), + expectedRequestParams.toString()); + }) + .verifyComplete(); + } + + @Test + public void testStaleConnectionCheck() { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("show databases"); + Connection connection = pluginExecutor.datasourceCreate(dsConfig).block(); + + Flux resultFlux = Mono.from(connection.close()) + .thenMany(pluginExecutor.executeParameterized(connection, new ExecuteActionDTO(), + dsConfig, actionConfiguration)); + + StepVerifier.create(resultFlux) + .expectErrorMatches(throwable -> throwable instanceof StaleConnectionException) + .verify(); + } + + @Test + public void testValidateDatasourceNullCredentials() { + dsConfig.setConnection(new com.appsmith.external.models.Connection()); + DBAuth auth = (DBAuth) dsConfig.getAuthentication(); + auth.setUsername(null); + auth.setPassword(null); + auth.setDatabaseName("someDbName"); + Set output = pluginExecutor.validateDatasource(dsConfig); + assertTrue(output.contains("Missing username for authentication.")); + assertTrue(output.contains("Missing password for authentication.")); + } + + @Test + public void testValidateDatasourceMissingDBName() { + ((DBAuth) dsConfig.getAuthentication()).setDatabaseName(""); + Set output = pluginExecutor.validateDatasource(dsConfig); + assertTrue(output + .stream() + .anyMatch(error -> error.contains("Missing database name."))); + } + + @Test + public void testValidateDatasourceNullEndpoint() { + dsConfig.setEndpoints(null); + Set output = pluginExecutor.validateDatasource(dsConfig); + assertTrue(output + .stream() + .anyMatch(error -> error.contains("Missing endpoint and url"))); + } + + @Test + public void testValidateDatasource_NullHost() { + dsConfig.setEndpoints(List.of(new Endpoint())); + Set output = pluginExecutor.validateDatasource(dsConfig); + assertTrue(output + .stream() + .anyMatch(error -> error.contains("Host value cannot be empty"))); + + Endpoint endpoint = new Endpoint(); + endpoint.setHost(address); + endpoint.setPort(port.longValue()); + dsConfig.setEndpoints(List.of(endpoint)); + } + + @Test + public void testValidateDatasourceInvalidEndpoint() { + String hostname = "r2dbc:mysql://localhost"; + dsConfig.getEndpoints().get(0).setHost(hostname); + Set output = pluginExecutor.validateDatasource(dsConfig); + assertTrue(output.contains( + "Host value cannot contain `/` or `:` characters. Found `" + hostname + "`.")); + } + + @Test + public void testAliasColumnNames() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT id as user_id FROM users WHERE id = 1"); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), + dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertArrayEquals( + new String[] { + "user_id" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray()); + }) + .verifyComplete(); + + return; + } + + @Test + public void testPreparedStatementErrorWithIsKeyword() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + /** + * - MySQL r2dbc driver is not able to substitute the `True/False` value + * properly after the IS keyword. + * Converting `True/False` to integer 1 or 0 also does not work in this case as + * MySQL syntax does not support + * integers with IS keyword. + * - I have raised an issue with r2dbc to track it: + * https://github.com/mirromutth/r2dbc-mysql/issues/200 + */ + actionConfiguration.setBody("SELECT id FROM test_boolean_type WHERE c_boolean IS {{binding1}};"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("binding1"); + param1.setValue("True"); + param1.setClientDataType(ClientDataType.BOOLEAN); + params.add(param1); + + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, + actionConfiguration)); + + StepVerifier.create(executeMono) + .verifyErrorSatisfies(error -> { + assertTrue(error instanceof AppsmithPluginException); + String expectedMessage = "Appsmith currently does not support the IS keyword with the prepared " + + + "statement setting turned ON. Please re-write your SQL query without the IS keyword or " + + + "turn OFF (unsafe) the 'Use prepared statement' knob from the settings tab."; + assertTrue(expectedMessage.equals(error.getMessage())); + }); + } + + @Test + public void testPreparedStatementWithRealTypes() { + ConnectionFactoryOptions baseOptions = MySQLR2DBCDatabaseContainer.getOptions(mySQLContainer); + ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); + Mono.from(ConnectionFactories.get(ob.build()).create()) + .map(connection -> connection.createBatch() + .add("create table test_real_types(id int, c_float float, c_double double, c_real real)") + .add("insert into test_real_types values (1, 1.123, 3.123, 5.123)") + .add("insert into test_real_types values (2, 11.123, 13.123, 15.123)")) + .flatMapMany(batch -> Flux.from(batch.execute())) + .blockLast(); // wait until completion of all the queries + + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + /** + * - For mysql float / double / real types the actual values that are stored in + * the db my differ by a very + * thin margin as long as they are approximately same. Hence adding comparison + * based check instead of direct + * equality. + * - Ref: https://dev.mysql.com/doc/refman/8.0/en/problems-with-float.html + */ + actionConfiguration.setBody( + "SELECT id FROM test_real_types WHERE ABS(c_float - {{binding1}}) < 0.1 AND ABS" + + "(c_double - {{binding2}}) < 0.1 AND ABS(c_real - {{binding3}}) < 0.1;"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("binding1"); + param1.setValue("1.123"); + param1.setClientDataType(ClientDataType.NUMBER); + params.add(param1); + + Param param2 = new Param(); + param2.setKey("binding2"); + param2.setValue("3.123"); + param2.setClientDataType(ClientDataType.NUMBER); + params.add(param2); + + Param param3 = new Param(); + param3.setKey("binding3"); + param3.setValue("5.123"); + param3.setClientDataType(ClientDataType.NUMBER); + params.add(param3); + + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, + actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + final JsonNode node = ((ArrayNode) result.getBody()); + assertEquals(1, node.size()); + // Verify selected row id. + assertEquals(1, node.get(0).get("id").asInt()); + }) + .verifyComplete(); + + Mono.from(ConnectionFactories.get(ob.build()).create()) + .map(connection -> connection.createBatch() + .add("drop table test_real_types")) + .flatMapMany(batch -> Flux.from(batch.execute())) + .blockLast(); // wait until completion of all the queries + } + + @Test + public void testPreparedStatementWithBooleanType() { + // Create a new table with boolean type + ConnectionFactoryOptions baseOptions = MySQLR2DBCDatabaseContainer.getOptions(mySQLContainer); + ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); + Mono.from(ConnectionFactories.get(ob.build()).create()) + .map(connection -> connection.createBatch() + .add("create table test_boolean_type(id int, c_boolean boolean)") + .add("insert into test_boolean_type values (1, True)") + .add("insert into test_boolean_type values (2, True)") + .add("insert into test_boolean_type values (3, False)")) + .flatMapMany(batch -> Flux.from(batch.execute())) + .blockLast(); // wait until completion of all the queries + + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT id FROM test_boolean_type WHERE c_boolean={{binding1}};"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("binding1"); + param1.setValue("True"); + param1.setClientDataType(ClientDataType.BOOLEAN); + params.add(param1); + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, + actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + final JsonNode node = ((ArrayNode) result.getBody()); + assertEquals(2, node.size()); + // Verify selected row id. + assertEquals(1, node.get(0).get("id").asInt()); + assertEquals(2, node.get(1).get("id").asInt()); + }) + .verifyComplete(); + + Mono.from(ConnectionFactories.get(ob.build()).create()) + .map(connection -> connection.createBatch() + .add("drop table test_boolean_type")) + .flatMapMany(batch -> Flux.from(batch.execute())) + .blockLast(); // wait until completion of all the queries + } + + @Test + public void testExecuteWithPreparedStatement() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration + .setBody("SELECT id FROM users WHERE id = {{binding1}} limit 1 offset {{binding2}};"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("binding1"); + param1.setValue("1"); + param1.setClientDataType(ClientDataType.NUMBER); + params.add(param1); + Param param2 = new Param(); + param2.setKey("binding2"); + param2.setValue("0"); + param2.setClientDataType(ClientDataType.NUMBER); + params.add(param2); + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, + actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertArrayEquals( + new String[] { + "id" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray()); + + // Verify value + assertEquals(1, node.get("id").asInt()); + + /* + * - Check if request params are sent back properly. + * - Not replicating the same to other tests as the overall flow remains the + * same w.r.t. request + * params. + */ + + // Check if '?' is replaced by $i. + assertEquals("SELECT id FROM users WHERE id = $1 limit 1 offset $2;", + ((RequestParamDTO) (((List) result.getRequest() + .getRequestParams())).get(0)).getValue()); + + // Check 1st prepared statement parameter + PsParameterDTO expectedPsParam1 = new PsParameterDTO("1", "INTEGER"); + PsParameterDTO returnedPsParam1 = (PsParameterDTO) ((RequestParamDTO) (((List) result + .getRequest().getRequestParams())).get(0)) + .getSubstitutedParams().get("$1"); + // Check if prepared stmt param value is correctly sent back. + assertEquals(expectedPsParam1.getValue(), returnedPsParam1.getValue()); + // Check if prepared stmt param type is correctly sent back. + assertEquals(expectedPsParam1.getType(), returnedPsParam1.getType()); + + // Check 2nd prepared statement parameter + PsParameterDTO expectedPsParam2 = new PsParameterDTO("0", "INTEGER"); + PsParameterDTO returnedPsParam2 = (PsParameterDTO) ((RequestParamDTO) (((List) result + .getRequest().getRequestParams())).get(0)) + .getSubstitutedParams().get("$2"); + // Check if prepared stmt param value is correctly sent back. + assertEquals(expectedPsParam2.getValue(), returnedPsParam2.getValue()); + // Check if prepared stmt param type is correctly sent back. + assertEquals(expectedPsParam2.getType(), returnedPsParam2.getType()); + }) + .verifyComplete(); + + return; + } + + @Test + public void testExecuteDataTypes() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT * FROM users WHERE id = 1"); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), + dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertEquals("2018-12-31", node.get("dob").asText()); + assertEquals("2018", node.get("yob").asText()); + assertTrue(node.get("time1").asText().matches("\\d{2}:\\d{2}:\\d{2}")); + assertTrue(node.get("created_on").asText() + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")); + assertTrue(node.get("updated_on").isNull()); + + assertArrayEquals( + new String[] { + "id", + "username", + "password", + "email", + "spouse_dob", + "dob", + "yob", + "time1", + "created_on", + "updated_on" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray()); + }) + .verifyComplete(); + } + /** - * - MySQL r2dbc driver is not able to substitute the `True/False` value properly after the IS keyword. - * Converting `True/False` to integer 1 or 0 also does not work in this case as MySQL syntax does not support - * integers with IS keyword. - * - I have raised an issue with r2dbc to track it: https://github.com/mirromutth/r2dbc-mysql/issues/200 + * 1. Add a test to check that mysql driver can interpret and read all the + * regular data types used in mysql. + * 2. List of the data types is taken is from + * https://dev.mysql.com/doc/refman/8.0/en/data-types.html + * 3. Data types tested here are: INTEGER, SMALLINT, TINYINT, MEDIUMINT, BIGINT, + * DECIMAL, FLOAT, DOUBLE, BIT, + * DATE, DATETIME, TIMESTAMP, TIME, YEAR, CHAR, VARCHAR, BINARY, VARBINARY, + * TINYBLOB, BLOB, MEDIUMBLOB, LONGBLOB, + * TINYTEXT, TEXT, MEDIUMTEXT, LONGTEXT, ENUM, SET, JSON, GEOMETRY, POINT */ - actionConfiguration.setBody("SELECT id FROM test_boolean_type WHERE c_boolean IS {{binding1}};"); - - List pluginSpecifiedTemplates = new ArrayList<>(); - pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); - actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - List params = new ArrayList<>(); - Param param1 = new Param(); - param1.setKey("binding1"); - param1.setValue("True"); - params.add(param1); - - executeActionDTO.setParams(params); - - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, actionConfiguration)); - - StepVerifier.create(executeMono) - .verifyErrorSatisfies(error -> { - assertTrue(error instanceof AppsmithPluginException); - String expectedMessage = "Appsmith currently does not support the IS keyword with the prepared " + - "statement setting turned ON. Please re-write your SQL query without the IS keyword or " + - "turn OFF (unsafe) the 'Use prepared statement' knob from the settings tab."; - assertTrue(expectedMessage.equals(error.getMessage())); - }); - } - - @Test - public void testPreparedStatementWithRealTypes() { - ConnectionFactoryOptions baseOptions = MySQLR2DBCDatabaseContainer.getOptions(mySQLContainer); - ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); - Mono.from(ConnectionFactories.get(ob.build()).create()) - .map(connection -> - connection.createBatch() - .add("create table test_real_types(id int, c_float float, c_double double, c_real real)") - .add("insert into test_real_types values (1, 1.123, 3.123, 5.123)") - .add("insert into test_real_types values (2, 11.123, 13.123, 15.123)") - ) - .flatMapMany(batch -> Flux.from(batch.execute())) - .blockLast(); //wait until completion of all the queries - - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - /** - * - For mysql float / double / real types the actual values that are stored in the db my differ by a very - * thin margin as long as they are approximately same. Hence adding comparison based check instead of direct - * equality. - * - Ref: https://dev.mysql.com/doc/refman/8.0/en/problems-with-float.html - */ - actionConfiguration.setBody("SELECT id FROM test_real_types WHERE ABS(c_float - {{binding1}}) < 0.1 AND ABS" + - "(c_double - {{binding2}}) < 0.1 AND ABS(c_real - {{binding3}}) < 0.1;"); - - List pluginSpecifiedTemplates = new ArrayList<>(); - pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); - actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - List params = new ArrayList<>(); - Param param1 = new Param(); - param1.setKey("binding1"); - param1.setValue("1.123"); - params.add(param1); - - Param param2 = new Param(); - param2.setKey("binding2"); - param2.setValue("3.123"); - params.add(param2); - - Param param3 = new Param(); - param3.setKey("binding3"); - param3.setValue("5.123"); - params.add(param3); - - executeActionDTO.setParams(params); - - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, actionConfiguration)); - - StepVerifier.create(executeMono) - .assertNext(result -> { - final JsonNode node = ((ArrayNode) result.getBody()); - assertEquals(1, node.size()); - // Verify selected row id. - assertEquals(1, node.get(0).get("id").asInt()); - }) - .verifyComplete(); - - Mono.from(ConnectionFactories.get(ob.build()).create()) - .map(connection -> - connection.createBatch() - .add("drop table test_real_types") - ) - .flatMapMany(batch -> Flux.from(batch.execute())) - .blockLast(); //wait until completion of all the queries - } - - @Test - public void testPreparedStatementWithBooleanType() { - // Create a new table with boolean type - ConnectionFactoryOptions baseOptions = MySQLR2DBCDatabaseContainer.getOptions(mySQLContainer); - ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); - Mono.from(ConnectionFactories.get(ob.build()).create()) - .map(connection -> - connection.createBatch() - .add("create table test_boolean_type(id int, c_boolean boolean)") - .add("insert into test_boolean_type values (1, True)") - .add("insert into test_boolean_type values (2, True)") - .add("insert into test_boolean_type values (3, False)") - ) - .flatMapMany(batch -> Flux.from(batch.execute())) - .blockLast(); //wait until completion of all the queries - - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("SELECT id FROM test_boolean_type WHERE c_boolean={{binding1}};"); - - List pluginSpecifiedTemplates = new ArrayList<>(); - pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); - actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - List params = new ArrayList<>(); - Param param1 = new Param(); - param1.setKey("binding1"); - param1.setValue("True"); - params.add(param1); - executeActionDTO.setParams(params); - - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, actionConfiguration)); - - StepVerifier.create(executeMono) - .assertNext(result -> { - final JsonNode node = ((ArrayNode) result.getBody()); - assertEquals(2, node.size()); - // Verify selected row id. - assertEquals(1, node.get(0).get("id").asInt()); - assertEquals(2, node.get(1).get("id").asInt()); - }) - .verifyComplete(); - - Mono.from(ConnectionFactories.get(ob.build()).create()) - .map(connection -> - connection.createBatch() - .add("drop table test_boolean_type") - ) - .flatMapMany(batch -> Flux.from(batch.execute())) - .blockLast(); //wait until completion of all the queries - } - - @Test - public void testExecuteWithPreparedStatement() { - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("SELECT id FROM users WHERE id = {{binding1}} limit 1 offset {{binding2}};"); - - List pluginSpecifiedTemplates = new ArrayList<>(); - pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); - actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - List params = new ArrayList<>(); - Param param1 = new Param(); - param1.setKey("binding1"); - param1.setValue("1"); - params.add(param1); - Param param2 = new Param(); - param2.setKey("binding2"); - param2.setValue("0"); - params.add(param2); - executeActionDTO.setParams(params); - - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, actionConfiguration)); - - StepVerifier.create(executeMono) - .assertNext(result -> { - final JsonNode node = ((ArrayNode) result.getBody()).get(0); - assertArrayEquals( - new String[]{ - "id" - }, - new ObjectMapper() - .convertValue(node, LinkedHashMap.class) - .keySet() - .toArray() - ); - - // Verify value - assertEquals(1, node.get("id").asInt()); - - /* - * - Check if request params are sent back properly. - * - Not replicating the same to other tests as the overall flow remains the same w.r.t. request - * params. - */ - - // Check if '?' is replaced by $i. - assertEquals("SELECT id FROM users WHERE id = $1 limit 1 offset $2;", - ((RequestParamDTO) (((List) result.getRequest().getRequestParams())).get(0)).getValue()); - - // Check 1st prepared statement parameter - PsParameterDTO expectedPsParam1 = new PsParameterDTO("1", "INTEGER"); - PsParameterDTO returnedPsParam1 = - (PsParameterDTO) ((RequestParamDTO) (((List) result.getRequest().getRequestParams())).get(0)).getSubstitutedParams().get("$1"); - // Check if prepared stmt param value is correctly sent back. - assertEquals(expectedPsParam1.getValue(), returnedPsParam1.getValue()); - // Check if prepared stmt param type is correctly sent back. - assertEquals(expectedPsParam1.getType(), returnedPsParam1.getType()); - - // Check 2nd prepared statement parameter - PsParameterDTO expectedPsParam2 = new PsParameterDTO("0", "INTEGER"); - PsParameterDTO returnedPsParam2 = - (PsParameterDTO) ((RequestParamDTO) (((List) result.getRequest().getRequestParams())).get(0)).getSubstitutedParams().get("$2"); - // Check if prepared stmt param value is correctly sent back. - assertEquals(expectedPsParam2.getValue(), returnedPsParam2.getValue()); - // Check if prepared stmt param type is correctly sent back. - assertEquals(expectedPsParam2.getType(), returnedPsParam2.getType()); - }) - .verifyComplete(); - - return; - } - - @Test - public void testExecuteDataTypes() { - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("SELECT * FROM users WHERE id = 1"); - - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); - - StepVerifier.create(executeMono) - .assertNext(result -> { - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - assertNotNull(result.getBody()); - - final JsonNode node = ((ArrayNode) result.getBody()).get(0); - assertEquals("2018-12-31", node.get("dob").asText()); - assertEquals("2018", node.get("yob").asText()); - assertTrue(node.get("time1").asText().matches("\\d{2}:\\d{2}:\\d{2}")); - assertTrue(node.get("created_on").asText().matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")); - assertTrue(node.get("updated_on").isNull()); - - assertArrayEquals( - new String[]{ - "id", - "username", - "password", - "email", - "spouse_dob", - "dob", - "yob", - "time1", - "created_on", - "updated_on" - }, - new ObjectMapper() - .convertValue(node, LinkedHashMap.class) - .keySet() - .toArray() - ); - }) - .verifyComplete(); - } - - /** - * 1. Add a test to check that mysql driver can interpret and read all the regular data types used in mysql. - * 2. List of the data types is taken is from https://dev.mysql.com/doc/refman/8.0/en/data-types.html - * 3. Data types tested here are: INTEGER, SMALLINT, TINYINT, MEDIUMINT, BIGINT, DECIMAL, FLOAT, DOUBLE, BIT, - * DATE, DATETIME, TIMESTAMP, TIME, YEAR, CHAR, VARCHAR, BINARY, VARBINARY, TINYBLOB, BLOB, MEDIUMBLOB, LONGBLOB, - * TINYTEXT, TEXT, MEDIUMTEXT, LONGTEXT, ENUM, SET, JSON, GEOMETRY, POINT - */ - @Test - public void testExecuteDataTypesExtensive() throws AppsmithPluginException { - String query_create_table_numeric_types = "create table test_numeric_types (c_integer INTEGER, c_smallint " + - "SMALLINT, c_tinyint TINYINT, c_mediumint MEDIUMINT, c_bigint BIGINT, c_decimal DECIMAL, c_float " + - "FLOAT, c_double DOUBLE, c_bit BIT(10));"; - String query_insert_into_table_numeric_types = "insert into test_numeric_types values (-1, 1, 1, 10, 2000, 1" + - ".02345, 0.1234, 1.0102344, b'0101010');"; - - String query_create_table_date_time_types = "create table test_date_time_types (c_date DATE, c_datetime " + - "DATETIME DEFAULT CURRENT_TIMESTAMP, c_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, c_time TIME, " + - "c_year YEAR);"; - String query_insert_into_table_date_time_types = "insert into test_date_time_types values ('2020-12-01', " + - "'2020-12-01 20:20:20', '2020-12-01 20:20:20', '20:20:20', 2020);"; - - String query_create_table_data_types = "create table test_data_types (c_char CHAR(50), c_varchar VARCHAR(50)," + - " c_binary BINARY(20), c_varbinary VARBINARY(20), c_tinyblob TINYBLOB, c_blob BLOB, c_mediumblob " + - "MEDIUMBLOB, c_longblob LONGBLOB, c_tinytext TINYTEXT, c_text TEXT, c_mediumtext MEDIUMTEXT, " + - "c_longtext LONGTEXT, c_enum ENUM('ONE'), c_set SET('a'));"; - String query_insert_data_types = "insert into test_data_types values ('test', 'test', 'a\\0\\t', 'a\\0\\t', " + - "'test', 'test', 'test', 'test', 'test', 'test', 'test', 'test', 'ONE', 'a');"; - - String query_create_table_json_data_type = "create table test_json_type (c_json JSON);"; - String query_insert_json_data_type = "insert into test_json_type values ('{\"key1\": \"value1\", \"key2\": " + - "\"value2\"}');"; - - String query_create_table_geometry_types = "create table test_geometry_types (c_geometry GEOMETRY, c_point " + - "POINT);"; - String query_insert_geometry_types = "insert into test_geometry_types values (ST_GeomFromText('POINT(1 1)'), " + - "ST_PointFromText('POINT(1 100)'));"; - - String query_select_from_test_numeric_types = "select * from test_numeric_types;"; - String query_select_from_test_date_time_types = "select * from test_date_time_types;"; - String query_select_from_test_json_data_type = "select * from test_json_type;"; - String query_select_from_test_data_types = "select * from test_data_types;"; - String query_select_from_test_geometry_types = "select * from test_geometry_types;"; - - ConnectionFactoryOptions baseOptions = MySQLR2DBCDatabaseContainer.getOptions(mySQLContainer); - ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); - Mono.from(ConnectionFactories.get(ob.build()).create()) - .map(connection -> { - return connection.createBatch() - .add(query_create_table_numeric_types) - .add(query_insert_into_table_numeric_types) - .add(query_create_table_date_time_types) - .add(query_insert_into_table_date_time_types) - .add(query_create_table_json_data_type) - .add(query_insert_json_data_type) - .add(query_create_table_data_types) - .add(query_insert_data_types) - .add(query_create_table_geometry_types) - .add(query_insert_geometry_types); - }) - .flatMapMany(batch -> Flux.from(batch.execute())) - .blockLast(); //wait until completion of all the queries - - /* Test numeric types */ - testExecute(query_select_from_test_numeric_types); - /* Test date time types */ - testExecute(query_select_from_test_date_time_types); - /* Test data types */ - testExecute(query_select_from_test_data_types); - /* Test data types */ - testExecute(query_select_from_test_json_data_type); - /* Test data types */ - testExecute(query_select_from_test_geometry_types); - - return; - } - - private void testExecute(String query) { - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody(query); - Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - assertNotNull(result.getBody()); - }) - .verifyComplete(); - } - - @Test - public void testStructure() { - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono structureMono = pluginExecutor.datasourceCreate(dsConfig) - .flatMap(connection -> pluginExecutor.getStructure(connection, dsConfig)); - - StepVerifier.create(structureMono) - .assertNext(structure -> { - assertNotNull(structure); - assertEquals(2, structure.getTables().size()); - - final DatasourceStructure.Table possessionsTable = structure.getTables().get(0); - assertEquals("possessions", possessionsTable.getName()); - assertEquals(DatasourceStructure.TableType.TABLE, possessionsTable.getType()); - assertArrayEquals( - new DatasourceStructure.Column[]{ - new DatasourceStructure.Column("id", "int", null, false), - new DatasourceStructure.Column("title", "varchar", null, false), - new DatasourceStructure.Column("user_id", "int", null, false), - new DatasourceStructure.Column("username", "varchar", null, false), - new DatasourceStructure.Column("email", "varchar", null, false), - }, - possessionsTable.getColumns().toArray() - ); - - final DatasourceStructure.PrimaryKey possessionsPrimaryKey = - new DatasourceStructure.PrimaryKey("PRIMARY", List.of("id")); - final DatasourceStructure.ForeignKey possessionsUserForeignKey = new DatasourceStructure.ForeignKey( - "possessions_ibfk_1", - List.of("username", "email"), - List.of("users.username", "users.email") - ); - assertArrayEquals( - new DatasourceStructure.Key[]{possessionsPrimaryKey, possessionsUserForeignKey}, - possessionsTable.getKeys().toArray() - ); - - assertArrayEquals( - new DatasourceStructure.Template[]{ - new DatasourceStructure.Template("SELECT", "SELECT * FROM possessions LIMIT 10;"), - new DatasourceStructure.Template("INSERT", "INSERT INTO possessions (id, title, user_id, username, email)\n" + - " VALUES (1, '', 1, '', '');"), - new DatasourceStructure.Template("UPDATE", "UPDATE possessions SET\n" + - " id = 1,\n" + - " title = '',\n" + - " user_id = 1,\n" + - " username = '',\n" + - " email = ''\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), - new DatasourceStructure.Template("DELETE", "DELETE FROM possessions\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!"), - }, - possessionsTable.getTemplates().toArray() - ); - - final DatasourceStructure.Table usersTable = structure.getTables().get(1); - assertEquals("users", usersTable.getName()); - assertEquals(DatasourceStructure.TableType.TABLE, usersTable.getType()); - assertArrayEquals( - new DatasourceStructure.Column[]{ - new DatasourceStructure.Column("id", "int", null, true), - new DatasourceStructure.Column("username", "varchar", null, false), - new DatasourceStructure.Column("password", "varchar", null, false), - new DatasourceStructure.Column("email", "varchar", null, false), - new DatasourceStructure.Column("spouse_dob", "date", null, false), - new DatasourceStructure.Column("dob", "date", null, false), - new DatasourceStructure.Column("yob", "year", null, false), - new DatasourceStructure.Column("time1", "time", null, false), - new DatasourceStructure.Column("created_on", "timestamp", null, false), - new DatasourceStructure.Column("updated_on", "datetime", null, false) - }, - usersTable.getColumns().toArray() - ); - - final DatasourceStructure.PrimaryKey usersPrimaryKey = new DatasourceStructure.PrimaryKey("PRIMARY", List.of("id")); - assertArrayEquals( - new DatasourceStructure.Key[]{usersPrimaryKey}, - usersTable.getKeys().toArray() - ); - - assertArrayEquals( - new DatasourceStructure.Template[]{ - new DatasourceStructure.Template("SELECT", "SELECT * FROM users LIMIT 10;"), - new DatasourceStructure.Template("INSERT", "INSERT INTO users (id, username, password, email, spouse_dob, dob, yob, time1, created_on, updated_on)\n" + - " VALUES (1, '', '', '', '2019-07-01', '2019-07-01', '', '', '2019-07-01 10:00:00', '2019-07-01 10:00:00');"), - new DatasourceStructure.Template("UPDATE", "UPDATE users SET\n" + - " id = 1,\n" + - " username = '',\n" + - " password = '',\n" + - " email = '',\n" + - " spouse_dob = '2019-07-01',\n" + - " dob = '2019-07-01',\n" + - " yob = '',\n" + - " time1 = '',\n" + - " created_on = '2019-07-01 10:00:00',\n" + - " updated_on = '2019-07-01 10:00:00'\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), - new DatasourceStructure.Template("DELETE", "DELETE FROM users\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!"), - }, - usersTable.getTemplates().toArray() - ); - }) - .verifyComplete(); - } - - @Test - public void testSslToggleMissingError() { - DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); - datasourceConfiguration.getConnection().getSsl().setAuthType(null); - - Mono> invalidsMono = Mono.just(pluginExecutor) - .map(executor -> executor.validateDatasource(datasourceConfiguration)); - - - StepVerifier.create(invalidsMono) - .assertNext(invalids -> { - String expectedError = "Appsmith server has failed to fetch SSL configuration from datasource " + - "configuration form. Please reach out to Appsmith customer support to resolve this."; - assertTrue(invalids - .stream() - .anyMatch(error -> expectedError.equals(error)) - ); - }) - .verifyComplete(); - } - - @Test - public void testSslDisabled() { - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("show session status like 'Ssl_cipher'"); - - DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); - datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.DISABLED); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(datasourceConfiguration); - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, - actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - Object body = result.getBody(); - assertNotNull(body); - assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"\"}]", body.toString()); - }) - .verifyComplete(); - } - - @Test - public void testSslRequired() { - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("show session status like 'Ssl_cipher'"); - - DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); - datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.REQUIRED); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(datasourceConfiguration); - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, - actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - Object body = result.getBody(); - assertNotNull(body); - assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"ECDHE-RSA-AES128-GCM-SHA256\"}]", - body.toString()); - }) - .verifyComplete(); - } - - @Test - public void testSslPreferred() { - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("show session status like 'Ssl_cipher'"); - - DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); - datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.PREFERRED); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(datasourceConfiguration); - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, - actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - Object body = result.getBody(); - assertNotNull(body); - assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"ECDHE-RSA-AES128-GCM-SHA256\"}]", - body.toString()); - }) - .verifyComplete(); - } - - @Test - public void testSslDefault() { - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("show session status like 'Ssl_cipher'"); - - DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); - datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.DEFAULT); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(datasourceConfiguration); - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, - actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - Object body = result.getBody(); - assertNotNull(body); - assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"ECDHE-RSA-AES128-GCM-SHA256\"}]", - body.toString()); - }) - .verifyComplete(); - } - - @Test - public void testDuplicateColumnNames() { - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("SELECT id, username as id, password, email as password FROM users WHERE id = 1"); - - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); - - StepVerifier.create(executeMono) - .assertNext(result -> { - assertNotEquals(0, result.getMessages().size()); - - String expectedMessage = "Your MySQL query result may not have all the columns because duplicate column names " + - "were found for the column(s)"; - assertTrue( - result.getMessages().stream() - .anyMatch(message -> message.contains(expectedMessage)) - ); - - /* - * - Check if all of the duplicate column names are reported. - */ - Set expectedColumnNames = Stream.of("id", "password") - .collect(Collectors.toCollection(HashSet::new)); - Set foundColumnNames = new HashSet<>(); - result.getMessages().stream() - .filter(message -> message.contains(expectedMessage)) - .forEach(message -> { - Arrays.stream(message.split(":")[1].split("\\.")[0].split(",")) - .forEach(columnName -> foundColumnNames.add(columnName.trim())); - }); - assertTrue(expectedColumnNames.equals(foundColumnNames)); - }) - .verifyComplete(); - } - - @Test - public void testExecuteDescribeTableCmd() { - dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("describe users"); - - Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - assertNotNull(result.getBody()); - String expectedBody = "[{\"Field\":\"id\",\"Type\":\"int\",\"Null\":\"NO\",\"Key\":\"PRI\",\"Default\":null,\"Extra\":\"auto_increment\"},{\"Field\":\"username\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"UNI\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"password\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"email\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"UNI\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"spouse_dob\",\"Type\":\"date\",\"Null\":\"YES\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"dob\",\"Type\":\"date\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"yob\",\"Type\":\"year\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"time1\",\"Type\":\"time\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"created_on\",\"Type\":\"timestamp\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"updated_on\",\"Type\":\"datetime\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"}]"; - assertEquals(expectedBody, result.getBody().toString()); - }) - .verifyComplete(); - } - - @Test - public void testExecuteDescTableCmd() { - dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("desc users"); - - Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - assertNotNull(result.getBody()); - String expectedBody = "[{\"Field\":\"id\",\"Type\":\"int\",\"Null\":\"NO\",\"Key\":\"PRI\",\"Default\":null,\"Extra\":\"auto_increment\"},{\"Field\":\"username\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"UNI\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"password\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"email\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"UNI\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"spouse_dob\",\"Type\":\"date\",\"Null\":\"YES\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"dob\",\"Type\":\"date\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"yob\",\"Type\":\"year\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"time1\",\"Type\":\"time\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"created_on\",\"Type\":\"timestamp\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"updated_on\",\"Type\":\"datetime\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"}]"; - assertEquals(expectedBody, result.getBody().toString()); - }) - .verifyComplete(); - } -} + @Test + public void testExecuteDataTypesExtensive() throws AppsmithPluginException { + String query_create_table_numeric_types = "create table test_numeric_types (c_integer INTEGER, c_smallint " + + + "SMALLINT, c_tinyint TINYINT, c_mediumint MEDIUMINT, c_bigint BIGINT, c_decimal DECIMAL, c_float " + + + "FLOAT, c_double DOUBLE, c_bit BIT(10));"; + String query_insert_into_table_numeric_types = "insert into test_numeric_types values (-1, 1, 1, 10, 2000, 1" + + + ".02345, 0.1234, 1.0102344, b'0101010');"; + + String query_create_table_date_time_types = "create table test_date_time_types (c_date DATE, c_datetime " + + + "DATETIME DEFAULT CURRENT_TIMESTAMP, c_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, c_time TIME, " + + + "c_year YEAR);"; + String query_insert_into_table_date_time_types = "insert into test_date_time_types values ('2020-12-01', " + + + "'2020-12-01 20:20:20', '2020-12-01 20:20:20', '20:20:20', 2020);"; + + String query_create_table_data_types = "create table test_data_types (c_char CHAR(50), c_varchar VARCHAR(50)," + + + " c_binary BINARY(20), c_varbinary VARBINARY(20), c_tinyblob TINYBLOB, c_blob BLOB, c_mediumblob " + + + "MEDIUMBLOB, c_longblob LONGBLOB, c_tinytext TINYTEXT, c_text TEXT, c_mediumtext MEDIUMTEXT, " + + + "c_longtext LONGTEXT, c_enum ENUM('ONE'), c_set SET('a'));"; + String query_insert_data_types = "insert into test_data_types values ('test', 'test', 'a\\0\\t', 'a\\0\\t', " + + + "'test', 'test', 'test', 'test', 'test', 'test', 'test', 'test', 'ONE', 'a');"; + + String query_create_table_json_data_type = "create table test_json_type (c_json JSON);"; + String query_insert_json_data_type = "insert into test_json_type values ('{\"key1\": \"value1\", \"key2\": " + + + "\"value2\"}');"; + + String query_create_table_geometry_types = "create table test_geometry_types (c_geometry GEOMETRY, c_point " + + + "POINT);"; + String query_insert_geometry_types = "insert into test_geometry_types values (ST_GeomFromText('POINT(1 1)'), " + + + "ST_PointFromText('POINT(1 100)'));"; + + String query_select_from_test_numeric_types = "select * from test_numeric_types;"; + String query_select_from_test_date_time_types = "select * from test_date_time_types;"; + String query_select_from_test_json_data_type = "select * from test_json_type;"; + String query_select_from_test_data_types = "select * from test_data_types;"; + String query_select_from_test_geometry_types = "select * from test_geometry_types;"; + + ConnectionFactoryOptions baseOptions = MySQLR2DBCDatabaseContainer.getOptions(mySQLContainer); + ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); + Mono.from(ConnectionFactories.get(ob.build()).create()) + .map(connection -> { + return connection.createBatch() + .add(query_create_table_numeric_types) + .add(query_insert_into_table_numeric_types) + .add(query_create_table_date_time_types) + .add(query_insert_into_table_date_time_types) + .add(query_create_table_json_data_type) + .add(query_insert_json_data_type) + .add(query_create_table_data_types) + .add(query_insert_data_types) + .add(query_create_table_geometry_types) + .add(query_insert_geometry_types); + }) + .flatMapMany(batch -> Flux.from(batch.execute())) + .blockLast(); // wait until completion of all the queries + + /* Test numeric types */ + testExecute(query_select_from_test_numeric_types); + /* Test date time types */ + testExecute(query_select_from_test_date_time_types); + /* Test data types */ + testExecute(query_select_from_test_data_types); + /* Test data types */ + testExecute(query_select_from_test_json_data_type); + /* Test data types */ + testExecute(query_select_from_test_geometry_types); + + return; + } + + private void testExecute(String query) { + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody(query); + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, + new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + }) + .verifyComplete(); + } + + @Test + public void testStructure() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono structureMono = pluginExecutor.datasourceCreate(dsConfig) + .flatMap(connection -> pluginExecutor.getStructure(connection, dsConfig)); + + StepVerifier.create(structureMono) + .assertNext(structure -> { + assertNotNull(structure); + assertEquals(2, structure.getTables().size()); + + Optional possessionsTableOptional = structure + .getTables() + .stream() + .filter(table -> table.getName() + .equalsIgnoreCase("possessions")) + .findFirst(); + assertTrue(possessionsTableOptional.isPresent()); + final DatasourceStructure.Table possessionsTable = possessionsTableOptional + .get(); + assertEquals(DatasourceStructure.TableType.TABLE, possessionsTable.getType()); + assertArrayEquals( + new DatasourceStructure.Column[] { + new DatasourceStructure.Column("id", "int", + null, false), + new DatasourceStructure.Column("title", + "varchar", null, false), + new DatasourceStructure.Column("user_id", "int", + null, false), + new DatasourceStructure.Column("username", + "varchar", null, false), + new DatasourceStructure.Column("email", + "varchar", null, false), + }, + possessionsTable.getColumns().toArray()); + + final DatasourceStructure.PrimaryKey possessionsPrimaryKey = new DatasourceStructure.PrimaryKey( + "PRIMARY", List.of("id")); + final DatasourceStructure.ForeignKey possessionsUserForeignKey = new DatasourceStructure.ForeignKey( + "possessions_ibfk_1", + List.of("username", "email"), + List.of("users.username", "users.email")); + assertArrayEquals( + new DatasourceStructure.Key[] { possessionsPrimaryKey, + possessionsUserForeignKey }, + possessionsTable.getKeys().toArray()); + + assertArrayEquals( + new DatasourceStructure.Template[] { + new DatasourceStructure.Template("SELECT", + "SELECT * FROM possessions LIMIT 10;"), + new DatasourceStructure.Template("INSERT", + "INSERT INTO possessions (id, title, user_id, username, email)\n" + + + " VALUES (1, '', 1, '', '');"), + new DatasourceStructure.Template("UPDATE", + "UPDATE possessions SET\n" + + " id = 1,\n" + + + " title = '',\n" + + + " user_id = 1,\n" + + + " username = '',\n" + + + " email = ''\n" + + + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), + new DatasourceStructure.Template("DELETE", + "DELETE FROM possessions\n" + + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!"), + }, + possessionsTable.getTemplates().toArray()); + + Optional usersTableOptional = structure.getTables() + .stream() + .filter(table -> table.getName().equalsIgnoreCase("users")) + .findFirst(); + assertTrue(usersTableOptional.isPresent()); + final DatasourceStructure.Table usersTable = usersTableOptional.get(); + assertEquals(DatasourceStructure.TableType.TABLE, usersTable.getType()); + assertArrayEquals( + new DatasourceStructure.Column[] { + new DatasourceStructure.Column("id", "int", + null, true), + new DatasourceStructure.Column("username", + "varchar", null, false), + new DatasourceStructure.Column("password", + "varchar", null, false), + new DatasourceStructure.Column("email", + "varchar", null, false), + new DatasourceStructure.Column("spouse_dob", + "date", null, false), + new DatasourceStructure.Column("dob", "date", + null, false), + new DatasourceStructure.Column("yob", "year", + null, false), + new DatasourceStructure.Column("time1", "time", + null, false), + new DatasourceStructure.Column("created_on", + "timestamp", null, false), + new DatasourceStructure.Column("updated_on", + "datetime", null, false) + }, + usersTable.getColumns().toArray()); + + final DatasourceStructure.PrimaryKey usersPrimaryKey = new DatasourceStructure.PrimaryKey( + "PRIMARY", List.of("id")); + assertArrayEquals( + new DatasourceStructure.Key[] { usersPrimaryKey }, + usersTable.getKeys().toArray()); + + assertArrayEquals( + new DatasourceStructure.Template[] { + new DatasourceStructure.Template("SELECT", + "SELECT * FROM users LIMIT 10;"), + new DatasourceStructure.Template("INSERT", + "INSERT INTO users (id, username, password, email, spouse_dob, dob, yob, time1, created_on, updated_on)\n" + + + " VALUES (1, '', '', '', '2019-07-01', '2019-07-01', '', '', '2019-07-01 10:00:00', '2019-07-01 10:00:00');"), + new DatasourceStructure.Template("UPDATE", + "UPDATE users SET\n" + + " id = 1,\n" + + + " username = '',\n" + + + " password = '',\n" + + + " email = '',\n" + + + " spouse_dob = '2019-07-01',\n" + + + " dob = '2019-07-01',\n" + + + " yob = '',\n" + + + " time1 = '',\n" + + + " created_on = '2019-07-01 10:00:00',\n" + + + " updated_on = '2019-07-01 10:00:00'\n" + + + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), + new DatasourceStructure.Template("DELETE", + "DELETE FROM users\n" + + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!"), + }, + usersTable.getTemplates().toArray()); + }) + .verifyComplete(); + } + + @Test + public void testSslToggleMissingError() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + datasourceConfiguration.getConnection().getSsl().setAuthType(null); + + Mono> invalidsMono = Mono.just(pluginExecutor) + .map(executor -> executor.validateDatasource(datasourceConfiguration)); + + StepVerifier.create(invalidsMono) + .assertNext(invalids -> { + String expectedError = "Appsmith server has failed to fetch SSL configuration from datasource " + + + "configuration form. Please reach out to Appsmith customer support to resolve this."; + assertTrue(invalids + .stream() + .anyMatch(error -> expectedError.equals(error))); + }) + .verifyComplete(); + } + + @Test + public void testSslDisabled() { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("show session status like 'Ssl_cipher'"); + + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.DISABLED); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(datasourceConfiguration); + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), + dsConfig, + actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + Object body = result.getBody(); + assertNotNull(body); + assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"\"}]", + body.toString()); + }) + .verifyComplete(); + } + + @Test + public void testSslRequired() { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("show session status like 'Ssl_cipher'"); + + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.REQUIRED); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(datasourceConfiguration); + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), + dsConfig, + actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + Object body = result.getBody(); + assertNotNull(body); + assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"ECDHE-RSA-AES128-GCM-SHA256\"}]", + body.toString()); + }) + .verifyComplete(); + } + + @Test + public void testSslPreferred() { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("show session status like 'Ssl_cipher'"); + + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.PREFERRED); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(datasourceConfiguration); + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), + dsConfig, + actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + Object body = result.getBody(); + assertNotNull(body); + assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"ECDHE-RSA-AES128-GCM-SHA256\"}]", + body.toString()); + }) + .verifyComplete(); + } + + @Test + public void testSslDefault() { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("show session status like 'Ssl_cipher'"); + + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.DEFAULT); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(datasourceConfiguration); + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), + dsConfig, + actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + Object body = result.getBody(); + assertNotNull(body); + assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"ECDHE-RSA-AES128-GCM-SHA256\"}]", + body.toString()); + }) + .verifyComplete(); + } + + @Test + public void testDuplicateColumnNames() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody( + "SELECT id, username as id, password, email as password FROM users WHERE id = 1"); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), + dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertNotEquals(0, result.getMessages().size()); + + String expectedMessage = "Your MySQL query result may not have all the columns because duplicate column names " + + + "were found for the column(s)"; + assertTrue( + result.getMessages().stream() + .anyMatch(message -> message + .contains(expectedMessage))); + + /* + * - Check if all of the duplicate column names are reported. + */ + Set expectedColumnNames = Stream.of("id", "password") + .collect(Collectors.toCollection(HashSet::new)); + Set foundColumnNames = new HashSet<>(); + result.getMessages().stream() + .filter(message -> message.contains(expectedMessage)) + .forEach(message -> { + Arrays.stream(message.split(":")[1].split("\\.")[0] + .split(",")) + .forEach(columnName -> foundColumnNames + .add(columnName.trim())); + }); + assertTrue(expectedColumnNames.equals(foundColumnNames)); + }) + .verifyComplete(); + } + + @Test + public void testExecuteDescribeTableCmd() { + dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("describe users"); + + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, + new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + String expectedBody = "[{\"Field\":\"id\",\"Type\":\"int\",\"Null\":\"NO\",\"Key\":\"PRI\",\"Default\":null,\"Extra\":\"auto_increment\"},{\"Field\":\"username\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"UNI\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"password\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"email\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"UNI\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"spouse_dob\",\"Type\":\"date\",\"Null\":\"YES\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"dob\",\"Type\":\"date\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"yob\",\"Type\":\"year\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"time1\",\"Type\":\"time\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"created_on\",\"Type\":\"timestamp\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"updated_on\",\"Type\":\"datetime\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"}]"; + assertEquals(expectedBody, result.getBody().toString()); + }) + .verifyComplete(); + } + + @Test + public void testExecuteDescTableCmd() { + dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("desc users"); + + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, + new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + String expectedBody = "[{\"Field\":\"id\",\"Type\":\"int\",\"Null\":\"NO\",\"Key\":\"PRI\",\"Default\":null,\"Extra\":\"auto_increment\"},{\"Field\":\"username\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"UNI\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"password\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"email\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"UNI\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"spouse_dob\",\"Type\":\"date\",\"Null\":\"YES\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"dob\",\"Type\":\"date\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"yob\",\"Type\":\"year\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"time1\",\"Type\":\"time\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"created_on\",\"Type\":\"timestamp\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"updated_on\",\"Type\":\"datetime\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"}]"; + assertEquals(expectedBody, result.getBody().toString()); + }) + .verifyComplete(); + } + + @Test + public void testNullObjectWithPreparedStatement() { + pluginExecutor = spy(new MySqlPlugin.MySqlPluginExecutor()); + doReturn(false).when(pluginExecutor).isIsOperatorUsed(any()); + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT * from (\n" + + "\tselect 'Appsmith' as company_name, true as open_source\n" + + "\tunion\n" + + "\tselect 'Retool' as company_name, false as open_source\n" + + "\tunion\n" + + "\tselect 'XYZ' as company_name, null as open_source\n" + + ") t\n" + + "where t.open_source IS {{binding1}}"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("binding1"); + param1.setValue(null); + param1.setClientDataType(ClientDataType.NULL); + params.add(param1); + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, + actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertTrue(result.getIsExecutionSuccess()); + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertArrayEquals( + new String[] { + "company_name", + "open_source" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray()); + + // Verify value + assertEquals(JsonNodeType.NULL, node.get("open_source").getNodeType()); + + }) + .verifyComplete(); + } + + @Test + public void testNullAsStringWithPreparedStatement() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT * from (\n" + + "\tselect 'Appsmith' as company_name, true as open_source\n" + + "\tunion\n" + + "\tselect 'Retool' as company_name, false as open_source\n" + + "\tunion\n" + + "\tselect 'XYZ' as company_name, 'null' as open_source\n" + + ") t\n" + + "where t.open_source = {{binding1}};"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("binding1"); + param1.setValue("null"); + param1.setClientDataType(ClientDataType.STRING); + params.add(param1); + + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, + actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertTrue(result.getIsExecutionSuccess()); + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertArrayEquals( + new String[] { + "company_name", + "open_source" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray()); + + // Verify value + assertEquals(JsonNodeType.STRING, node.get("open_source").getNodeType()); + + }) + .verifyComplete(); + } + + @Test + public void testNumericValuesHavingLeadingZeroWithPreparedStatement() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT {{binding1}} as numeric_string;"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("binding1"); + param1.setValue("098765"); + param1.setClientDataType(ClientDataType.STRING); + params.add(param1); + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, + actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertTrue(result.getIsExecutionSuccess()); + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertArrayEquals( + new String[] { + "numeric_string" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray()); + + // Verify value + assertEquals(JsonNodeType.STRING, node.get("numeric_string").getNodeType()); + assertEquals(param1.getValue(), node.get("numeric_string").asText()); + + }) + .verifyComplete(); + } + + @Test + public void testLongValueWithPreparedStatement() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("select id from users LIMIT {{binding1}}"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("binding1"); + param1.setValue("2147483648"); + param1.setClientDataType(ClientDataType.NUMBER); + params.add(param1); + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, + actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertTrue(result.getIsExecutionSuccess()); + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertArrayEquals( + new String[] { + "id" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray()); + + // Verify value + assertEquals(JsonNodeType.NUMBER, node.get("id").getNodeType()); + + }) + .verifyComplete(); + } +} \ No newline at end of file