diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..f632b2c --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,57 @@ +version: 2.1 + +orbs: + qqq-orb: kingsrook/qqq-orb@0.5.0 + +workflows: + + test_only: + jobs: + - qqq-orb/mvn_test_only: + context: [ qqq-maven-registry-credentials ] + filters: + branches: + ignore: /(develop|main|release/.*|hotfix/.*|integration.*)/ + tags: + only: [] + + publish_snapshot: + jobs: + - qqq-orb/mvn_publish: + context: [ qqq-maven-registry-credentials ] + branch_type: snapshot + filters: + branches: + only: + - develop + + publish_release_candidate: + jobs: + - qqq-orb/mvn_publish: + context: [ qqq-maven-registry-credentials ] + branch_type: release_candidate + filters: + branches: + only: + - /release\/.*/ + + publish_release: + jobs: + - qqq-orb/mvn_publish: + context: [ qqq-maven-registry-credentials ] + branch_type: release + filters: + branches: + only: [ main ] + tags: + only: [ /v.*/ ] + + publish_hotfix_release: + jobs: + - qqq-orb/mvn_publish: + context: [ qqq-maven-registry-credentials ] + branch_type: hotfix + filters: + branches: + only: + - /hotfix\/.*/ diff --git a/CHANGELOG.md b/CHANGELOG.md index bbe1dfd..fea915e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Data acquisition tools for dr5hn repository - Gzip decompression for cities data - Population and country filtering options +- Liquibase changelog generator with prefix substitution +- Unit tests for config, producer, generator, and sync components +- CircleCI CI/CD pipeline configuration + +### Changed +- Upgraded to QQQ 0.35.0 with Java 21 support ## [0.1.0-alpha] - 2024-12-28 diff --git a/README.md b/README.md index 9d3b7ee..e6bd4d0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # QBit: Geo Data [![Alpha](https://img.shields.io/badge/status-alpha-orange)]() +[![CircleCI](https://circleci.com/gh/QRun-IO/qbit-geo-data.svg?style=shield)](https://circleci.com/gh/QRun-IO/qbit-geo-data) Geographic reference data QBit providing countries, states/provinces, and cities for QQQ applications. @@ -141,6 +142,24 @@ This creates two independent sets of tables: Query each using its prefixed table name, same entity classes. +## Database Schema + +Generate a Liquibase changelog for your database: + +```java +import com.kingsrook.qbits.geodata.liquibase.GeoDataLiquibaseGenerator; + +GeoDataQBitConfig config = new GeoDataQBitConfig() + .withTableNamePrefix("shipping") + .withEnableCountries(true) + .withEnableStateProvinces(true) + .withEnableCities(true); + +GeoDataLiquibaseGenerator.generate(config, Path.of("db/changelog-geo-data.xml")); +``` + +The generator creates a changelog with your prefix substituted and only includes tables you've enabled. + ## License AGPL-3.0 - See [LICENSE](LICENSE) diff --git a/pom.xml b/pom.xml index 0101838..bbf3b5f 100644 --- a/pom.xml +++ b/pom.xml @@ -18,10 +18,10 @@ - 17 - 17 + 21 + 21 UTF-8 - 0.23.0 + 0.35.0 diff --git a/qbit-geo-data-core/src/main/java/com/kingsrook/qbits/geodata/liquibase/GeoDataLiquibaseGenerator.java b/qbit-geo-data-core/src/main/java/com/kingsrook/qbits/geodata/liquibase/GeoDataLiquibaseGenerator.java new file mode 100644 index 0000000..a7884b2 --- /dev/null +++ b/qbit-geo-data-core/src/main/java/com/kingsrook/qbits/geodata/liquibase/GeoDataLiquibaseGenerator.java @@ -0,0 +1,154 @@ +/******************************************************************************* + ** Liquibase changelog generator for the Geographic Data QBit. + ** + ** Reads the bundled changelog template and generates a final changelog with: + ** - Prefix substitution for table/constraint/index names + ** - Section removal for disabled entities + *******************************************************************************/ +package com.kingsrook.qbits.geodata.liquibase; + + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qbits.geodata.GeoDataQBitConfig; + +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +public class GeoDataLiquibaseGenerator +{ + private static final QLogger LOG = QLogger.getLogger(GeoDataLiquibaseGenerator.class); + + private static final String TEMPLATE_RESOURCE = "/db/changelog-template.xml"; + private static final Pattern SECTION_PATTERN = Pattern.compile( + "\\s*.*?\\s*", + Pattern.DOTALL + ); + + + + /******************************************************************************* + ** Generate a Liquibase changelog from the template using the provided config. + ** + ** @param config the QBit configuration with prefix and enabled tables + ** @param outputPath the path to write the generated changelog + ** @throws IOException if template cannot be read or output cannot be written + *******************************************************************************/ + public static void generate(GeoDataQBitConfig config, Path outputPath) throws IOException + { + String template = loadTemplate(); + String result = processTemplate(template, config); + + Files.createDirectories(outputPath.getParent()); + Files.writeString(outputPath, result, StandardCharsets.UTF_8); + + LOG.info("Generated Liquibase changelog", logPair("outputPath", outputPath)); + } + + + + /******************************************************************************* + ** Generate changelog content as a string without writing to a file. + ** + ** @param config the QBit configuration with prefix and enabled tables + ** @return the generated changelog XML content + ** @throws IOException if template cannot be read + *******************************************************************************/ + public static String generate(GeoDataQBitConfig config) throws IOException + { + String template = loadTemplate(); + return processTemplate(template, config); + } + + + + /******************************************************************************* + ** Load the changelog template from classpath resources. + *******************************************************************************/ + private static String loadTemplate() throws IOException + { + try(InputStream is = GeoDataLiquibaseGenerator.class.getResourceAsStream(TEMPLATE_RESOURCE)) + { + if(is == null) + { + throw new IOException("Changelog template not found: " + TEMPLATE_RESOURCE); + } + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + } + + + + /******************************************************************************* + ** Process the template by removing disabled sections and substituting prefix. + *******************************************************************************/ + private static String processTemplate(String template, GeoDataQBitConfig config) + { + String result = template; + + ///////////////////////////////////////////////////////////////////////// + // Build set of sections to remove based on disabled entities // + ///////////////////////////////////////////////////////////////////////// + Set sectionsToRemove = new HashSet<>(); + if(!Boolean.TRUE.equals(config.getEnableCountries())) + { + sectionsToRemove.add("country"); + } + if(!Boolean.TRUE.equals(config.getEnableStateProvinces())) + { + sectionsToRemove.add("stateProvince"); + } + if(!Boolean.TRUE.equals(config.getEnableCities())) + { + sectionsToRemove.add("city"); + } + + ///////////////////////////////////////////////////////////////////////// + // Remove disabled sections // + ///////////////////////////////////////////////////////////////////////// + if(!sectionsToRemove.isEmpty()) + { + Matcher matcher = SECTION_PATTERN.matcher(result); + StringBuffer sb = new StringBuffer(); + while(matcher.find()) + { + String sectionName = matcher.group(1); + if(sectionsToRemove.contains(sectionName)) + { + matcher.appendReplacement(sb, "\n"); + } + } + matcher.appendTail(sb); + result = sb.toString(); + } + + ///////////////////////////////////////////////////////////////////////// + // Substitute prefix placeholder // + ///////////////////////////////////////////////////////////////////////// + String prefix = config.getTableNamePrefix(); + if(StringUtils.hasContent(prefix)) + { + result = result.replace("${prefix}", prefix); + } + else + { + ////////////////////////////////////////////////////////////////////////// + // No prefix configured - remove the prefix and underscore // + ////////////////////////////////////////////////////////////////////////// + result = result.replace("${prefix}_", ""); + result = result.replace("${prefix}-", ""); + result = result.replace("${prefix}", ""); + } + + return result; + } +} diff --git a/qbit-geo-data-core/src/main/java/com/kingsrook/qbits/geodata/sync/GeoDataSyncStep.java b/qbit-geo-data-core/src/main/java/com/kingsrook/qbits/geodata/sync/GeoDataSyncStep.java index a2f83af..a14eb96 100644 --- a/qbit-geo-data-core/src/main/java/com/kingsrook/qbits/geodata/sync/GeoDataSyncStep.java +++ b/qbit-geo-data-core/src/main/java/com/kingsrook/qbits/geodata/sync/GeoDataSyncStep.java @@ -18,8 +18,8 @@ import java.util.List; import java.util.Map; import java.util.Objects; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; -import com.kingsrook.qqq.backend.core.actions.processes.RunBackendStepInput; -import com.kingsrook.qqq.backend.core.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; diff --git a/qbit-geo-data-core/src/main/resources/db/changelog-template.xml b/qbit-geo-data-core/src/main/resources/db/changelog-template.xml index 73ed079..770f4ec 100644 --- a/qbit-geo-data-core/src/main/resources/db/changelog-template.xml +++ b/qbit-geo-data-core/src/main/resources/db/changelog-template.xml @@ -5,58 +5,114 @@ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.20.xsd"> - - - + + + - - + + + + - + - - + + + + + + - + - - - + + + - + - + + - - + constraintName="${prefix}_fk_state_province_country"/> + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/qbit-geo-data-core/src/test/java/com/kingsrook/qbits/geodata/GeoDataQBitConfigTest.java b/qbit-geo-data-core/src/test/java/com/kingsrook/qbits/geodata/GeoDataQBitConfigTest.java new file mode 100644 index 0000000..d1cfd09 --- /dev/null +++ b/qbit-geo-data-core/src/test/java/com/kingsrook/qbits/geodata/GeoDataQBitConfigTest.java @@ -0,0 +1,182 @@ +/******************************************************************************* + ** Unit tests for GeoDataQBitConfig. + *******************************************************************************/ +package com.kingsrook.qbits.geodata; + + +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + + +class GeoDataQBitConfigTest +{ + + /******************************************************************************* + ** Test that validation fails when backendName is missing. + *******************************************************************************/ + @Test + void testValidate_missingBackendName_addsError() + { + GeoDataQBitConfig config = new GeoDataQBitConfig(); + QInstance qInstance = new QInstance(); + List errors = new ArrayList<>(); + + config.validate(qInstance, errors); + + assertThat(errors).contains("backendName is required for GeoDataQBit"); + } + + + + /******************************************************************************* + ** Test that validation fails when backend is not found in QInstance. + *******************************************************************************/ + @Test + void testValidate_backendNotFound_addsError() + { + GeoDataQBitConfig config = new GeoDataQBitConfig() + .withBackendName("nonexistent"); + QInstance qInstance = new QInstance(); + List errors = new ArrayList<>(); + + config.validate(qInstance, errors); + + assertThat(errors).contains("Backend not found: nonexistent"); + } + + + + /******************************************************************************* + ** Test that validation fails when cities enabled without states. + *******************************************************************************/ + @Test + void testValidate_citiesWithoutStates_addsError() + { + GeoDataQBitConfig config = new GeoDataQBitConfig() + .withBackendName("rdbms") + .withEnableCities(true) + .withEnableStateProvinces(false); + + QInstance qInstance = new QInstance(); + qInstance.addBackend(new QBackendMetaData().withName("rdbms")); + List errors = new ArrayList<>(); + + config.validate(qInstance, errors); + + assertThat(errors).contains("enableStateProvinces must be true when enableCities is true (cities reference states)"); + } + + + + /******************************************************************************* + ** Test that validation fails when states enabled without countries. + *******************************************************************************/ + @Test + void testValidate_statesWithoutCountries_addsError() + { + GeoDataQBitConfig config = new GeoDataQBitConfig() + .withBackendName("rdbms") + .withEnableStateProvinces(true) + .withEnableCountries(false); + + QInstance qInstance = new QInstance(); + qInstance.addBackend(new QBackendMetaData().withName("rdbms")); + List errors = new ArrayList<>(); + + config.validate(qInstance, errors); + + assertThat(errors).contains("enableCountries must be true when enableStateProvinces is true (states reference countries)"); + } + + + + /******************************************************************************* + ** Test that validation passes with valid configuration. + *******************************************************************************/ + @Test + void testValidate_validConfig_noErrors() + { + GeoDataQBitConfig config = new GeoDataQBitConfig() + .withBackendName("rdbms") + .withTableNamePrefix("shipping") + .withEnableCountries(true) + .withEnableStateProvinces(true) + .withEnableCities(true); + + QInstance qInstance = new QInstance(); + qInstance.addBackend(new QBackendMetaData().withName("rdbms")); + List errors = new ArrayList<>(); + + config.validate(qInstance, errors); + + assertThat(errors).isEmpty(); + } + + + + /******************************************************************************* + ** Test getEnabledTableNames returns all tables when all enabled. + *******************************************************************************/ + @Test + void testGetEnabledTableNames_allEnabled() + { + GeoDataQBitConfig config = new GeoDataQBitConfig() + .withEnableCountries(true) + .withEnableStateProvinces(true) + .withEnableCities(true); + + List tables = config.getEnabledTableNames(); + + assertThat(tables).containsExactly("country", "stateProvince", "city"); + } + + + + /******************************************************************************* + ** Test getEnabledTableNames returns only countries when others disabled. + *******************************************************************************/ + @Test + void testGetEnabledTableNames_onlyCountries() + { + GeoDataQBitConfig config = new GeoDataQBitConfig() + .withEnableCountries(true) + .withEnableStateProvinces(false) + .withEnableCities(false); + + List tables = config.getEnabledTableNames(); + + assertThat(tables).containsExactly("country"); + } + + + + /******************************************************************************* + ** Test applyPrefix with prefix configured. + *******************************************************************************/ + @Test + void testApplyPrefix_withPrefix() + { + GeoDataQBitConfig config = new GeoDataQBitConfig() + .withTableNamePrefix("shipping"); + + assertThat(config.applyPrefix("country")).isEqualTo("shipping_country"); + assertThat(config.applyPrefix("stateProvince")).isEqualTo("shipping_stateProvince"); + } + + + + /******************************************************************************* + ** Test applyPrefix without prefix returns original name. + *******************************************************************************/ + @Test + void testApplyPrefix_withoutPrefix() + { + GeoDataQBitConfig config = new GeoDataQBitConfig(); + + assertThat(config.applyPrefix("country")).isEqualTo("country"); + } +} diff --git a/qbit-geo-data-core/src/test/java/com/kingsrook/qbits/geodata/GeoDataQBitProducerTest.java b/qbit-geo-data-core/src/test/java/com/kingsrook/qbits/geodata/GeoDataQBitProducerTest.java new file mode 100644 index 0000000..96134a8 --- /dev/null +++ b/qbit-geo-data-core/src/test/java/com/kingsrook/qbits/geodata/GeoDataQBitProducerTest.java @@ -0,0 +1,138 @@ +/******************************************************************************* + ** Unit tests for GeoDataQBitProducer. + *******************************************************************************/ +package com.kingsrook.qbits.geodata; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + + +class GeoDataQBitProducerTest +{ + + /******************************************************************************* + ** Test that producer throws exception when config is invalid. + *******************************************************************************/ + @Test + void testProduce_invalidConfig_throwsException() + { + GeoDataQBitProducer producer = new GeoDataQBitProducer() + .withConfig(new GeoDataQBitConfig()); // Missing backendName + + QInstance qInstance = new QInstance(); + + assertThatThrownBy(() -> producer.produce(qInstance, "test")) + .isInstanceOf(QException.class) + .hasMessageContaining("backendName is required"); + } + + + + /******************************************************************************* + ** Test that producer throws exception when backend not found. + *******************************************************************************/ + @Test + void testProduce_backendNotFound_throwsException() + { + GeoDataQBitProducer producer = new GeoDataQBitProducer() + .withConfig(new GeoDataQBitConfig() + .withBackendName("nonexistent")); + + QInstance qInstance = new QInstance(); + + assertThatThrownBy(() -> producer.produce(qInstance, "test")) + .isInstanceOf(QException.class) + .hasMessageContaining("Backend not found"); + } + + + + /******************************************************************************* + ** Test that producer creates QBit metadata. + *******************************************************************************/ + @Test + void testProduce_createsQBitMetaData() throws QException + { + QInstance qInstance = new QInstance(); + qInstance.addBackend(new QBackendMetaData().withName("rdbms")); + + GeoDataQBitProducer producer = new GeoDataQBitProducer() + .withConfig(new GeoDataQBitConfig() + .withBackendName("rdbms") + .withTableNamePrefix("geo") + .withEnableCountries(true) + .withEnableStateProvinces(false) + .withEnableCities(false)); + + producer.produce(qInstance, "my-geo-qbit"); + + assertThat(qInstance.getQBits()).isNotEmpty(); + assertThat(qInstance.getQBits().values().stream() + .anyMatch(qb -> "my-geo-qbit".equals(qb.getNamespace()))).isTrue(); + } + + + + /******************************************************************************* + ** Test that producer stores config in QBit metadata. + *******************************************************************************/ + @Test + void testProduce_storesConfigInQBitMetaData() throws QException + { + QInstance qInstance = new QInstance(); + qInstance.addBackend(new QBackendMetaData().withName("rdbms")); + + GeoDataQBitConfig config = new GeoDataQBitConfig() + .withBackendName("rdbms") + .withTableNamePrefix("shipping") + .withEnableCountries(true) + .withEnableStateProvinces(false) + .withEnableCities(false); + + GeoDataQBitProducer producer = new GeoDataQBitProducer() + .withConfig(config); + + producer.produce(qInstance, "shipping-geo"); + + assertThat(qInstance.getQBits().values().stream() + .filter(qb -> "shipping-geo".equals(qb.getNamespace())) + .findFirst() + .map(qb -> qb.getConfig()) + .orElse(null)).isEqualTo(config); + } + + + + /******************************************************************************* + ** Test fluent withConfig method. + *******************************************************************************/ + @Test + void testWithConfig_returnsSelf() + { + GeoDataQBitProducer producer = new GeoDataQBitProducer(); + GeoDataQBitConfig config = new GeoDataQBitConfig(); + + GeoDataQBitProducer result = producer.withConfig(config); + + assertThat(result).isSameAs(producer); + assertThat(producer.getConfig()).isSameAs(config); + } + + + + /******************************************************************************* + ** Test constants are correctly defined. + *******************************************************************************/ + @Test + void testConstants() + { + assertThat(GeoDataQBitProducer.GROUP_ID).isEqualTo("com.kingsrook.qbits"); + assertThat(GeoDataQBitProducer.ARTIFACT_ID).isEqualTo("qbit-geo-data"); + assertThat(GeoDataQBitProducer.VERSION).isEqualTo("0.1.0"); + } +} diff --git a/qbit-geo-data-core/src/test/java/com/kingsrook/qbits/geodata/liquibase/GeoDataLiquibaseGeneratorTest.java b/qbit-geo-data-core/src/test/java/com/kingsrook/qbits/geodata/liquibase/GeoDataLiquibaseGeneratorTest.java new file mode 100644 index 0000000..0d81683 --- /dev/null +++ b/qbit-geo-data-core/src/test/java/com/kingsrook/qbits/geodata/liquibase/GeoDataLiquibaseGeneratorTest.java @@ -0,0 +1,140 @@ +/******************************************************************************* + ** Unit tests for GeoDataLiquibaseGenerator. + *******************************************************************************/ +package com.kingsrook.qbits.geodata.liquibase; + + +import java.io.IOException; +import com.kingsrook.qbits.geodata.GeoDataQBitConfig; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + + +class GeoDataLiquibaseGeneratorTest +{ + + /******************************************************************************* + ** Test that prefix is substituted in generated changelog. + *******************************************************************************/ + @Test + void testGenerate_substitutesPrefix() throws IOException + { + GeoDataQBitConfig config = new GeoDataQBitConfig() + .withTableNamePrefix("shipping") + .withEnableCountries(true) + .withEnableStateProvinces(true) + .withEnableCities(true); + + String result = GeoDataLiquibaseGenerator.generate(config); + + assertThat(result).contains("tableName=\"shipping_country\""); + assertThat(result).contains("tableName=\"shipping_state_province\""); + assertThat(result).contains("tableName=\"shipping_city\""); + assertThat(result).contains("id=\"shipping-create-country-v1\""); + assertThat(result).doesNotContain("${prefix}"); + } + + + + /******************************************************************************* + ** Test that city section is removed when cities disabled. + *******************************************************************************/ + @Test + void testGenerate_removesCitySection() throws IOException + { + GeoDataQBitConfig config = new GeoDataQBitConfig() + .withTableNamePrefix("test") + .withEnableCountries(true) + .withEnableStateProvinces(true) + .withEnableCities(false); + + String result = GeoDataLiquibaseGenerator.generate(config); + + assertThat(result).contains("test_country"); + assertThat(result).contains("test_state_province"); + assertThat(result).doesNotContain("test_city"); + assertThat(result).doesNotContain("SECTION: city"); + } + + + + /******************************************************************************* + ** Test that stateProvince section is removed when states disabled. + *******************************************************************************/ + @Test + void testGenerate_removesStateProvinceSection() throws IOException + { + GeoDataQBitConfig config = new GeoDataQBitConfig() + .withTableNamePrefix("test") + .withEnableCountries(true) + .withEnableStateProvinces(false) + .withEnableCities(false); + + String result = GeoDataLiquibaseGenerator.generate(config); + + assertThat(result).contains("test_country"); + assertThat(result).doesNotContain("test_state_province"); + assertThat(result).doesNotContain("test_city"); + } + + + + /******************************************************************************* + ** Test that all sections removed when all disabled. + *******************************************************************************/ + @Test + void testGenerate_removesAllSections() throws IOException + { + GeoDataQBitConfig config = new GeoDataQBitConfig() + .withTableNamePrefix("test") + .withEnableCountries(false) + .withEnableStateProvinces(false) + .withEnableCities(false); + + String result = GeoDataLiquibaseGenerator.generate(config); + + assertThat(result).doesNotContain("createTable"); + assertThat(result).contains("databaseChangeLog"); + } + + + + /******************************************************************************* + ** Test generation without prefix removes placeholder. + *******************************************************************************/ + @Test + void testGenerate_noPrefix() throws IOException + { + GeoDataQBitConfig config = new GeoDataQBitConfig() + .withEnableCountries(true) + .withEnableStateProvinces(false) + .withEnableCities(false); + + String result = GeoDataLiquibaseGenerator.generate(config); + + assertThat(result).contains("tableName=\"country\""); + assertThat(result).doesNotContain("${prefix}"); + assertThat(result).doesNotContain("_country\""); + } + + + + /******************************************************************************* + ** Test that foreign key constraints use correct prefixed table names. + *******************************************************************************/ + @Test + void testGenerate_foreignKeysUsePrefixedNames() throws IOException + { + GeoDataQBitConfig config = new GeoDataQBitConfig() + .withTableNamePrefix("billing") + .withEnableCountries(true) + .withEnableStateProvinces(true) + .withEnableCities(true); + + String result = GeoDataLiquibaseGenerator.generate(config); + + assertThat(result).contains("referencedTableName=\"billing_country\""); + assertThat(result).contains("referencedTableName=\"billing_state_province\""); + assertThat(result).contains("constraintName=\"billing_fk_state_province_country\""); + } +} diff --git a/qbit-geo-data-core/src/test/java/com/kingsrook/qbits/geodata/sync/GeoDataSyncProcessMetaDataProducerTest.java b/qbit-geo-data-core/src/test/java/com/kingsrook/qbits/geodata/sync/GeoDataSyncProcessMetaDataProducerTest.java new file mode 100644 index 0000000..51c5e79 --- /dev/null +++ b/qbit-geo-data-core/src/test/java/com/kingsrook/qbits/geodata/sync/GeoDataSyncProcessMetaDataProducerTest.java @@ -0,0 +1,75 @@ +/******************************************************************************* + ** Unit tests for GeoDataSyncProcessMetaDataProducer. + *******************************************************************************/ +package com.kingsrook.qbits.geodata.sync; + + +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + + +class GeoDataSyncProcessMetaDataProducerTest +{ + + /******************************************************************************* + ** Test that NAME constant is correctly defined. + *******************************************************************************/ + @Test + void testNameConstant() + { + assertThat(GeoDataSyncProcessMetaDataProducer.NAME).isEqualTo("geoDataSync"); + } + + + + /******************************************************************************* + ** Test that produce returns valid process metadata. + *******************************************************************************/ + @Test + void testProduce_returnsValidProcess() + { + GeoDataSyncProcessMetaDataProducer producer = new GeoDataSyncProcessMetaDataProducer(); + QInstance qInstance = new QInstance(); + + QProcessMetaData process = producer.produce(qInstance); + + assertThat(process).isNotNull(); + assertThat(process.getName()).isEqualTo("geoDataSync"); + assertThat(process.getLabel()).isEqualTo("Sync Geographic Data"); + } + + + + /******************************************************************************* + ** Test that process has sync step. + *******************************************************************************/ + @Test + void testProduce_hasSyncStep() + { + GeoDataSyncProcessMetaDataProducer producer = new GeoDataSyncProcessMetaDataProducer(); + QInstance qInstance = new QInstance(); + + QProcessMetaData process = producer.produce(qInstance); + + assertThat(process.getStepList()).hasSize(1); + assertThat(process.getStepList().get(0).getName()).isEqualTo("sync"); + } + + + + /******************************************************************************* + ** Test that sync step references GeoDataSyncStep code. + *******************************************************************************/ + @Test + void testProduce_syncStepReferencesCorrectCode() + { + GeoDataSyncProcessMetaDataProducer producer = new GeoDataSyncProcessMetaDataProducer(); + QInstance qInstance = new QInstance(); + + QProcessMetaData process = producer.produce(qInstance); + + assertThat(process.getStepList().get(0)).isNotNull(); + } +} diff --git a/qbit-geo-data-core/src/test/java/com/kingsrook/qbits/geodata/sync/GeoDataSyncStepTest.java b/qbit-geo-data-core/src/test/java/com/kingsrook/qbits/geodata/sync/GeoDataSyncStepTest.java new file mode 100644 index 0000000..7b60bf2 --- /dev/null +++ b/qbit-geo-data-core/src/test/java/com/kingsrook/qbits/geodata/sync/GeoDataSyncStepTest.java @@ -0,0 +1,61 @@ +/******************************************************************************* + ** Unit tests for GeoDataSyncStep. + *******************************************************************************/ +package com.kingsrook.qbits.geodata.sync; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + + +class GeoDataSyncStepTest +{ + + /******************************************************************************* + ** Test that constant is correctly defined. + *******************************************************************************/ + @Test + void testFieldConstant() + { + assertThat(GeoDataSyncStep.FIELD_TABLE_NAME_PREFIX).isEqualTo("tableNamePrefix"); + } + + + + /******************************************************************************* + ** Test that run throws exception when prefix is missing. + *******************************************************************************/ + @Test + void testRun_missingPrefix_throwsException() + { + GeoDataSyncStep step = new GeoDataSyncStep(); + RunBackendStepInput input = new RunBackendStepInput(); + RunBackendStepOutput output = new RunBackendStepOutput(); + + assertThatThrownBy(() -> step.run(input, output)) + .isInstanceOf(QException.class) + .hasMessageContaining("tableNamePrefix is required"); + } + + + + /******************************************************************************* + ** Test that run throws exception when prefix is empty. + *******************************************************************************/ + @Test + void testRun_emptyPrefix_throwsException() + { + GeoDataSyncStep step = new GeoDataSyncStep(); + RunBackendStepInput input = new RunBackendStepInput(); + input.addValue(GeoDataSyncStep.FIELD_TABLE_NAME_PREFIX, ""); + RunBackendStepOutput output = new RunBackendStepOutput(); + + assertThatThrownBy(() -> step.run(input, output)) + .isInstanceOf(QException.class) + .hasMessageContaining("tableNamePrefix is required"); + } +}