diff --git a/cli/flamingock-cli/CLI-README.md b/cli/flamingock-cli/CLI-README.md index 941a75736..5fd02e36b 100644 --- a/cli/flamingock-cli/CLI-README.md +++ b/cli/flamingock-cli/CLI-README.md @@ -121,13 +121,26 @@ flamingock: service-identifier: "flamingock-cli" audit: couchbase: - endpoint: "http://localhost:8000" + endpoint: "couchbase://localhost:12110" username: "your-username" password: "your-password" bucket-name: "test" table: "flamingockAuditLog" # Optional, defaults to "flamingockAuditLog" ``` +### SQL Example +```yaml +flamingock: + service-identifier: "flamingock-cli" + audit: + sql: + endpoint: "jdbc:sqlserver://localhost:1433/test-db" + username: "your-username" + password: "your-password" + sql-dialect: "SqlServer" + table: "flamingockAuditLog" # Optional, defaults to "flamingockAuditLog" +``` + ### Configuration File Resolution 1. Command line argument: `--config /path/to/file.yml` 2. Default: `flamingock-cli.yml` in bin directory diff --git a/cli/flamingock-cli/build.gradle.kts b/cli/flamingock-cli/build.gradle.kts index 09aebf1fd..150b0b9f8 100644 --- a/cli/flamingock-cli/build.gradle.kts +++ b/cli/flamingock-cli/build.gradle.kts @@ -10,6 +10,7 @@ description = "Command-line interface for Flamingock audit and issue management dependencies { implementation(project(":core:flamingock-core")) implementation(project(":community:flamingock-community")) + implementation(project(":utils:sql-util")) // CLI framework implementation("info.picocli:picocli:4.7.5") @@ -25,6 +26,20 @@ dependencies { implementation("software.amazon.awssdk:dynamodb:2.20.0") implementation ("com.couchbase.client:java-client:3.7.3") + // SQL drivers + implementation("mysql:mysql-connector-java:8.0.33") + implementation("com.microsoft.sqlserver:mssql-jdbc:12.4.2.jre8") + implementation("com.oracle.database.jdbc:ojdbc8:21.9.0.0") + implementation("org.postgresql:postgresql:42.7.3") + implementation("org.mariadb.jdbc:mariadb-java-client:3.3.2") + implementation("com.h2database:h2:2.2.224") + implementation("org.xerial:sqlite-jdbc:3.41.2.1") + implementation("com.ibm.informix:jdbc:4.50.10") + implementation("org.firebirdsql.jdbc:jaybird:4.0.10.java8") + + // HikariCP for SQL database connection pooling + implementation("com.zaxxer:HikariCP:3.4.5") + // SLF4J API - needed for interface compatibility (provided by flamingock-core) // implementation("org.slf4j:slf4j-api:1.7.36") // Already provided by core dependencies @@ -37,8 +52,14 @@ dependencies { testImplementation("org.assertj:assertj-core:3.24.2") testImplementation("org.testcontainers:junit-jupiter:1.19.3") testImplementation("org.testcontainers:mongodb:1.19.3") - testImplementation("org.testcontainers:couchbase:1.21.3") + testImplementation("org.testcontainers:couchbase:1.19.3") testImplementation("com.github.stefanbirkner:system-lambda:1.2.1") + // SQL Testcontainers + testImplementation("org.testcontainers:mysql:1.19.3") + testImplementation("org.testcontainers:mssqlserver:1.19.3") + testImplementation("org.testcontainers:oracle-xe:1.19.3") + testImplementation("org.testcontainers:postgresql:1.19.3") + testImplementation("org.testcontainers:mariadb:1.19.3") } @@ -50,6 +71,7 @@ val uberJar by tasks.registering(Jar::class) { archiveBaseName.set("flamingock-cli") archiveClassifier.set("uber") archiveVersion.set(project.version.toString()) + isZip64 = true duplicatesStrategy = DuplicatesStrategy.EXCLUDE @@ -63,7 +85,9 @@ val uberJar by tasks.registering(Jar::class) { dependsOn(configurations.runtimeClasspath) from({ configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) } - }) + }) { + exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA") + } } val createScripts by tasks.registering(CreateStartScripts::class) { diff --git a/cli/flamingock-cli/src/dist/flamingock-cli.yml b/cli/flamingock-cli/src/dist/flamingock-cli.yml index be0c96861..9b99267d3 100644 --- a/cli/flamingock-cli/src/dist/flamingock-cli.yml +++ b/cli/flamingock-cli/src/dist/flamingock-cli.yml @@ -26,3 +26,11 @@ flamingock: # password: "your-password" # bucket-name: "test" # table: "flamingockAuditLog" # Optional, defaults to "flamingockAuditLog" + + # SQL Configuration (uncomment and modify to use) + # sql: + # endpoint: "jdbc:sqlserver://localhost:1433/test-db" + # username: "your-username" + # password: "your-password" + # sql-dialect: "SqlServer" + # table: "flamingockAuditLog" # Optional, defaults to "flamingockAuditLog" diff --git a/cli/flamingock-cli/src/main/java/io/flamingock/cli/config/ConfigLoader.java b/cli/flamingock-cli/src/main/java/io/flamingock/cli/config/ConfigLoader.java index 2f98f85df..2387e678a 100644 --- a/cli/flamingock-cli/src/main/java/io/flamingock/cli/config/ConfigLoader.java +++ b/cli/flamingock-cli/src/main/java/io/flamingock/cli/config/ConfigLoader.java @@ -104,6 +104,20 @@ private static FlamingockConfig parseConfig(Map yamlData) { databaseConfig.setCouchbase(couchbaseConfig); } + Map sqlData = (Map) auditData.get("sql"); + if (sqlData != null) { + DatabaseConfig.SqlConfig sqlConfig = new DatabaseConfig.SqlConfig(); + sqlConfig.setEndpoint((String) sqlData.get("endpoint")); + sqlConfig.setUsername((String) sqlData.get("username")); + sqlConfig.setPassword((String) sqlData.get("password")); + sqlConfig.setSqlDialect((String) sqlData.get("sql-dialect")); + sqlConfig.setTable((String) sqlData.get("table")); + if (sqlData.get("properties") != null) { + sqlConfig.setProperties((Map) sqlData.get("properties")); + } + databaseConfig.setSql(sqlConfig); + } + config.setAudit(databaseConfig); } @@ -118,8 +132,9 @@ public static DatabaseType detectDatabaseType(FlamingockConfig config) { boolean hasMongoDB = config.getAudit().getMongodb() != null; boolean hasDynamoDB = config.getAudit().getDynamodb() != null; boolean hasCouchbase = config.getAudit().getCouchbase() != null; + boolean hasSql = config.getAudit().getSql() != null; - if (Stream.of(hasMongoDB, hasDynamoDB, hasCouchbase) + if (Stream.of(hasMongoDB, hasDynamoDB, hasCouchbase, hasSql) .filter(b -> b) .count()>1) { throw new IllegalArgumentException("Multiple database configurations found. Please configure only one database type."); @@ -131,12 +146,14 @@ public static DatabaseType detectDatabaseType(FlamingockConfig config) { return DatabaseType.DYNAMODB; } else if (hasCouchbase) { return DatabaseType.COUCHBASE; + } else if (hasSql) { + return DatabaseType.SQL; } else { - throw new IllegalArgumentException("No supported database configuration found. Please configure MongoDB, DynamoDB or Couchbase."); + throw new IllegalArgumentException("No supported database configuration found. Please configure MongoDB, DynamoDB, Couchbase or SQL."); } } public enum DatabaseType { - MONGODB, DYNAMODB, COUCHBASE + MONGODB, DYNAMODB, COUCHBASE, SQL } } diff --git a/cli/flamingock-cli/src/main/java/io/flamingock/cli/config/DatabaseConfig.java b/cli/flamingock-cli/src/main/java/io/flamingock/cli/config/DatabaseConfig.java index 88c06e0cb..ebe859623 100644 --- a/cli/flamingock-cli/src/main/java/io/flamingock/cli/config/DatabaseConfig.java +++ b/cli/flamingock-cli/src/main/java/io/flamingock/cli/config/DatabaseConfig.java @@ -15,12 +15,15 @@ */ package io.flamingock.cli.config; +import io.flamingock.internal.common.sql.SqlDialect; + import java.util.Map; public class DatabaseConfig { private MongoDBConfig mongodb; private DynamoDBConfig dynamodb; private CouchbaseConfig couchbase; + private SqlConfig sql; public MongoDBConfig getMongodb() { return mongodb; @@ -46,6 +49,14 @@ public void setCouchbase(CouchbaseConfig couchbase) { this.couchbase = couchbase; } + public SqlConfig getSql() { + return sql; + } + + public void setSql(SqlConfig sql) { + this.sql = sql; + } + public static class MongoDBConfig { private String connectionString; private String database; @@ -225,4 +236,90 @@ public void setProperties(Map properties) { this.properties = properties; } } + + public static class SqlConfig { + private String endpoint; + private String username; + private String password; + private SqlDialect sqlDialect; + private String table; + private Map properties; + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public SqlDialect getSqlDialect() { + return sqlDialect; + } + + public void setSqlDialect(String sqlDialect) { + this.sqlDialect = SqlDialect.valueOf(sqlDialect.toUpperCase()); + } + + public String getDriverClassName() { + switch (sqlDialect) { + case MYSQL: + return "com.mysql.cj.jdbc.Driver"; + case MARIADB: + return "org.mariadb.jdbc.Driver"; + case POSTGRESQL: + return "org.postgresql.Driver"; + case SQLITE: + return "org.sqlite.JDBC"; + case H2: + return "org.h2.Driver"; + case SQLSERVER: + return "com.microsoft.sqlserver.jdbc.SQLServerDriver"; + case SYBASE: + return "com.sybase.jdbc4.jdbc.SybDriver"; + case FIREBIRD: + return "org.firebirdsql.jdbc.FBDriver"; + case INFORMIX: + return "com.informix.jdbc.IfxDriver"; + case ORACLE: + return "oracle.jdbc.OracleDriver"; + case DB2: + return "com.ibm.db2.jcc.DB2Driver"; + default: + throw new IllegalArgumentException("Unsupported SQL Dialect: " + sqlDialect); + } + } + + public String getTable() { + return table; + } + + public void setTable(String table) { + this.table = table; + } + + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + } } diff --git a/cli/flamingock-cli/src/main/java/io/flamingock/cli/factory/SqlDataSourceFactory.java b/cli/flamingock-cli/src/main/java/io/flamingock/cli/factory/SqlDataSourceFactory.java new file mode 100644 index 000000000..3b06f1938 --- /dev/null +++ b/cli/flamingock-cli/src/main/java/io/flamingock/cli/factory/SqlDataSourceFactory.java @@ -0,0 +1,91 @@ +/* + * Copyright 2025 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.cli.factory; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.flamingock.cli.config.DatabaseConfig; +import io.flamingock.internal.common.sql.SqlDialect; +import org.sqlite.SQLiteDataSource; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.sql.Statement; + +public class SqlDataSourceFactory { + + public static DataSource createSqlDataSource(DatabaseConfig.SqlConfig config) { + if (config == null) { + throw new IllegalArgumentException("SQL configuration is required"); + } + + if (config.getEndpoint() == null) { + throw new IllegalArgumentException("Database endpoint is required"); + } + + if (config.getSqlDialect() == null) { + throw new IllegalArgumentException("Sql dialect is required"); + } + + if (!SqlDialect.SQLITE.equals(config.getSqlDialect())) { + if (config.getUsername() == null) { + throw new IllegalArgumentException("Database username is required"); + } + if (config.getPassword() == null) { + throw new IllegalArgumentException("Database password is required"); + } + } + + try { + DataSource sqlDatasource; + + if (config.getSqlDialect().equals(SqlDialect.SQLITE)) { + SQLiteDataSource sqliteDatasource = new SQLiteDataSource(); + sqliteDatasource.setUrl(config.getEndpoint()); + + try (Connection conn = sqliteDatasource.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.execute("PRAGMA journal_mode=WAL;"); + stmt.execute("PRAGMA busy_timeout=5000;"); + } + + sqlDatasource = sqliteDatasource; + } else { + HikariConfig datasourceConfig = new HikariConfig(); + datasourceConfig.setJdbcUrl(config.getEndpoint()); + datasourceConfig.setUsername(config.getUsername()); + datasourceConfig.setPassword(config.getPassword()); + datasourceConfig.setDriverClassName(config.getDriverClassName()); + + sqlDatasource = new HikariDataSource(datasourceConfig); + } + + // Test the connection by listing tables + try (Connection conn = sqlDatasource.getConnection()) { + DatabaseMetaData metaData = conn.getMetaData(); + metaData.getTables(null, null, "%", null); + } catch (SQLException e) { + throw new RuntimeException("Failed to validate SQL DataSource connection: " + e.getMessage(), e); + } + + return sqlDatasource; + } catch (Exception e) { + throw new RuntimeException("Failed to create SQL DataSource: " + e.getMessage(), e); + } + } +} diff --git a/cli/flamingock-cli/src/main/java/io/flamingock/cli/service/AuditService.java b/cli/flamingock-cli/src/main/java/io/flamingock/cli/service/AuditService.java index 7f3045dfa..14829ad07 100644 --- a/cli/flamingock-cli/src/main/java/io/flamingock/cli/service/AuditService.java +++ b/cli/flamingock-cli/src/main/java/io/flamingock/cli/service/AuditService.java @@ -23,9 +23,11 @@ import io.flamingock.cli.factory.CouchbaseClusterFactory; import io.flamingock.cli.factory.DynamoDBClientFactory; import io.flamingock.cli.factory.MongoClientFactory; +import io.flamingock.cli.factory.SqlDataSourceFactory; import io.flamingock.community.couchbase.driver.CouchbaseAuditStore; import io.flamingock.community.dynamodb.driver.DynamoDBAuditStore; import io.flamingock.community.mongodb.sync.driver.MongoDBSyncAuditStore; +import io.flamingock.community.sql.driver.SqlAuditStore; import io.flamingock.internal.common.core.audit.AuditEntry; import io.flamingock.internal.common.core.context.Context; import io.flamingock.internal.common.core.context.Dependency; @@ -40,6 +42,7 @@ import io.flamingock.internal.core.store.AuditStore; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import javax.sql.DataSource; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -142,6 +145,8 @@ private AuditStore createAuditStore(Context context) { return createDynamoAuditStore(context); case COUCHBASE: return createCouchbaseAuditStore(context); + case SQL: + return createSqlAuditStore(context); default: throw new IllegalStateException("Unsupported database type: " + databaseType); } @@ -176,4 +181,14 @@ private AuditStore createCouchbaseAuditStore(Context context) { return new CouchbaseAuditStore(couchbaseCluster, couchbaseConfig.getBucketName()) .withAuditRepositoryName(couchbaseConfig.getTable()); } + + private AuditStore createSqlAuditStore(Context context) { + DatabaseConfig.SqlConfig sqlConfig = config.getAudit().getSql(); + + // Create Couchbase cluster + DataSource dataSource = SqlDataSourceFactory.createSqlDataSource(sqlConfig); + + return new SqlAuditStore(dataSource) + .withAuditRepositoryName(sqlConfig.getTable()); + } } diff --git a/cli/flamingock-cli/src/test/java/io/flamingock/cli/SimpleCLITest.java b/cli/flamingock-cli/src/test/java/io/flamingock/cli/SimpleCLITest.java index df78685a3..f2600793a 100644 --- a/cli/flamingock-cli/src/test/java/io/flamingock/cli/SimpleCLITest.java +++ b/cli/flamingock-cli/src/test/java/io/flamingock/cli/SimpleCLITest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import picocli.CommandLine; +import static io.flamingock.internal.common.sql.SqlDialect.SQLSERVER; import static org.assertj.core.api.Assertions.*; class SimpleCLITest { @@ -53,6 +54,7 @@ void shouldDetectDatabaseTypes() { FlamingockConfig mongoConfig = TestUtils.createMongoConfig(); FlamingockConfig dynamoConfig = TestUtils.createDynamoConfig(); FlamingockConfig couchbaseConfig = TestUtils.createCouchbaseConfig(); + FlamingockConfig sqlConfig = TestUtils.createSqlConfig(); // When/Then assertThat(ConfigLoader.detectDatabaseType(mongoConfig)) @@ -63,6 +65,9 @@ void shouldDetectDatabaseTypes() { assertThat(ConfigLoader.detectDatabaseType(couchbaseConfig)) .isEqualTo(ConfigLoader.DatabaseType.COUCHBASE); + + assertThat(ConfigLoader.detectDatabaseType(sqlConfig)) + .isEqualTo(ConfigLoader.DatabaseType.SQL); } @Test @@ -71,6 +76,7 @@ void shouldCreateTestConfigs() { FlamingockConfig mongoConfig = TestUtils.createMongoConfig(); FlamingockConfig dynamoConfig = TestUtils.createDynamoConfig(); FlamingockConfig couchbaseoConfig = TestUtils.createCouchbaseConfig(); + FlamingockConfig sqlConfig = TestUtils.createSqlConfig(); // Then assertThat(mongoConfig).isNotNull(); @@ -84,5 +90,9 @@ void shouldCreateTestConfigs() { assertThat(couchbaseoConfig).isNotNull(); assertThat(couchbaseoConfig.getAudit().getCouchbase()).isNotNull(); assertThat(couchbaseoConfig.getAudit().getCouchbase().getBucketName()).isEqualTo("test-bucket"); + + assertThat(sqlConfig).isNotNull(); + assertThat(sqlConfig.getAudit().getSql()).isNotNull(); + assertThat(sqlConfig.getAudit().getSql().getSqlDialect()).isEqualTo(SQLSERVER); } } diff --git a/cli/flamingock-cli/src/test/java/io/flamingock/cli/config/SimpleConfigLoaderTest.java b/cli/flamingock-cli/src/test/java/io/flamingock/cli/config/SimpleConfigLoaderTest.java index 0be1bee60..732d71b54 100644 --- a/cli/flamingock-cli/src/test/java/io/flamingock/cli/config/SimpleConfigLoaderTest.java +++ b/cli/flamingock-cli/src/test/java/io/flamingock/cli/config/SimpleConfigLoaderTest.java @@ -23,6 +23,7 @@ import java.nio.file.Files; import java.nio.file.Path; +import static io.flamingock.internal.common.sql.SqlDialect.SQLSERVER; import static org.assertj.core.api.Assertions.*; class SimpleConfigLoaderTest { @@ -83,6 +84,25 @@ void shouldLoadValidCouchbaseConfiguration() throws IOException { assertThat(config.getAudit().getCouchbase().getBucketName()).isEqualTo("test-bucket"); } + @Test + void shouldLoadValidSqlConfiguration() throws IOException { + // Given + Path configFile = tempDir.resolve("sql-config.yml"); + Files.write(configFile, TestUtils.getValidSqlYaml().getBytes()); + + // When + FlamingockConfig config = ConfigLoader.loadConfig(configFile.toString()); + + // Then + assertThat(config.getServiceIdentifier()).isEqualTo("test-cli"); + assertThat(config.getAudit()).isNotNull(); + assertThat(config.getAudit().getSql()).isNotNull(); + assertThat(config.getAudit().getSql().getEndpoint()).isEqualTo("jdbc:sqlserver://localhost:1433"); + assertThat(config.getAudit().getSql().getUsername()).isEqualTo("test-user"); + assertThat(config.getAudit().getSql().getPassword()).isEqualTo("test-password"); + assertThat(config.getAudit().getSql().getSqlDialect()).isEqualTo(SQLSERVER); + } + @Test void shouldDetectMongoDBType() throws IOException { // Given @@ -119,6 +139,18 @@ void shouldDetectCouchbaseType() throws IOException { assertThat(type).isEqualTo(ConfigLoader.DatabaseType.COUCHBASE); } + @Test + void shouldDetectSqlType() throws IOException { + // Given + FlamingockConfig config = TestUtils.createSqlConfig(); + + // When + ConfigLoader.DatabaseType type = ConfigLoader.detectDatabaseType(config); + + // Then + assertThat(type).isEqualTo(ConfigLoader.DatabaseType.SQL); + } + @Test void shouldThrowExceptionForMissingFile() { // When/Then diff --git a/cli/flamingock-cli/src/test/java/io/flamingock/cli/integration/CLISqlIntegrationTest.java b/cli/flamingock-cli/src/test/java/io/flamingock/cli/integration/CLISqlIntegrationTest.java new file mode 100644 index 000000000..dde828441 --- /dev/null +++ b/cli/flamingock-cli/src/test/java/io/flamingock/cli/integration/CLISqlIntegrationTest.java @@ -0,0 +1,207 @@ +/* + * Copyright 2025 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.cli.integration; + +import com.github.stefanbirkner.systemlambda.SystemLambda; +import io.flamingock.cli.FlamingockCli; +import io.flamingock.internal.common.sql.SqlDialect; +import io.flamingock.internal.common.sql.testContainers.SharedSqlContainers; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.junit.jupiter.Testcontainers; +import picocli.CommandLine; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Testcontainers +class CLISqlIntegrationTest { + + private static final Map> containers = new HashMap<>(); + + static Stream dialectProvider() { + String enabledDialects = System.getProperty("sql.test.dialects", "mysql"); + Set enabled = Arrays.stream(enabledDialects.split(",")) + .map(String::trim) + .collect(Collectors.toSet()); + + Stream allDialects = Stream.of( + Arguments.of(SqlDialect.MYSQL, "mysql"), + Arguments.of(SqlDialect.SQLSERVER, "sqlserver"), + Arguments.of(SqlDialect.ORACLE, "oracle"), + Arguments.of(SqlDialect.POSTGRESQL, "postgresql"), + Arguments.of(SqlDialect.MARIADB, "mariadb"), + Arguments.of(SqlDialect.H2, "h2"), + Arguments.of(SqlDialect.SQLITE, "sqlite"), + Arguments.of(SqlDialect.INFORMIX, "informix"), + Arguments.of(SqlDialect.FIREBIRD, "firebird") + ); + + return allDialects.filter(args -> { + String dialectName = (String) args.get()[1]; + return enabled.contains(dialectName); + }); + } + + @BeforeAll + static void startContainers() { + for (Arguments arg : dialectProvider().toArray(Arguments[]::new)) { + SqlDialect dialect = (SqlDialect) arg.get()[0]; + String dialectName = (String) arg.get()[1]; + if (!"h2".equals(dialectName) && !"sqlite".equals(dialectName)) { + JdbcDatabaseContainer container = SharedSqlContainers.getContainer(dialectName); + container.start(); + containers.put(dialectName, container); + } + } + } + + @AfterAll + static void stopContainers() { + containers.values().forEach(JdbcDatabaseContainer::stop); + } + + @ParameterizedTest + @MethodSource("dialectProvider") + void shouldRunAuditListCommandWithSql(SqlDialect sqlDialect, String dialectName) throws Exception { + JdbcDatabaseContainer sqlContainer = SharedSqlContainers.getContainer(dialectName); + // Given - Create temporary config file with TestContainers SQL connection + Path configFile = Files.createTempFile("sql-integration", ".yml"); + String sqlConfig = "flamingock:\n" + + " service-identifier: \"integration-test-cli\"\n" + + " audit:\n" + + " sql:\n" + + " endpoint: \"" + sqlContainer.getJdbcUrl() + "\"\n" + + " username: \"" + sqlContainer.getUsername() + "\"\n" + + " password: \"" + sqlContainer.getPassword() + "\"\n" + + " sql-dialect: \"" + dialectName + "\"\n"; + + Files.write(configFile, sqlConfig.getBytes()); + + try { + // When - Execute CLI audit list command + String output = SystemLambda.tapSystemOut(() -> { + String errorOutput = SystemLambda.tapSystemErr(() -> { + String[] args = {"--config", configFile.toString(), "audit", "list"}; + CommandLine cmd = new CommandLine(new FlamingockCli()); + int exitCode = cmd.execute(args); + + // Then - Should be able to start CLI and attempt database connection + // Exit code might be 1 if no audit entries exist or OpsClient fails to initialize + // The important thing is that TestContainers SQL is running and CLI can connect + assertThat(exitCode).isIn(0, 1); // Accept both success and expected failures + }); + System.out.println("Error output captured: " + errorOutput); + }); + + // Verify TestContainers setup is working and CLI attempted database connection + System.out.println("CLI output: " + output); + System.out.println("SQL container is running: " + sqlContainer.isRunning()); + System.out.println("SQL connection string: " + sqlContainer.getJdbcUrl()); + + // The fact that we got here means TestContainers worked and CLI applied + assertThat(sqlContainer.isRunning()).isTrue(); + + } finally { + // Cleanup + Files.deleteIfExists(configFile); + } + } + + @ParameterizedTest + @MethodSource("dialectProvider") + void shouldRunAuditListCommandVerboseWithSql(SqlDialect sqlDialect, String dialectName) throws Exception { + JdbcDatabaseContainer sqlContainer = SharedSqlContainers.getContainer(dialectName); + // Given + Path configFile = Files.createTempFile("sql-integration-verbose", ".yml"); + String sqlConfig = "flamingock:\n" + + " service-identifier: \"integration-test-verbose-cli\"\n" + + " audit:\n" + + " sql:\n" + + " endpoint: \"" + sqlContainer.getJdbcUrl() + "\"\n" + + " username: \"" + sqlContainer.getUsername() + "\"\n" + + " password: \"" + sqlContainer.getPassword() + "\"\n" + + " sql-dialect: \"" + dialectName + "\"\n"; + + Files.write(configFile, sqlConfig.getBytes()); + + try { + // When - Execute CLI audit list command with verbose flag + String output = SystemLambda.tapSystemOut(() -> { + String[] args = {"--debug", "--config", configFile.toString(), "audit", "list"}; + CommandLine cmd = new CommandLine(new FlamingockCli()); + int exitCode = cmd.execute(args); + + // Then - Accept multiple exit codes: 0=success, 1=runtime error, 2=CLI syntax/config error + assertThat(exitCode).isIn(0, 1, 2); + }); + + System.out.println("Verbose CLI output: " + output); + + } finally { + Files.deleteIfExists(configFile); + } + } + + @ParameterizedTest + @MethodSource("dialectProvider") + void shouldHandleInvalidSqlConnectionGracefully(SqlDialect sqlDialect, String dialectName) throws Exception { + JdbcDatabaseContainer sqlContainer = SharedSqlContainers.getContainer(dialectName); + // Given - Invalid SQL connection + Path configFile = Files.createTempFile("sql-invalid", ".yml"); + String invalidConfig = "flamingock:\n" + + " service-identifier: \"invalid-test-cli\"\n" + + " audit:\n" + + " sql:\n" + + " endpoint: \"jdbc:sqlserver://invalid-host:1433\"\n" + + " username: \"" + sqlContainer.getUsername() + "\"\n" + + " password: \"" + sqlContainer.getPassword() + "\"\n" + + " sql-dialect: \"" + dialectName + "\"\n"; + + Files.write(configFile, invalidConfig.getBytes()); + + try { + // When - Execute CLI with invalid connection + String output = SystemLambda.tapSystemOut(() -> { + String[] args = {"--config", configFile.toString(), "audit", "list"}; + CommandLine cmd = new CommandLine(new FlamingockCli()); + int exitCode = cmd.execute(args); + + // Then - Should handle connection error gracefully (non-zero exit code expected) + assertThat(exitCode).isNotEqualTo(0); + }); + + System.out.println("Error output: " + output); + + } finally { + Files.deleteIfExists(configFile); + } + } +} diff --git a/cli/flamingock-cli/src/test/java/io/flamingock/cli/test/TestUtils.java b/cli/flamingock-cli/src/test/java/io/flamingock/cli/test/TestUtils.java index 471a19838..151ff05d2 100644 --- a/cli/flamingock-cli/src/test/java/io/flamingock/cli/test/TestUtils.java +++ b/cli/flamingock-cli/src/test/java/io/flamingock/cli/test/TestUtils.java @@ -71,6 +71,22 @@ public static FlamingockConfig createCouchbaseConfig() { return config; } + public static FlamingockConfig createSqlConfig() { + FlamingockConfig config = new FlamingockConfig(); + config.setServiceIdentifier("test-service"); + + DatabaseConfig databaseConfig = new DatabaseConfig(); + DatabaseConfig.SqlConfig sqlConfig = new DatabaseConfig.SqlConfig(); + sqlConfig.setEndpoint("jdbc:sqlserver://localhost:1433"); + sqlConfig.setUsername("test-user"); + sqlConfig.setPassword("test-password"); + sqlConfig.setSqlDialect("SqlServer"); + databaseConfig.setSql(sqlConfig); + + config.setAudit(databaseConfig); + return config; + } + public static List createSampleAuditEntries() { AuditEntry entry1 = new AuditEntry( "exec-001", @@ -149,4 +165,15 @@ public static String getValidCouchbaseYaml() { " password: \"test-password\"\n" + " bucket-name: \"test-bucket\"\n"; } + + public static String getValidSqlYaml() { + return "flamingock:\n" + + " service-identifier: \"test-cli\"\n" + + " audit:\n" + + " sql:\n" + + " endpoint: \"jdbc:sqlserver://localhost:1433\"\n" + + " username: \"test-user\"\n" + + " password: \"test-password\"\n" + + " sql-dialect: \"SqlServer\"\n"; + } } diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditTestHelper.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditTestHelper.java index a91661e5c..ab6a8a47a 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditTestHelper.java +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditTestHelper.java @@ -16,6 +16,7 @@ package io.flamingock.community.sql; import io.flamingock.internal.common.sql.SqlDialect; +import io.flamingock.internal.common.sql.testContainers.SharedSqlContainers; import org.testcontainers.containers.JdbcDatabaseContainer; import javax.sql.DataSource; diff --git a/utils/sql-util/build.gradle.kts b/utils/sql-util/build.gradle.kts index e93b7533a..dbbb4a202 100644 --- a/utils/sql-util/build.gradle.kts +++ b/utils/sql-util/build.gradle.kts @@ -5,3 +5,14 @@ java { languageVersion.set(JavaLanguageVersion.of(8)) } } + +dependencies { + implementation("com.zaxxer:HikariCP:3.4.5") + implementation("org.testcontainers:junit-jupiter:1.19.3") + // SQL Testcontainers + implementation("org.testcontainers:mysql:1.19.3") + implementation("org.testcontainers:mssqlserver:1.19.3") + implementation("org.testcontainers:oracle-xe:1.19.3") + implementation("org.testcontainers:postgresql:1.19.3") + implementation("org.testcontainers:mariadb:1.19.3") +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/FirebirdJdbcContainer.java b/utils/sql-util/src/main/java/io/flamingock/internal/common/sql/testContainers/FirebirdJdbcContainer.java similarity index 98% rename from community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/FirebirdJdbcContainer.java rename to utils/sql-util/src/main/java/io/flamingock/internal/common/sql/testContainers/FirebirdJdbcContainer.java index c0f6f5cf5..faf49a862 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/FirebirdJdbcContainer.java +++ b/utils/sql-util/src/main/java/io/flamingock/internal/common/sql/testContainers/FirebirdJdbcContainer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.community.sql; +package io.flamingock.internal.common.sql.testContainers; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.wait.strategy.Wait; diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/InformixContainer.java b/utils/sql-util/src/main/java/io/flamingock/internal/common/sql/testContainers/InformixContainer.java similarity index 98% rename from community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/InformixContainer.java rename to utils/sql-util/src/main/java/io/flamingock/internal/common/sql/testContainers/InformixContainer.java index 846917c52..907b262ce 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/InformixContainer.java +++ b/utils/sql-util/src/main/java/io/flamingock/internal/common/sql/testContainers/InformixContainer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.community.sql; +package io.flamingock.internal.common.sql.testContainers; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.wait.strategy.Wait; diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SharedSqlContainers.java b/utils/sql-util/src/main/java/io/flamingock/internal/common/sql/testContainers/SharedSqlContainers.java similarity index 99% rename from community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SharedSqlContainers.java rename to utils/sql-util/src/main/java/io/flamingock/internal/common/sql/testContainers/SharedSqlContainers.java index a8ee4932f..76922af10 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SharedSqlContainers.java +++ b/utils/sql-util/src/main/java/io/flamingock/internal/common/sql/testContainers/SharedSqlContainers.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.community.sql; +package io.flamingock.internal.common.sql.testContainers; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource;