diff --git a/src/main/java/com/jfeatures/msg/sql/ReadFileFromResources.java b/src/main/java/com/jfeatures/msg/sql/ReadFileFromResources.java index 10ef5de..88b8d12 100644 --- a/src/main/java/com/jfeatures/msg/sql/ReadFileFromResources.java +++ b/src/main/java/com/jfeatures/msg/sql/ReadFileFromResources.java @@ -11,6 +11,8 @@ */ public final class ReadFileFromResources { + private static java.util.function.Supplier classLoaderSupplier = ReadFileFromResources.class::getClassLoader; + private ReadFileFromResources() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } @@ -53,7 +55,7 @@ public static String readFileFromResources(String fileName, Charset charset) { // Normalize path separators for cross-platform compatibility String normalizedFileName = fileName.replace('\\', '/'); - try (var inputStream = ReadFileFromResources.class.getClassLoader().getResourceAsStream(normalizedFileName)) { + try (var inputStream = classLoaderSupplier.get().getResourceAsStream(normalizedFileName)) { // Critical fix: Check for null before calling readAllBytes() if (inputStream == null) { throw new IllegalArgumentException( @@ -101,4 +103,8 @@ private static void validateFileNameSecurity(String fileName) { } } } + + static void setClassLoaderSupplier(java.util.function.Supplier supplier) { + classLoaderSupplier = supplier != null ? supplier : ReadFileFromResources.class::getClassLoader; + } } diff --git a/src/test/java/com/jfeatures/msg/codegen/ParameterMetadataExtractorTest.java b/src/test/java/com/jfeatures/msg/codegen/ParameterMetadataExtractorTest.java index c99fb39..8aa3a08 100644 --- a/src/test/java/com/jfeatures/msg/codegen/ParameterMetadataExtractorTest.java +++ b/src/test/java/com/jfeatures/msg/codegen/ParameterMetadataExtractorTest.java @@ -1,10 +1,12 @@ package com.jfeatures.msg.codegen; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; import static org.mockito.Mockito.lenient; import com.jfeatures.msg.codegen.domain.DBColumn; +import java.lang.reflect.InvocationTargetException; import java.sql.*; import java.util.List; import java.util.stream.Stream; @@ -244,6 +246,79 @@ void testExtractParameters_UnknownSqlType_UsesFallbackType() throws SQLException assertEquals("VARCHAR", result.get(0).jdbcType()); // Default fallback } + @Test + void testExtractParameters_SqlLongerThanLimit_ThrowsException() { + StringBuilder builder = new StringBuilder("SELECT * FROM big_table WHERE "); + while (builder.length() <= 10_050) { + builder.append("column = ? AND "); + } + String longSql = builder.append("1=1").toString(); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> + extractor.extractParameters(longSql) + ); + + assertEquals("SQL query too long", exception.getMessage()); + } + + @Test + void testExtractParameters_ParameterMetadataUnavailable_FallsBackToParsing() throws SQLException { + // Given + String sql = "UPDATE customers SET name = ? WHERE id = ?"; + + setupParameterMetaData(2, new int[]{Types.VARCHAR, Types.INTEGER}); + when(preparedStatement.getParameterMetaData()).thenThrow(new SQLException("metadata not available")); + + // When + List result = extractor.extractParameters(sql); + + // Then - fallback parsing should provide at least one where column entry + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals("id", result.get(0).columnName()); + assertEquals("param2", result.get(1).columnName()); + } + + @Test + void testExtractParameters_WhereClauseTooLong_ThrowsException() throws Exception { + StringBuilder whereBuilder = new StringBuilder(); + while (whereBuilder.length() <= 10_100) { + whereBuilder.append("column").append(whereBuilder.length()).append(" = ? AND "); + } + String longWhereSql = "SELECT * FROM customers WHERE " + whereBuilder.append("1=1").toString(); + + var method = ParameterMetadataExtractor.class.getDeclaredMethod("extractColumnNamesFromWhereClause", String.class, int.class); + method.setAccessible(true); + + InvocationTargetException exception = assertThrows(InvocationTargetException.class, () -> + method.invoke(extractor, longWhereSql, 1) + ); + + assertThat(exception.getCause()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("WHERE clause too long"); + } + + @Test + void testExtractParameters_ExtractWhereClauseSqlTooLong_ThrowsException() throws Exception { + StringBuilder sqlBuilder = new StringBuilder("SELECT * FROM customers WHERE "); + while (sqlBuilder.length() <= 10_050) { + sqlBuilder.append("column = ? OR "); + } + sqlBuilder.append("1=1"); + + var method = ParameterMetadataExtractor.class.getDeclaredMethod("extractWhereClause", String.class); + method.setAccessible(true); + + InvocationTargetException exception = assertThrows(InvocationTargetException.class, () -> + method.invoke(extractor, sqlBuilder.toString()) + ); + + assertThat(exception.getCause()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("SQL query too long"); + } + @Test void testExtractParameters_DatabaseConnectionFails_ThrowsSQLException() throws SQLException { // Given @@ -585,4 +660,4 @@ void testExtractParameters_NegativeParameterCount_ReturnsEmptyList() throws SQLE assertNotNull(result); assertEquals(0, result.size()); } -} \ No newline at end of file +} diff --git a/src/test/java/com/jfeatures/msg/codegen/UtilityClassConstructorCoverageTest.java b/src/test/java/com/jfeatures/msg/codegen/UtilityClassConstructorCoverageTest.java new file mode 100644 index 0000000..10a24e9 --- /dev/null +++ b/src/test/java/com/jfeatures/msg/codegen/UtilityClassConstructorCoverageTest.java @@ -0,0 +1,55 @@ +package com.jfeatures.msg.codegen; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +class UtilityClassConstructorCoverageTest { + + @ParameterizedTest + @MethodSource("utilityClasses") + void utilityConstructorsThrowUnsupportedOperationException(Class utilityClass) throws Exception { + Constructor constructor = utilityClass.getDeclaredConstructor(); + constructor.setAccessible(true); + + InvocationTargetException exception = assertThrows(InvocationTargetException.class, constructor::newInstance); + assertThat(exception.getCause()) + .isInstanceOf(UnsupportedOperationException.class); + } + + private static Stream> utilityClasses() { + return Stream.of( + com.jfeatures.msg.codegen.GenerateController.class, + com.jfeatures.msg.codegen.GenerateDAO.class, + com.jfeatures.msg.codegen.GenerateDTO.class, + com.jfeatures.msg.codegen.GenerateDatabaseConfig.class, + com.jfeatures.msg.codegen.GenerateDeleteController.class, + com.jfeatures.msg.codegen.GenerateDeleteDAO.class, + com.jfeatures.msg.codegen.GenerateDeleteDTO.class, + com.jfeatures.msg.codegen.GenerateInsertController.class, + com.jfeatures.msg.codegen.GenerateInsertDAO.class, + com.jfeatures.msg.codegen.GenerateInsertDTO.class, + com.jfeatures.msg.codegen.GenerateSpringBootApp.class, + com.jfeatures.msg.codegen.GenerateUpdateController.class, + com.jfeatures.msg.codegen.GenerateUpdateDAO.class, + com.jfeatures.msg.codegen.GenerateUpdateDTO.class, + com.jfeatures.msg.codegen.sql.SqlParameterReplacer.class, + com.jfeatures.msg.codegen.util.SqlBuilders.class, + com.jfeatures.msg.codegen.util.DtoFieldNameConverter.class, + com.jfeatures.msg.codegen.util.JavaPackageNameBuilder.class, + com.jfeatures.msg.codegen.util.JavaPoetClassNameBuilder.class, + com.jfeatures.msg.codegen.util.JavaPoetTypeNameBuilder.class, + com.jfeatures.msg.codegen.jdbc.JdbcMethodSelector.class, + com.jfeatures.msg.codegen.mapping.ResultSetMappingGenerator.class, + com.jfeatures.msg.codegen.util.SqlStatementDetector.class, + com.jfeatures.msg.sql.ReadFileFromResources.class, + com.jfeatures.msg.codegen.util.ParameterBuilders.class, + com.jfeatures.msg.codegen.util.FieldBuilders.class + ); + } +} diff --git a/src/test/java/com/jfeatures/msg/codegen/database/DatabaseConnectionFactoryTest.java b/src/test/java/com/jfeatures/msg/codegen/database/DatabaseConnectionFactoryTest.java index 1fcf0cc..ee53b02 100644 --- a/src/test/java/com/jfeatures/msg/codegen/database/DatabaseConnectionFactoryTest.java +++ b/src/test/java/com/jfeatures/msg/codegen/database/DatabaseConnectionFactoryTest.java @@ -1,11 +1,20 @@ package com.jfeatures.msg.codegen.database; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; import com.jfeatures.msg.codegen.domain.DatabaseConnection; +import java.sql.SQLException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.mockito.MockedConstruction; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import com.jfeatures.msg.config.DataSourceConfig; +import com.jfeatures.msg.config.JdbcTemplateConfig; +import com.jfeatures.msg.config.NamedParameterJdbcTemplateConfig; class DatabaseConnectionFactoryTest { @@ -70,6 +79,48 @@ void testCreateDatabaseConnection_ErrorHandling() { } } + @Test + void testCreateDatabaseConnection_RuntimeExceptionWrappedAsDatabaseConnectionException() { + try (MockedConstruction dataSourceConfigConstruction = mockConstruction(DataSourceConfig.class, (mock, context) -> { + javax.sql.DataSource dataSource = mock(javax.sql.DataSource.class); + when(mock.dataSource()).thenReturn(dataSource); + }); + MockedConstruction jdbcTemplateConstruction = mockConstruction(JdbcTemplateConfig.class, (mock, context) -> { + when(mock.jdbcTemplate(any(javax.sql.DataSource.class))).thenThrow(new RuntimeException("boom")); + }); + MockedConstruction namedConstruction = mockConstruction(NamedParameterJdbcTemplateConfig.class)) { + + DatabaseConnectionFactory factory = new DatabaseConnectionFactory(); + DatabaseConnectionException exception = assertThrows(DatabaseConnectionException.class, factory::createDatabaseConnection); + assertThat(exception) + .hasMessage("Failed to create database connection due to configuration error") + .hasCauseInstanceOf(RuntimeException.class); + } + } + + @Test + void testCreateDatabaseConnection_CheckedExceptionWrappedAsDatabaseConnectionException() { + javax.sql.DataSource dataSource = mock(javax.sql.DataSource.class); + JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); + + try (MockedConstruction dataSourceConfigConstruction = mockConstruction(DataSourceConfig.class, (mock, context) -> { + when(mock.dataSource()).thenReturn(dataSource); + }); + MockedConstruction jdbcTemplateConstruction = mockConstruction(JdbcTemplateConfig.class, (mock, context) -> { + when(mock.jdbcTemplate(dataSource)).thenReturn(jdbcTemplate); + }); + MockedConstruction namedConstruction = mockConstruction(NamedParameterJdbcTemplateConfig.class, (mock, context) -> { + when(mock.namedParameterJdbcTemplate(dataSource)).thenThrow(new SQLException("failure")); + })) { + + DatabaseConnectionFactory factory = new DatabaseConnectionFactory(); + DatabaseConnectionException exception = assertThrows(DatabaseConnectionException.class, factory::createDatabaseConnection); + assertThat(exception) + .hasMessage("Failed to create database connection due to unexpected error") + .hasCauseInstanceOf(SQLException.class); + } + } + @Test void testCreateDatabaseConnection_LoggingBehavior() { // Test that the factory logs appropriate messages @@ -163,4 +214,4 @@ void testCreateDatabaseConnection_ComponentIntegration() { assertNotNull(e.getMessage(), "Failure should have meaningful message"); } } -} \ No newline at end of file +} diff --git a/src/test/java/com/jfeatures/msg/codegen/mapping/ResultSetMappingGeneratorTest.java b/src/test/java/com/jfeatures/msg/codegen/mapping/ResultSetMappingGeneratorTest.java index 72566a4..b9b3fca 100644 --- a/src/test/java/com/jfeatures/msg/codegen/mapping/ResultSetMappingGeneratorTest.java +++ b/src/test/java/com/jfeatures/msg/codegen/mapping/ResultSetMappingGeneratorTest.java @@ -6,6 +6,7 @@ import com.jfeatures.msg.codegen.dbmetadata.ColumnMetadata; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.TypeName; +import java.lang.reflect.Method; import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; @@ -331,6 +332,15 @@ void testSimpleClassNames() { assertTrue(result.contains("SimpleDTO")); } + @Test + void capitalizeHandlesNullAndEmptyValues() throws Exception { + Method method = ResultSetMappingGenerator.class.getDeclaredMethod("capitalize", String.class); + method.setAccessible(true); + + assertNull(method.invoke(null, (String) null)); + assertEquals("", method.invoke(null, "")); + } + private ColumnMetadata createColumn(String columnName) { ColumnMetadata column = new ColumnMetadata(); column.setColumnName(columnName); @@ -338,4 +348,4 @@ private ColumnMetadata createColumn(String columnName) { column.setColumnType(Types.VARCHAR); return column; } -} \ No newline at end of file +} diff --git a/src/test/java/com/jfeatures/msg/codegen/util/FieldBuildersTest.java b/src/test/java/com/jfeatures/msg/codegen/util/FieldBuildersTest.java index eb597a4..5958ab8 100644 --- a/src/test/java/com/jfeatures/msg/codegen/util/FieldBuildersTest.java +++ b/src/test/java/com/jfeatures/msg/codegen/util/FieldBuildersTest.java @@ -232,6 +232,16 @@ void shouldCreatePublicDtoField() { assertThat(field.javadoc.toString()).isEmpty(); // Public DTO fields don't have JavaDoc } + @Test + void shouldUseAliasForPublicDtoField() { + ColumnMetadata metadata = createColumnMetadata("customer_id", "bigint", false); + metadata.setColumnAlias("id_alias"); + + FieldSpec field = FieldBuilders.publicDtoField(metadata); + + assertThat(field.name).isEqualTo("id_alias"); + } + @Test void shouldThrowExceptionForNullColumnMetadataInDtoField() { assertThrows(IllegalArgumentException.class, diff --git a/src/test/java/com/jfeatures/msg/codegen/util/ParameterBuildersTest.java b/src/test/java/com/jfeatures/msg/codegen/util/ParameterBuildersTest.java index f8713c5..b97d140 100644 --- a/src/test/java/com/jfeatures/msg/codegen/util/ParameterBuildersTest.java +++ b/src/test/java/com/jfeatures/msg/codegen/util/ParameterBuildersTest.java @@ -2,7 +2,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mockStatic; +import com.jfeatures.msg.codegen.SQLServerDataTypeEnum; import com.jfeatures.msg.codegen.dbmetadata.ColumnMetadata; import com.jfeatures.msg.codegen.domain.DBColumn; import com.squareup.javapoet.ClassName; @@ -14,6 +16,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.MockedStatic; /** * Comprehensive tests for ParameterBuilders utility class to achieve 90%+ coverage. @@ -549,6 +552,21 @@ void shouldHandlePrimitiveTypeBoxingInColumnMetadata() { assertParameter(parameters.get(0), "countValue", ClassName.get(Integer.class)); } + @Test + void shouldBoxPrimitiveTypesReturnedFromTypeLookup() { + ColumnMetadata column = createColumnMetadata("primitive_flag", "bit"); + + try (MockedStatic mocked = mockStatic(SQLServerDataTypeEnum.class)) { + mocked.when(() -> SQLServerDataTypeEnum.getClassForType("bit")).thenReturn(boolean.class); + + List columns = List.of(column); + List parameters = ParameterBuilders.fromColumnMetadata(columns, false); + + assertThat(parameters).hasSize(1); + assertParameter(parameters.get(0), "primitiveFlag", ClassName.get(Boolean.class)); + } + } + private static void assertParameter( ParameterSpec parameter, String expectedName, diff --git a/src/test/java/com/jfeatures/msg/codegen/util/SqlStatementDetectorTest.java b/src/test/java/com/jfeatures/msg/codegen/util/SqlStatementDetectorTest.java index 2cd9257..9e10328 100644 --- a/src/test/java/com/jfeatures/msg/codegen/util/SqlStatementDetectorTest.java +++ b/src/test/java/com/jfeatures/msg/codegen/util/SqlStatementDetectorTest.java @@ -59,4 +59,22 @@ void shouldReturnUnknownForUnsupportedStatement() throws Exception { assertThat(SqlStatementDetector.detectStatementType("DROP TABLE customer")) .isEqualTo(SqlStatementType.UNKNOWN); } -} \ No newline at end of file + + @Test + void shouldFallbackToUpdateWhenParsingFails() throws Exception { + assertThat(SqlStatementDetector.detectStatementType("UPDATE broken set")) + .isEqualTo(SqlStatementType.UPDATE); + } + + @Test + void shouldFallbackToInsertWhenParsingFails() throws Exception { + assertThat(SqlStatementDetector.detectStatementType("INSERT broken values")) + .isEqualTo(SqlStatementType.INSERT); + } + + @Test + void shouldFallbackToDeleteWhenParsingFails() throws Exception { + assertThat(SqlStatementDetector.detectStatementType("DELETE broken")) + .isEqualTo(SqlStatementType.DELETE); + } +}