diff --git a/mysql-dual-conn/docker-compose.yml b/mysql-dual-conn/docker-compose.yml new file mode 100644 index 00000000..60357611 --- /dev/null +++ b/mysql-dual-conn/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3.8' +services: + mysql: + image: mysql:8.0 + container_name: mysql-dual-conn + environment: + MYSQL_ROOT_PASSWORD: rootpass + ports: + - "3306:3306" + volumes: + - ./init.sql:/docker-entrypoint-initdb.d/init.sql diff --git a/mysql-dual-conn/init.sql b/mysql-dual-conn/init.sql new file mode 100644 index 00000000..d1c2a791 --- /dev/null +++ b/mysql-dual-conn/init.sql @@ -0,0 +1,10 @@ +CREATE DATABASE IF NOT EXISTS myntra_oms; +CREATE DATABASE IF NOT EXISTS camunda; + +CREATE USER IF NOT EXISTS 'omsAppUser'@'%' IDENTIFIED BY 'omsPassword'; +GRANT ALL PRIVILEGES ON myntra_oms.* TO 'omsAppUser'@'%'; + +CREATE USER IF NOT EXISTS 'stagebuster'@'%' IDENTIFIED BY 'camundaPassword'; +GRANT ALL PRIVILEGES ON camunda.* TO 'stagebuster'@'%'; + +FLUSH PRIVILEGES; diff --git a/mysql-dual-conn/pom.xml b/mysql-dual-conn/pom.xml new file mode 100644 index 00000000..00d95024 --- /dev/null +++ b/mysql-dual-conn/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.example + mysql-dual-conn + 0.0.1-SNAPSHOT + mysql-dual-conn + E2E test for Keploy MySQL multi-connection handshake matching + + + 17 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-jdbc + + + com.mysql + mysql-connector-j + runtime + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/mysql-dual-conn/src/main/java/com/example/mysqlreplicate/DataSourceConfig.java b/mysql-dual-conn/src/main/java/com/example/mysqlreplicate/DataSourceConfig.java new file mode 100644 index 00000000..00133952 --- /dev/null +++ b/mysql-dual-conn/src/main/java/com/example/mysqlreplicate/DataSourceConfig.java @@ -0,0 +1,108 @@ +package com.example.mysqlreplicate; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; + +/** + * Replicates the multi-datasource setup that triggers the Keploy + * "no mysql mocks matched the HandshakeResponse41" error. + * + * Two HikariCP pools connect to the SAME MySQL server but with + * DIFFERENT usernames and databases. During Keploy test replay, + * each new TCP connection triggers simulateInitialHandshake which + * must match the client's HandshakeResponse41 against recorded + * config mocks. The mismatch occurs because: + * + * 1. mocks[0] (always omsAppUser/myntra_oms) is used to send the + * server greeting to ALL incoming connections. + * 2. When the camunda pool connects, its HandshakeResponse41 has + * username=stagebuster, database=camunda, and different + * capability_flags (423535119 vs 20881935). + * 3. The matcher loops all config mocks looking for a match on + * username + database + capability_flags + charset + filler. + * If no recorded config mock matches those exact fields, the + * error fires. + */ +@Configuration +public class DataSourceConfig { + + // ---- OMS pool (matches omsAppUser / myntra_oms mocks) ---- + + @Value("${datasource.oms.jdbc-url}") + private String omsJdbcUrl; + + @Value("${datasource.oms.username}") + private String omsUsername; + + @Value("${datasource.oms.password}") + private String omsPassword; + + @Value("${datasource.oms.driver-class-name}") + private String omsDriverClass; + + // ---- Camunda pool (matches stagebuster / camunda mocks) ---- + + @Value("${datasource.camunda.jdbc-url}") + private String camundaJdbcUrl; + + @Value("${datasource.camunda.username}") + private String camundaUsername; + + @Value("${datasource.camunda.password}") + private String camundaPassword; + + @Value("${datasource.camunda.driver-class-name}") + private String camundaDriverClass; + + @Bean(name = "omsDataSource", destroyMethod = "close") + @Primary + public HikariDataSource omsDataSource() { + HikariConfig config = new HikariConfig(); + config.setPoolName("oms-dataSource"); + config.setUsername(omsUsername); + config.setPassword(omsPassword); + return buildDataSource(config, 5, omsJdbcUrl, omsDriverClass); + } + + @Bean(name = "camundaDataSource", destroyMethod = "close") + public HikariDataSource camundaDataSource() { + HikariConfig config = new HikariConfig(); + config.setPoolName("camunda-dataSource"); + config.setUsername(camundaUsername); + config.setPassword(camundaPassword); + return buildDataSource(config, 5, camundaJdbcUrl, camundaDriverClass); + } + + @Bean(name = "omsJdbcTemplate") + @Primary + public JdbcTemplate omsJdbcTemplate() { + return new JdbcTemplate(omsDataSource()); + } + + @Bean(name = "camundaJdbcTemplate") + public JdbcTemplate camundaJdbcTemplate() { + return new JdbcTemplate(camundaDataSource()); + } + + private HikariDataSource buildDataSource(HikariConfig config, int maxConns, + String jdbcUrl, String driverClass) { + config.setMaximumPoolSize(maxConns); + config.setMinimumIdle(2); + config.setKeepaliveTime(5000); + config.setIdleTimeout(10000); + config.setConnectionTimeout(5000); + config.setValidationTimeout(2000); + config.setMaxLifetime(7200000); + config.setLeakDetectionThreshold(2000); + config.setDriverClassName(driverClass); + config.setJdbcUrl(jdbcUrl); + return new HikariDataSource(config); + } +} diff --git a/mysql-dual-conn/src/main/java/com/example/mysqlreplicate/MysqlReplicateApplication.java b/mysql-dual-conn/src/main/java/com/example/mysqlreplicate/MysqlReplicateApplication.java new file mode 100644 index 00000000..ec6b77f9 --- /dev/null +++ b/mysql-dual-conn/src/main/java/com/example/mysqlreplicate/MysqlReplicateApplication.java @@ -0,0 +1,11 @@ +package com.example.mysqlreplicate; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MysqlReplicateApplication { + public static void main(String[] args) { + SpringApplication.run(MysqlReplicateApplication.class, args); + } +} diff --git a/mysql-dual-conn/src/main/java/com/example/mysqlreplicate/QueryController.java b/mysql-dual-conn/src/main/java/com/example/mysqlreplicate/QueryController.java new file mode 100644 index 00000000..376ac6e0 --- /dev/null +++ b/mysql-dual-conn/src/main/java/com/example/mysqlreplicate/QueryController.java @@ -0,0 +1,59 @@ +package com.example.mysqlreplicate; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Simple REST controller that queries both datasources. + * Each endpoint triggers a DB query that forces a connection + * from the respective pool — reproducing the multi-handshake + * scenario during Keploy replay. + */ +@RestController +public class QueryController { + + private final JdbcTemplate omsJdbc; + private final JdbcTemplate camundaJdbc; + + public QueryController(@Qualifier("omsJdbcTemplate") JdbcTemplate omsJdbc, + @Qualifier("camundaJdbcTemplate") JdbcTemplate camundaJdbc) { + this.omsJdbc = omsJdbc; + this.camundaJdbc = camundaJdbc; + } + + /** + * Queries both databases, triggering connections from both pools. + * During Keploy test mode this forces two distinct HandshakeResponse41 + * packets with different username/database/capability_flags values. + */ + @GetMapping("/api/query-both") + public Map queryBoth() { + Map result = new HashMap<>(); + + // OMS query — user=omsAppUser, db=myntra_oms + List> omsResult = omsJdbc.queryForList("SELECT 1 AS oms_check"); + result.put("oms", omsResult); + + // Camunda query — user=stagebuster, db=camunda + List> camundaResult = camundaJdbc.queryForList("SELECT 1 AS camunda_check"); + result.put("camunda", camundaResult); + + return result; + } + + @GetMapping("/api/oms") + public List> queryOms() { + return omsJdbc.queryForList("SELECT 1 AS oms_check"); + } + + @GetMapping("/api/camunda") + public List> queryCamunda() { + return camundaJdbc.queryForList("SELECT 1 AS camunda_check"); + } +} diff --git a/mysql-dual-conn/src/main/resources/application.properties b/mysql-dual-conn/src/main/resources/application.properties new file mode 100644 index 00000000..9849d6cb --- /dev/null +++ b/mysql-dual-conn/src/main/resources/application.properties @@ -0,0 +1,15 @@ +server.port=8080 + +# --- OMS DataSource (primary) --- +datasource.oms.jdbc-url=jdbc:mysql://localhost:3306/myntra_oms?useSSL=false +datasource.oms.username=omsAppUser +datasource.oms.password=omsPassword +datasource.oms.driver-class-name=com.mysql.cj.jdbc.Driver + +# --- Camunda DataSource (secondary, different user & capability flags) --- +# allowPublicKeyRetrieval=true causes different MySQL capability flags, +# which is the key condition that triggers the multi-handshake matching bug. +datasource.camunda.jdbc-url=jdbc:mysql://localhost:3306/camunda?useSSL=false&allowPublicKeyRetrieval=true +datasource.camunda.username=stagebuster +datasource.camunda.password=camundaPassword +datasource.camunda.driver-class-name=com.mysql.cj.jdbc.Driver