From 10a8b50756a092672d9dd7922c993886b26a28d6 Mon Sep 17 00:00:00 2001 From: slayerjain Date: Fri, 20 Mar 2026 21:19:06 +0530 Subject: [PATCH] feat: add mysql-dual-conn E2E sample app Spring Boot app with two HikariCP connection pools using different MySQL credentials and JDBC URL parameters. Tests that Keploy correctly matches HandshakeResponse41 packets during replay when capability flags differ between connections. --- mysql-dual-conn/docker-compose.yml | 11 ++ mysql-dual-conn/init.sql | 10 ++ mysql-dual-conn/pom.xml | 48 ++++++++ .../mysqlreplicate/DataSourceConfig.java | 108 ++++++++++++++++++ .../MysqlReplicateApplication.java | 11 ++ .../mysqlreplicate/QueryController.java | 59 ++++++++++ .../src/main/resources/application.properties | 15 +++ 7 files changed, 262 insertions(+) create mode 100644 mysql-dual-conn/docker-compose.yml create mode 100644 mysql-dual-conn/init.sql create mode 100644 mysql-dual-conn/pom.xml create mode 100644 mysql-dual-conn/src/main/java/com/example/mysqlreplicate/DataSourceConfig.java create mode 100644 mysql-dual-conn/src/main/java/com/example/mysqlreplicate/MysqlReplicateApplication.java create mode 100644 mysql-dual-conn/src/main/java/com/example/mysqlreplicate/QueryController.java create mode 100644 mysql-dual-conn/src/main/resources/application.properties 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