diff --git a/src/main/resources/META-INF/archetype-post-generate.groovy b/src/main/resources/META-INF/archetype-post-generate.groovy index d838936..8e79346 100644 --- a/src/main/resources/META-INF/archetype-post-generate.groovy +++ b/src/main/resources/META-INF/archetype-post-generate.groovy @@ -15,11 +15,23 @@ def typeDisplay = { String t -> s == 'graphql' ? 'GraphQL' : s } -/** Lower-cases the first character of a string, used to splice a description into a sentence. */ -def lcFirst = { String s -> - (s == null || s.isEmpty()) ? s : Character.toLowerCase(s.charAt(0)).toString() + s.substring(1) +/** Returns the identifier-cased display name used in appName-derived / pom + * values for a type: the conventional acronym casing for known types (REST, GraphQL), or the + * type capitalized otherwise. */ +def typeDisplayName = { String t -> + def s = t.trim().toLowerCase() + if (s == 'rest') return 'REST' + if (s == 'graphql') return 'GraphQL' + s.isEmpty() ? s : Character.toUpperCase(s.charAt(0)) + s.substring(1) } +/** Renders appName for use in a class name: spaces removed, e.g. "Order Service" -> "OrderService". */ +def appNameClass = { String s -> s.replace(' ', '') } + +/** Renders appName for use in a generated property value: spaces removed and lowercased, + * e.g. "Order Service" -> "orderservice". */ +def appNameProperty = { String s -> s.replace(' ', '').toLowerCase() } + def ensureDir = { File base, String rel -> // File(parent, child) normalises separators on all platforms new File(base, rel).mkdirs() @@ -32,11 +44,13 @@ def writeFile = { File base, String rel, String text -> f.withWriter('UTF-8') { w -> w.write(text) } } +// Java rejects two package-info.java files compiled for the same package, which a src/main/java + +// src/test/java pair would be if both carried one. Only src/main/java gets a package-info.java; +// src/test/java packages are still scaffolded (empty) for test classes to land in. def srcTree = { File base, String module, String pkgPath, - List mainSubs, List testSubs, String description, - String testDescription = null -> + List subs, String description -> ensureDir(base, "${module}/src/main/java") - mainSubs.each { sub -> + subs.each { sub -> def fullPkg = (pkgPath + '/' + sub).replace('/', '.') def dir = "${module}/src/main/java/${pkgPath}/${sub}" ensureDir(base, dir) @@ -49,21 +63,7 @@ package ${fullPkg}; } ensureDir(base, "${module}/src/main/resources") ensureDir(base, "${module}/src/test/java") - // Test-package Javadoc reuses the main wording, prefixed to mark it as the tests for that code, - // unless the caller supplies an explicit testDescription (e.g. acceptance-tests, whose test - // package holds the functional tests themselves rather than tests *for* the main package). - def testDesc = testDescription ?: ('Tests for ' + lcFirst(description.replaceAll(/\.\s*$/, '')) + '.') - testSubs.each { sub -> - def fullPkg = (pkgPath + '/' + sub).replace('/', '.') - def dir = "${module}/src/test/java/${pkgPath}/${sub}" - ensureDir(base, dir) - writeFile(base, "${dir}/package-info.java", """\ -/** - * ${testDesc} - */ -package ${fullPkg}; -""") - } + subs.each { sub -> ensureDir(base, "${module}/src/test/java/${pkgPath}/${sub}") } ensureDir(base, "${module}/src/test/resources") } @@ -81,18 +81,23 @@ def failsafeSection = """\ /** Builds a child-module pom.xml string (2-space indent). `prefix` is the project artifactId; * every module's artifactId is rendered as "{prefix}-{aId}" and the inherited parent as * "{prefix}-parent". - * A dependency entry is either a String (an internal module's bare name, rendered as - * "{prefix}-{name}" with this project's groupId and BOM-managed version) or a Map - * [groupId:…, artifactId:…, scope:…, exclusions:[[groupId:…, artifactId:…], …]] for an external - * dependency. Dependencies are grouped into a "" section (scope 'test', emitted first) - * and a "" section (everything else); both headers are always present. */ + * A dependency entry is a String (an internal module's bare name, rendered as "{prefix}-{name}" + * with this project's groupId and BOM-managed version), or a Map: `[module:…, label:…]` is an + * internal module reference like the String form but with a `label`, while + * `[groupId:…, artifactId:…, scope:…, label:…, exclusions:[[groupId:…, artifactId:…], …]]` is an + * external dependency. `label` renders as a "" comment above the first entry of + * each contiguous run sharing that label (e.g. several external deps grouped under "spring"). + * Dependencies are grouped into a "" section (scope 'test', emitted first) and a + * "" section (everything else); both headers are always present. + * `name` is optional and renders a `` element before `` when given. */ def modulePom = { String gId, String prefix, String ver, - String aId, String desc, List deps = [], String buildSection = '' -> + String aId, String desc, List deps = [], String buildSection = '', String name = null -> def depsSection = '' if (deps) { def renderDep = { dep -> - def dGroup = (dep instanceof Map) ? dep.groupId : gId - def dArtifact = (dep instanceof Map) ? dep.artifactId : "${prefix}-${dep}" + def isModuleRef = dep instanceof Map && dep.module + def dGroup = (dep instanceof Map && !isModuleRef) ? dep.groupId : gId + def dArtifact = isModuleRef ? "${prefix}-${dep.module}" : ((dep instanceof Map) ? dep.artifactId : "${prefix}-${dep}") def dScope = (dep instanceof Map && dep.scope) ? "\n ${dep.scope}" : '' def dExclusions = '' if (dep instanceof Map && dep.exclusions) { @@ -111,9 +116,20 @@ def modulePom = { String gId, String prefix, String ver, ${dArtifact}${dScope}${dExclusions} """ } + def renderGroup = { List groupDeps -> + def lines = [] + def prevLabel = null + groupDeps.each { dep -> + def label = (dep instanceof Map) ? dep.label : null + if (label && label != prevLabel) lines << " " + lines << renderDep(dep) + prevLabel = label + } + lines.join('\n') + } def isTest = { it instanceof Map && it.scope == 'test' } - def testLines = deps.findAll(isTest).collect(renderDep).join('\n') - def prodLines = deps.findAll { !isTest(it) }.collect(renderDep).join('\n') + def testLines = renderGroup(deps.findAll(isTest)) + def prodLines = renderGroup(deps.findAll { !isTest(it) }) def section = [' '] if (testLines) section << testLines section << '' @@ -124,6 +140,7 @@ def modulePom = { String gId, String prefix, String ver, // #3: a blank line separates from when both are present. def buildPart = buildSection ? "\n${buildSection}" : '' + def nameElement = name ? "\n ${name}" : '' """\ @@ -140,7 +157,7 @@ def modulePom = { String gId, String prefix, String ver, ../parent/pom.xml - ${prefix}-${aId} + ${prefix}-${aId}${nameElement} ${desc} ${depsSection}${buildPart} """ @@ -156,6 +173,8 @@ def props = request.properties def pkg = props.getProperty('package') ?: groupId def pkgPath = pkg.replace('.', '/') +def appName = (props.getProperty('appName') ?: '').trim() + def integrationsInput = (props.getProperty('integrations') ?: '').trim() def serviceAreasInput = (props.getProperty('serviceAreas') ?: '').trim() def presentationTypesInput = (props.getProperty('presentationTypes') ?: 'rest').trim() @@ -212,8 +231,16 @@ def allModules = ( // app depends on all non-test, non-self modules def appDeps = allModules.findAll { it != 'acceptance-tests' && it != 'common-testing' && it != 'app' } -// acceptance-tests depends on app + all domain modules (including each service area's domain module) +// acceptance-tests depends on app + all domain modules (including each service area's domain +// module); all of them are internal modules, labeled "this app" in the generated pom. def atDeps = (['app', 'common-domain'] + intDomainMods + svcDomainMods + presDomainMods).sort().unique() + .collect { [module: it, label: 'this app'] } + +// Composition flags, needed both early (common-testing, integration modules) and late (app, +// acceptance-tests) in generation. +def hasDatabaseIntegration = integrations.any { it.rawType == 'database' } +def hasRestPresentation = presentations.contains('rest') +def hasGraphqlPresentation = presentations.contains('graphql') // ─── Resolve Project Root ───────────────────────────────────────────────────── @@ -232,11 +259,17 @@ if (subDir.isDirectory()) projectDir = subDir // consumes an external API via spring-boot-starter-restclient (no servlet web stack). All // gated on includeSpring; with Spring off no module references Spring at all. -def STARTER = [groupId: 'org.springframework.boot', artifactId: 'spring-boot-starter'] -def STARTER_WEB = [groupId: 'org.springframework.boot', artifactId: 'spring-boot-starter-web'] -def STARTER_DATA_JPA = [groupId: 'org.springframework.boot', artifactId: 'spring-boot-starter-data-jpa'] -def STARTER_GRAPHQL = [groupId: 'org.springframework.boot', artifactId: 'spring-boot-starter-graphql'] -def STARTER_RESTCLIENT = [groupId: 'org.springframework.boot', artifactId: 'spring-boot-starter-restclient'] +def STARTER = [groupId: 'org.springframework.boot', artifactId: 'spring-boot-starter'] +def STARTER_WEB = [groupId: 'org.springframework.boot', artifactId: 'spring-boot-starter-web'] +def STARTER_WEBMVC = [groupId: 'org.springframework.boot', artifactId: 'spring-boot-starter-webmvc'] +def STARTER_ACTUATOR = [groupId: 'org.springframework.boot', artifactId: 'spring-boot-starter-actuator'] +def STARTER_DATA_JPA = [groupId: 'org.springframework.boot', artifactId: 'spring-boot-starter-data-jpa'] +def STARTER_GRAPHQL = [groupId: 'org.springframework.boot', artifactId: 'spring-boot-starter-graphql'] +def STARTER_GRAPHQL_TEST = [groupId: 'org.springframework.boot', artifactId: 'spring-boot-starter-graphql-test'] +def STARTER_RESTCLIENT = [groupId: 'org.springframework.boot', artifactId: 'spring-boot-starter-restclient'] +def STARTER_VALIDATION = [groupId: 'org.springframework.boot', artifactId: 'spring-boot-starter-validation'] +def SPRING_BOOT_ADMIN_CLIENT = [groupId: 'de.codecentric', artifactId: 'spring-boot-admin-starter-client'] +def DATASOURCE_PROXY_STARTER = [groupId: 'com.github.gavlyukovskiy', artifactId: 'datasource-proxy-spring-boot-starter'] /** Role-specific starter for an integration (client-side) module, or null when none fits * (those modules rely on the core starter inherited transitively via common-domain). */ @@ -363,9 +396,32 @@ def springCoreDm = !includeSpring ? '' : '\n' + [ ' ', ].join('\n') +// datasource-proxy-spring-boot-starter and spring-boot-admin-starter-client aren't part of the +// Spring Boot BOM (third-party groupIds), so — like spring-core above — they need their own +// dependencyManagement entry with an explicit version, pinned via a property. +def datasourceProxyDm = !includeSpring ? '' : '\n' + [ + ' ', + ' com.github.gavlyukovskiy', + ' datasource-proxy-spring-boot-starter', + ' ${datasource-proxy-spring-boot-starter.version}', + ' ', +].join('\n') + +def springBootAdminDm = !includeSpring ? '' : '\n' + [ + ' ', + ' de.codecentric', + ' spring-boot-admin-starter-client', + ' ${spring-boot-admin-starter-client.version}', + ' ', +].join('\n') + +def datasourceProxyVersionProp = includeSpring ? ' 2.0.1\n' : '' +def springBootAdminVersionProp = includeSpring ? ' 4.1.1\n' : '' + def parentDependencies = !includeSpring ? '' : '\n' + [ ' ', ' ', + ' ', ' ', ' org.springframework.boot', ' spring-boot-starter-test', @@ -413,7 +469,9 @@ ${parentInherit} ${groupId} ${version} pom - ${artifactId}-parent + ${appName} Parent + + ${appName}'s parent POM. ${modulesXml} @@ -426,12 +484,12 @@ ${modulesXml} UTF-8 - 3.13.0 +${datasourceProxyVersionProp} 3.13.0 3.5.5 3.4.2 3.5.5 3.4.0 - 2.18.0 +${springBootAdminVersionProp} 2.18.0 @@ -440,7 +498,7 @@ ${modulesXml} ${dmTestXml} -${dmProdXml}${springCoreDm} +${dmProdXml}${springCoreDm}${datasourceProxyDm}${springBootAdminDm} ${parentDependencies} @@ -606,7 +664,7 @@ writeFile(projectDir, 'common-domain/pom.xml', 'Domain classes common to all modules.', commonSpring)) srcTree(projectDir, 'common-domain', pkgPath, - ['common/domain'], ['common/domain'], + ['common/domain'], 'Domain classes common to all modules.') configClass('common-domain', 'common/domain') @@ -615,20 +673,84 @@ configClass('common-domain', 'common/domain') // common-testing carries spring-boot-starter-test at compile scope (it is test-support code that // other modules compile their tests against), overriding the test-scoped copy every module inherits // from the parent (#5). No commons-logging exclusion is needed here — the parent already excludes it. -def commonTestingDeps = ['common-domain'] +// It also depends on every domain module. No domain module depends back on common-testing (only +// service/domain-service's *business-logic* sibling, service/service-{name}, does), so this stays +// acyclic. +def allDomainMods = (intDomainMods + svcDomainMods + presDomainMods).sort() +def commonTestingDeps = ['common-domain'] + allDomainMods if (includeSpring) { commonTestingDeps << [groupId: 'org.springframework.boot', artifactId: 'spring-boot-starter-test', scope: 'compile'] + // Needed at compile scope for GraphQlAcceptanceTestBase (below) to reference + // @AutoConfigureHttpGraphQlTester / HttpGraphQlTester. + if (hasGraphqlPresentation) commonTestingDeps << (STARTER_GRAPHQL_TEST + [scope: 'compile']) } writeFile(projectDir, 'common-testing/pom.xml', modulePom(groupId, artifactId, version, 'common-testing', - 'Testing infrastructure common to all test modules.', - commonTestingDeps)) + "${appName}'s common testing.", + commonTestingDeps, + '', + "${appName} Common Testing")) srcTree(projectDir, 'common-testing', pkgPath, - ['common/testing'], ['common/testing'], + ['common/testing'], 'Test infrastructure common to all modules.') +// Shared acceptance-test base classes: exercise the application on a real running server (random +// port), one per presentation type actually generated. AppRestAcceptanceTestBase / +// AppGraphQlAcceptanceTestBase (generated in acceptance-tests) extend these directly. +if (includeSpring && hasRestPresentation) { +writeFile(projectDir, "common-testing/src/main/java/${pkgPath}/common/testing/RestAcceptanceTestBase.java", """\ +package ${pkg}.common.testing; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.web.servlet.client.RestTestClient; + +/** + * Base test class for REST acceptance tests, exercising the application on a real running server. + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public abstract class RestAcceptanceTestBase { + + @LocalServerPort + private int port; + + /** + * Returns a REST test client bound to this test's running server instance. + * + * @return a REST test client for issuing requests against the running application. + */ + protected RestTestClient restTestClient() { + return RestTestClient.bindToServer() + .baseUrl("http://localhost:" + port) + .build(); + } +} +""") +} +if (includeSpring && hasGraphqlPresentation) { +writeFile(projectDir, "common-testing/src/main/java/${pkgPath}/common/testing/GraphQlAcceptanceTestBase.java", """\ +package ${pkg}.common.testing; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.graphql.test.autoconfigure.tester.AutoConfigureHttpGraphQlTester; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.graphql.test.tester.HttpGraphQlTester; + +/** + * Base test class for GraphQL acceptance tests, exercising the application on a real running server. + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureHttpGraphQlTester +public abstract class GraphQlAcceptanceTestBase { + + @Autowired + protected HttpGraphQlTester graphQlTester; +} +""") +} + // ─── Integration: domain-{type}-{name} and integration-{type}-{name} ────────── integrations.each { intg -> @@ -644,18 +766,20 @@ integrations.each { intg -> "Domain classes for the ${intg.name} ${disp} data source.", ['common-domain'])) srcTree(projectDir, domMod, pkgPath, - [domSub], [domSub], + [domSub], "Domain classes for the ${intg.name} ${disp} integration.") configClass(domMod, domSub) + // SQL logging via datasource-proxy applies only to database integrations. + def sqlLoggingDep = (includeSpring && intg.rawType == 'database') ? [DATASOURCE_PROXY_STARTER + [label: 'sql logging']] : [] writeFile(projectDir, "${implMod}/pom.xml", modulePom(groupId, artifactId, version, implMod, "Integration classes (DAOs, repositories) for the ${intg.name} ${disp} data source.", - ['common-domain', domMod] + starterDeps(integrationStarter(intg.rawType)), + ['common-domain', domMod] + starterDeps(integrationStarter(intg.rawType)) + sqlLoggingDep, failsafeSection)) srcTree(projectDir, implMod, pkgPath, - [implSub], [implSub], + [implSub], "Integration classes for the ${intg.name} ${disp} data source.") configClass(implMod, implSub) } @@ -669,18 +793,26 @@ serviceModules.each { svcMod -> def domMod = "domain-${svcMod}" def domPkg = "domain/${svcPkg}" - def svcDesc = svcMod == 'service' ? 'Business logic for the application.' + def svcPackageDesc = svcMod == 'service' ? 'Business logic for the application.' : "Business logic for the ${label} service area." // #4: domain-service modules describe the service tier (plus the area name when named). - def domDesc = svcMod == 'service' ? 'Domain classes for the service tier.' + def domPackageDesc = svcMod == 'service' ? 'Domain classes for the service tier.' : "Domain classes for the ${label} service tier." - // Domain classes owned by this service area. + // Named service areas fold the area name into the appName-derived pom name/description: mid- + // sentence for prose ("...'s orders service tier..."), trailing for the identifier + // ("...Service Orders"), matching the service-{name}/domain-service-{name} directory convention. + def areaInfix = svcMod == 'service' ? '' : " ${label}" + def areaSuffix = svcMod == 'service' ? '' : " ${label.capitalize()}" + + // Domain classes owned by this service area. No domain module depends on common-testing. + def domDeps = ['common-domain'] + if (includeSpring) domDeps << (STARTER_VALIDATION + [label: 'spring']) writeFile(projectDir, "${domMod}/pom.xml", modulePom(groupId, artifactId, version, - domMod, domDesc, - ['common-domain'])) - srcTree(projectDir, domMod, pkgPath, [domPkg], [domPkg], domDesc) + domMod, "${appName}'s${areaInfix} service tier domain classes.", + domDeps, '', "${appName} Domain Service${areaSuffix}")) + srcTree(projectDir, domMod, pkgPath, [domPkg], domPackageDesc) configClass(domMod, domPkg) // Business logic, depending on its own service-area domain module and (#2) the integration @@ -689,28 +821,35 @@ serviceModules.each { svcMod -> def svcIntegrations = svcMod == 'service' ? intImplMods : integrations.findAll { it.name == label }.collect { "integration-${it.abbrev}-${it.name}" } + def svcDeps = ['common-domain', domMod] + svcIntegrations + if (includeSpring) svcDeps << [module: 'common-testing', scope: 'test'] writeFile(projectDir, "${svcMod}/pom.xml", modulePom(groupId, artifactId, version, - svcMod, svcDesc, - ['common-domain', domMod] + svcIntegrations)) - srcTree(projectDir, svcMod, pkgPath, [svcPkg], [svcPkg], svcDesc) + svcMod, "${appName}'s${areaInfix} service tier (business logic).", + svcDeps, '', "${appName} Service${areaSuffix}")) + srcTree(projectDir, svcMod, pkgPath, [svcPkg], svcPackageDesc) configClass(svcMod, svcPkg) } // ─── Presentation: domain-{type} and presentation-{type} ────────────────────── presentations.each { pType -> - def domMod = "domain-${pType}" - def implMod = "presentation-${pType}" - def disp = typeDisplay(pType) - + def domMod = "domain-${pType}" + def implMod = "presentation-${pType}" + def disp = typeDisplay(pType) + def dispName = typeDisplayName(pType) + + // Presentation-tier domain classes (DTOs, etc.) map to/from the service-tier domain objects, + // hence the dependency on every domain-service* module ("the service domain deps"). + def domDeps = ['common-domain'] + svcDomainMods + if (includeSpring) domDeps << (STARTER_VALIDATION + [label: 'spring']) writeFile(projectDir, "${domMod}/pom.xml", modulePom(groupId, artifactId, version, domMod, - "Domain classes for the ${disp} presentation tier.", - ['common-domain'])) + "${appName}'s ${dispName} presentation tier domain classes.", + domDeps, '', "${appName} Domain ${dispName}")) srcTree(projectDir, domMod, pkgPath, - ["domain/${pType}"], ["domain/${pType}"], + ["domain/${pType}"], "Domain classes for the ${disp} presentation tier.") configClass(domMod, "domain/${pType}") @@ -721,57 +860,67 @@ presentations.each { pType -> def implPkgDesc = pType == 'graphql' ? "Resolver classes for the ${disp} presentation tier." : "Request-handling classes for the ${disp} presentation tier." + // presentation-graphql carries an appName-derived pom name/description; other presentation + // types (no dedicated doc requirement yet) keep the generic role-based description. + def implPomName = pType == 'graphql' ? "${appName} Presentation GraphQL" : null + def implPomDesc = pType == 'graphql' ? "${appName}'s GraphQL presentation tier." : implDesc // #1: every presentation module depends on all service modules. writeFile(projectDir, "${implMod}/pom.xml", modulePom(groupId, artifactId, version, - implMod, implDesc, - ['common-domain', domMod] + serviceModules + starterDeps(presentationStarter(pType)))) + implMod, implPomDesc, + ['common-domain', domMod] + serviceModules + starterDeps(presentationStarter(pType)), + '', implPomName)) srcTree(projectDir, implMod, pkgPath, - ["presentation/${pType}"], ["presentation/${pType}"], + ["presentation/${pType}"], implPkgDesc) configClass(implMod, "presentation/${pType}") } // ─── app ───────────────────────────────────────────────────────────────────── -// The app declares the web / data-jpa starters only when its composition warrants them, and -// inherits whatever else its composed modules bring. A 'rest' presentation makes it a servlet -// web app (so it also gets the WAR-deployment AppServletInitializer); a 'database' integration +// Every Spring-enabled app is always a monitorable web service — core starter, servlet web stack, +// actuator, and Spring Boot Admin registration — regardless of presentationTypes, since actuator +// needs a web server to expose its endpoints over HTTP. A 'database' integration additionally // gives it JPA + transactions (so Application carries @EnableTransactionManagement, which needs // spring-tx from spring-boot-starter-data-jpa). All gated on includeSpring. -def hasRestPresentation = presentations.contains('rest') -def hasDatabaseIntegration = integrations.any { it.rawType == 'database' } - def appStarters = [] if (includeSpring) { - if (hasRestPresentation) appStarters << STARTER_WEB + appStarters << (STARTER + [label: 'spring']) + appStarters << (STARTER_WEBMVC + [label: 'spring']) + appStarters << (STARTER_ACTUATOR + [label: 'spring']) + appStarters << (SPRING_BOOT_ADMIN_CLIENT + [label: 'spring']) if (hasDatabaseIntegration) appStarters << STARTER_DATA_JPA } writeFile(projectDir, 'app/pom.xml', modulePom(groupId, artifactId, version, 'app', - 'Application assembly — produces the runnable artifact.', - appDeps + appStarters)) -srcTree(projectDir, 'app', pkgPath, ['app'], ['app'], + "${appName}'s application configuration including building its deployment assembly.", + appDeps + appStarters, '', "${appName} App")) +srcTree(projectDir, 'app', pkgPath, ['app'], 'Application assembly and entry point.') if (includeSpring) { // Spring Boot entry-point classes live in the app's .config package and @Import every -// module's @Configuration class so the whole tree is assembled into one context. +// module's @Configuration class so the whole tree is assembled into one context. Each config +// class gets its own import statement so the @Import list itself can use short simple names +// (module names are unique, so the simple class names never collide). def appConfigPkg = (pkgPath + '/app/config').replace('/', '.') -def importsBlock = configFqns.sort().collect { " ${it}.class," }.join('\n') +def baseImports = [ + 'org.springframework.boot.SpringApplication', + 'org.springframework.boot.autoconfigure.SpringBootApplication', + 'org.springframework.context.annotation.Import', +] // @EnableTransactionManagement requires spring-tx (pulled in by spring-boot-starter-data-jpa), // so it is only emitted when a database integration puts that starter on the app's classpath. -def txImport = hasDatabaseIntegration ? 'import org.springframework.transaction.annotation.EnableTransactionManagement;\n' : '' +if (hasDatabaseIntegration) baseImports << 'org.springframework.transaction.annotation.EnableTransactionManagement' +def importStatements = (baseImports + configFqns).sort().collect { "import ${it};" }.join('\n') +def importsBlock = configFqns.sort().collect { " ${it.substring(it.lastIndexOf('.') + 1)}.class," }.join('\n') def txAnnotation = hasDatabaseIntegration ? '@EnableTransactionManagement\n' : '' writeFile(projectDir, "app/src/main/java/${pkgPath}/app/config/Application.java", """\ package ${appConfigPkg}; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Import; -${txImport} +${importStatements} + /** * Spring Boot main application class for running standalone in Boot configured embedded container. Not used with WAR * deployment. @@ -786,9 +935,8 @@ public class Application { } } """) -// AppServletInitializer extends SpringBootServletInitializer (a servlet web stack), so it is -// only generated when a 'rest' presentation makes the app a servlet web application. -if (hasRestPresentation) { +// The app is always a servlet web application (spring-boot-starter-webmvc, above), so +// AppServletInitializer (WAR deployment) is always generated alongside it. writeFile(projectDir, "app/src/main/java/${pkgPath}/app/config/AppServletInitializer.java", """\ package ${appConfigPkg}; @@ -810,19 +958,182 @@ public class AppServletInitializer extends SpringBootServletInitializer { } } """) + +// JPA and SQL-logging properties only make sense when a database integration puts +// spring-boot-starter-data-jpa / datasource-proxy-spring-boot-starter on app's classpath. +def jpaOpenInViewLine = hasDatabaseIntegration ? 'spring.jpa.open-in-view=false\n' : '' +def metricsBlock = hasDatabaseIntegration ? """\ +### Metrics +jdbc.datasource-proxy.enabled=true +spring.jpa.properties.hibernate.generate_statistics=true + +""" : '' +def jpaBlock = hasDatabaseIntegration ? """\ +## JPA +spring.jpa.show-sql=false +spring.jpa.hibernate.ddl-auto=none + +""" : '' +def sqlLoggingBlock = hasDatabaseIntegration ? """\ + +### SQL logging +decorator.datasource.enabled=true +decorator.datasource.datasource-proxy.format-sql=true +logging.level.net.ttddyy.dsproxy.listener=TRACE +""" : '' + +writeFile(projectDir, 'app/src/main/resources/application.properties', """\ +## App +spring.application.name=${appNameProperty(appName)} +${jpaOpenInViewLine}spring.main.banner-mode=off + +## Actuators +### enable all endpoints +management.endpoints.web.exposure.include=* + +### show health check details +management.endpoint.health.show-details=always +management.endpoint.health.show-components=always + +#### display env values (only do this in local or dev environments to avoid display secrets) +management.endpoint.env.show-values=always + +### enable extra auto-configured InfoContributors +management.info.env.enabled=true +management.info.java.enabled=true +management.info.os.enabled=true +management.info.process.enabled=true + +### display all git info +management.info.git.mode=full + +### tracing +management.tracing.sampling.probability=1.0 + +#### disable open telemetry metric export +management.otlp.metrics.export.enabled=false + +${metricsBlock}## Spring Boot Admin +spring.boot.admin.client.enabled=true +spring.boot.admin.client.url=TODO SET ME + +${jpaBlock}## Logging +logging.include-application-name=false +logging.level.root=INFO +logging.file.name=TODO SET ME.log +logging.pattern.file=%date %-10thread %-5level %-100.100logger %mdc: %marker: %msg%n +logging.logback.rollingpolicy.max-file-size=10gb +logging.logback.rollingpolicy.max-history=8 +logging.structured.json.context.prefix=labels +logging.level.org.springframework.transaction.interceptor=TRACE +${sqlLoggingBlock}""") + +['ci', 'dev', 'local', 'prod', 'qa', 'uat'].each { profile -> + writeFile(projectDir, "app/src/main/resources/application-${profile}.properties", '') } } // ─── acceptance-tests ───────────────────────────────────────────────────────── +// TEST: common-testing (+ spring-graphql-test when a graphql presentation needs GraphQL test +// support). PROD: every internal module it depends on ("this app"), + sql logging support. +def atModuleDeps = [[module: 'common-testing', scope: 'test', label: 'this app']] +if (includeSpring && hasGraphqlPresentation) { + atModuleDeps << [groupId: 'org.springframework.graphql', artifactId: 'spring-graphql-test', + scope: 'test', label: 'spring'] +} +atModuleDeps += atDeps +if (includeSpring && hasDatabaseIntegration) atModuleDeps << (DATASOURCE_PROXY_STARTER + [label: 'sql logging']) + writeFile(projectDir, 'acceptance-tests/pom.xml', modulePom(groupId, artifactId, version, 'acceptance-tests', - 'Functional acceptance tests for the application.', - atDeps, - failsafeSection)) -// The test package holds the functional tests themselves (no "Tests for " prefix); the rarely-used -// main package holds supporting classes / infrastructure for those tests. -srcTree(projectDir, 'acceptance-tests', pkgPath, ['at'], ['at'], - 'Supporting classes and infrastructure for the functional acceptance tests.', + "${appName}'s acceptance tests.", + atModuleDeps, + failsafeSection, "${appName} Acceptance Tests")) +// The actual *AT test classes live under src/test/java (no package-info.java there, see srcTree); +// this main-side package-info is acceptance-tests' only one, so it describes the functional tests +// themselves rather than "supporting infrastructure for" them. +srcTree(projectDir, 'acceptance-tests', pkgPath, ['at'], 'Functional acceptance tests for the application.') + +if (includeSpring) { +def atPkg = (pkgPath + '/at').replace('/', '.') +def atConfigPkg = (pkgPath + '/at/config').replace('/', '.') +def atConfigClassName = "${appNameClass(appName)}AcceptanceTestConfiguration" +def atConfigFqn = "${atConfigPkg}.${atConfigClassName}".toString() + +writeFile(projectDir, "acceptance-tests/src/test/java/${pkgPath}/at/config/${atConfigClassName}.java", """\ +package ${atConfigPkg}; + +import org.springframework.context.annotation.Configuration; + +/** + * Configuration class for ${appName} ATs. + */ +@Configuration +public class ${atConfigClassName} { +} +""") + +writeFile(projectDir, 'acceptance-tests/src/test/resources/logback-spring.xml', """\ + + + + + + + +""") + +// Both AT base classes @ContextConfiguration against the acceptance-test config plus every +// generated module's @Configuration class: acceptance-tests depends on app, which transitively +// depends on every other module, so the whole composition is already on its test classpath. +def contextClasses = [atConfigFqn] + configFqns.sort() +def simpleName = { String fqn -> fqn.substring(fqn.lastIndexOf('.') + 1) } +def contextClassesBlock = contextClasses.collect { simpleName(it) + '.class' }.join(', ') +// DataSourceAutoConfiguration / XADataSourceAutoConfiguration only need excluding — and are only +// guaranteed to be on the classpath — when a database integration pulls in spring-boot-jdbc. +def dbExcludeFqns = hasDatabaseIntegration ? [ + 'org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration', + 'org.springframework.boot.jdbc.autoconfigure.XADataSourceAutoConfiguration', +] : [] +def enableAutoConfigLine = dbExcludeFqns + ? "@EnableAutoConfiguration(exclude = { ${dbExcludeFqns.collect { simpleName(it) + '.class' }.join(', ')} })\n" + : '' +def dbExcludeImports = dbExcludeFqns ? (['org.springframework.boot.autoconfigure.EnableAutoConfiguration'] + dbExcludeFqns) : [] + +if (hasRestPresentation) { +def imports = (['org.springframework.test.context.ContextConfiguration', "${pkg}.common.testing.RestAcceptanceTestBase".toString()] + + dbExcludeImports + contextClasses).sort().collect { "import ${it};" }.join('\n') +writeFile(projectDir, "acceptance-tests/src/test/java/${pkgPath}/at/AppRestAcceptanceTestBase.java", """\ +package ${atPkg}; + +${imports} + +/** + * Base test class for all ${appName} REST acceptance tests. + */ +@ContextConfiguration(classes = { ${contextClassesBlock} }) +${enableAutoConfigLine}public abstract class AppRestAcceptanceTestBase extends RestAcceptanceTestBase { +} +""") +} + +if (hasGraphqlPresentation) { +def imports = (['org.springframework.test.context.ContextConfiguration', "${pkg}.common.testing.GraphQlAcceptanceTestBase".toString()] + + dbExcludeImports + contextClasses).sort().collect { "import ${it};" }.join('\n') +writeFile(projectDir, "acceptance-tests/src/test/java/${pkgPath}/at/AppGraphQlAcceptanceTestBase.java", """\ +package ${atPkg}; + +${imports} + +/** + * Base test class for all ${appName} GraphQL acceptance tests. + */ +@ContextConfiguration(classes = { ${contextClassesBlock} }) +${enableAutoConfigLine}public abstract class AppGraphQlAcceptanceTestBase extends GraphQlAcceptanceTestBase { +} +""") +} +} diff --git a/src/main/resources/META-INF/maven/archetype-metadata.xml b/src/main/resources/META-INF/maven/archetype-metadata.xml index 30ab065..228c6c8 100644 --- a/src/main/resources/META-INF/maven/archetype-metadata.xml +++ b/src/main/resources/META-INF/maven/archetype-metadata.xml @@ -12,6 +12,12 @@ PROMPT GUIDE ============ + appName + Brief, human-readable application name: capitalized words separated by spaces, + e.g. "Order Service". Drives generated class names (spaces removed, e.g. + OrderService), generated property values (spaces removed and lowercased, e.g. + orderservice), and prose/Javadoc (used unmodified). + integrations Comma-separated list of type:name pairs. Supported types: database, rest, graphql, file, email, jms, kafka, ... @@ -36,6 +42,11 @@ 1.0.0-SNAPSHOT + + + + diff --git a/src/site/asciidoc/building.adoc b/src/site/asciidoc/building.adoc index 4ec3298..2577578 100644 --- a/src/site/asciidoc/building.adoc +++ b/src/site/asciidoc/building.adoc @@ -44,7 +44,8 @@ Each test is a directory containing three files: `archetype.properties`:: Property values passed to the archetype at generation time — the Maven coordinates plus the -three archetype-specific properties (`integrations`, `serviceAreas`, `presentationTypes`). +five archetype-specific properties (`appName`, `integrations`, `serviceAreas`, `presentationTypes`, +`includeSpring`). `goal.txt`:: The Maven goal to run against the generated project after generation. diff --git a/src/site/asciidoc/index.adoc b/src/site/asciidoc/index.adoc index 6325cfc..2f5f6f0 100644 --- a/src/site/asciidoc/index.adoc +++ b/src/site/asciidoc/index.adoc @@ -22,8 +22,9 @@ mvn archetype:generate \ Replace `VERSION` with the archetype release you want to use. -Maven prompts for standard coordinates (`groupId`, `artifactId`, `version`, `package`) -followed by three archetype-specific properties. +Maven prompts for `appName` and `integrations` first (the two required properties with no +default), then the standard coordinates (`groupId`, `artifactId`, `package`), then confirms. +`serviceAreas`, `presentationTypes`, and `includeSpring` have defaults and are not prompted interactively. See link:usage.html[Usage] for the full prompts reference. == Example Output @@ -50,6 +51,8 @@ my-service/ ---- `parent/` is a sibling directory — there is no `pom.xml` at the project root. -Each module contains a full `src/` scaffold and a `package-info.java` in every leaf package. +Each module contains a full `src/` scaffold and a `package-info.java` in every `src/main/java` +leaf package (never under `src/test/java`, since Java rejects two package-info.java files +compiled for the same package). See link:modules.html[Modules] for a description of each module, its dependencies, and how to build. diff --git a/src/site/asciidoc/modules.adoc b/src/site/asciidoc/modules.adoc index fde8fcf..7e7532c 100644 --- a/src/site/asciidoc/modules.adoc +++ b/src/site/asciidoc/modules.adoc @@ -5,12 +5,17 @@ Every module contains a full `src/` scaffold (`src/main/java`, `src/main/resources`, `src/test/java`, `src/test/resources`) -with a `package-info.java` in each module's primary leaf package under both -`src/main/java` and `src/test/java`. +with a `package-info.java` in each module's primary leaf package under +`src/main/java` only — never `src/test/java`, since Java rejects two +`package-info.java` files compiled for the same package. (`acceptance-tests` is the +exception worth calling out: its real test content — the `*AT` classes — lives under +`src/test/java`, but its lone package-info, under `src/main/java`, still describes +"Functional acceptance tests for the application.") Every module that the `app` composes also carries a Spring `@Configuration` class in a -`.config` sub-package; the `app` module's `Application` class `@Import`s them all so the -whole tree assembles into a single Spring context. +`.config` sub-package; the `app` module's `Application` class `@Import`s them all (each +referenced by simple name via its own import statement) so the whole tree assembles into +a single Spring context. == Module Naming @@ -22,6 +27,38 @@ is `my-service-parent`. Every module inherits its `groupId` and `version` from ` This prefix applies to `artifactId`s only — the directory names and Java package names listed below are unaffected. +Several modules also carry a Maven ``/`` derived from the `appName` prompt +(see <>). + +[[appname-naming]] +== appName-Derived Naming + +`appName` (e.g. `Order Service`) drives the ``/`` of these modules. The +area name is folded into named service areas' `service-{name}`/`domain-service-{name}` +naming (trailing for ``, mid-sentence for prose); other modules use `appName` as-is. + +[cols="1,2,2"] +|=== +|Module |`` |`` + +|`parent/` |`${appName} Parent` |`${appName}'s parent POM.` +|`common-testing` |`${appName} Common Testing` |`${appName}'s common testing.` +|`domain-{type}` (REST) |`${appName} Domain REST` |`${appName}'s REST presentation tier domain classes.` +|`domain-{type}` (GraphQL) |`${appName} Domain GraphQL` |`${appName}'s GraphQL presentation tier domain classes.` +|`presentation-graphql` |`${appName} Presentation GraphQL` |`${appName}'s GraphQL presentation tier.` +|`domain-service[-{name}]` |`${appName} Domain Service[ {Area}]` |`${appName}'s[ {area}] service tier domain classes.` +|`service[-{name}]` |`${appName} Service[ {Area}]` |`${appName}'s[ {area}] service tier (business logic).` +|`app` |`${appName} App` |`${appName}'s application configuration including building its deployment assembly.` +|`acceptance-tests` |`${appName} Acceptance Tests` |`${appName}'s acceptance tests.` +|=== + +`common-domain`, `presentation-rest`, and the per-integration modules (`domain-{type}-{name}`, +`integration-{type}-{name}`) have no dedicated `appName`-derived ``/description yet and +keep their existing generic description text. + +`spring.application.name` in `app`'s `application.properties` uses `appName`'s generated-property +form (spaces removed, lowercased), e.g. `orderservice`. + == Fixed Modules These modules are always generated regardless of input. @@ -35,7 +72,16 @@ _Package_: `{base.package}.common.domain` Shared test infrastructure. + _Package_: `{base.package}.common.testing` + -_Depends on_: `common-domain` +_Depends on_: `common-domain` and every other domain module (`domain-{type}-{name}`, +`domain-service`/`domain-service-{name}`, `domain-{type}` presentation domain modules). +No domain module depends back on `common-testing` — only `service`/`service-{name}` does — so +this stays acyclic. + +_Spring (includeSpring=true)_: `spring-boot-starter-test` at compile scope; additionally +`spring-boot-starter-graphql-test` at compile scope when a `graphql` presentation is present. +Carries `RestAcceptanceTestBase` (when a `rest` presentation is present) and/or +`GraphQlAcceptanceTestBase` (when a `graphql` presentation is present) — shared +`@SpringBootTest(webEnvironment = RANDOM_PORT)` base classes that `acceptance-tests`' +`AppRestAcceptanceTestBase`/`AppGraphQlAcceptanceTestBase` extend. `service` _or_ `service-{name}`:: Business logic for a service area (see the `serviceAreas` prompt). @@ -43,20 +89,28 @@ Business logic for a service area (see the `serviceAreas` prompt). _Package_: `{base.package}.service` or `{base.package}.service.{name}` + _Depends on_: `common-domain`, its own `domain-service` / `domain-service-{name}` module, and the integration module(s) it consumes — a named area takes the integration sharing its name, while the -single default `service` takes every integration module +single default `service` takes every integration module; if `includeSpring=true`, also +`common-testing` (test scope) `domain-service` _or_ `domain-service-{name}`:: Domain classes owned by a service area — one per `service` / `service-{name}` module. + _Package_: `{base.package}.domain.service` or `{base.package}.domain.service.{name}` + -_Depends on_: `common-domain` +_Depends on_: `common-domain`; if `includeSpring=true`, also `spring-boot-starter-validation` +(no domain module depends on `common-testing`) `app`:: -Application assembly — produces the runnable artifact (e.g. Spring Boot über-jar). -Contains the Spring Boot `Application` (`@SpringBootApplication`) class in -`{base.package}.app.config`, and adds the role starters its composition calls for — a `rest` -presentation adds `spring-boot-starter-web` and an `AppServletInitializer` (WAR deployment) -class; a `database` integration adds `spring-boot-starter-data-jpa` and `@EnableTransactionManagement`. +Application assembly — produces the runnable artifact (e.g. Spring Boot über-jar). If +`includeSpring=true`, `app` is always a monitorable web service regardless of +`presentationTypes` — it adds the core `spring-boot-starter`, `spring-boot-starter-webmvc`, +`spring-boot-starter-actuator`, and `spring-boot-admin-starter-client` (actuator needs a web +server to expose its endpoints), and always generates an `AppServletInitializer` (WAR +deployment) class; a `database` integration additionally adds `spring-boot-starter-data-jpa` +and `@EnableTransactionManagement`. Contains the Spring Boot `Application` +(`@SpringBootApplication`) class in `{base.package}.app.config`. Also creates +`application.properties` (actuator, Spring Boot Admin registration, logging defaults, plus JPA +and SQL logging when a `database` integration is present) and six empty per-environment files +(`application-{ci,dev,local,prod,qa,uat}.properties`) in `src/main/resources`. + _Package_: `{base.package}.app`, `{base.package}.app.config` + _Depends on_: all non-test modules (`common-domain`, all domain and integration modules, @@ -68,8 +122,20 @@ Functional acceptance tests that exercise the application from the outside. _Package_: `{base.package}.at` + _Depends on_: `app`, `common-domain`, all `domain-{type}-{name}` integration domain modules, all `domain-service` / `domain-service-{name}` service-area domain modules, -all `domain-{type}` presentation domain modules + -_Plugin_: `maven-failsafe-plugin` (`integration-test` + `verify` goals) +all `domain-{type}` presentation domain modules (all labeled "this app" in the pom); also +`common-testing` (test scope) unconditionally, and — if `includeSpring=true` and a `database` +integration is present — `datasource-proxy-spring-boot-starter` (SQL logging), plus +`spring-graphql-test` (test scope) when a `graphql` presentation is present + +_Plugin_: `maven-failsafe-plugin` (`integration-test` + `verify` goals) + +_Spring (includeSpring=true)_: generates `{appNameClass}AcceptanceTestConfiguration` +(package `{base.package}.at.config`) and `src/test/resources/logback-spring.xml`; generates +`AppRestAcceptanceTestBase` (package `{base.package}.at`, extends `common-testing`'s +`RestAcceptanceTestBase`) when a `rest` presentation is present, and/or +`AppGraphQlAcceptanceTestBase` (extends `GraphQlAcceptanceTestBase`) when a `graphql` +presentation is present — both `@ContextConfiguration`'d with the AT configuration class plus +every generated module's `@Configuration` class (reachable transitively via the `app` +dependency), and excluding `DataSourceAutoConfiguration`/`XADataSourceAutoConfiguration` only +when a `database` integration is present. == Integration Modules @@ -87,7 +153,8 @@ Integration implementation classes — DAOs, Spring Data JPA repositories, REST (e.g. `integration-db-users`, `integration-rest-orders`). + _Package_: `{base.package}.integration.{type}.{name}` + -_Depends on_: `common-domain`, `domain-{type}-{name}` + +_Depends on_: `common-domain`, `domain-{type}-{name}`; if `includeSpring=true` and the type +is `database`, also `datasource-proxy-spring-boot-starter` (SQL logging) + _Plugin_: `maven-failsafe-plugin` (`integration-test` + `verify` goals) == Presentation Modules @@ -95,10 +162,12 @@ _Plugin_: `maven-failsafe-plugin` (`integration-test` + `verify` goals) Generated per type in the `presentationTypes` prompt. `domain-{type}`:: -Domain classes for a presentation tier (e.g. `domain-rest`, `domain-graphql`). +Domain classes for a presentation tier (e.g. `domain-rest`, `domain-graphql`) — these map +to/from the service-tier domain objects ("the service domain deps"). + _Package_: `{base.package}.domain.{type}` + -_Depends on_: `common-domain` +_Depends on_: `common-domain`, every `domain-service`/`domain-service-{name}` module; if +`includeSpring=true`, also `spring-boot-starter-validation` `presentation-{type}`:: Request-handling classes — controllers, resolvers, etc. @@ -116,10 +185,13 @@ It contains: * `` — inherits `spring-boot-starter-parent` (4.1.0) via an empty ``, supplying Spring dependency management and plugin defaults * `pom` +* ``/`` derived from `appName` (see <>) * `` — all sibling modules referenced as `../module` relative paths, sorted alphabetically * `` — every module pinned to `${project.version}`, split into a TEST section (`acceptance-tests` and `common-testing`, at `test`) and a PROD section (all other - modules, plus managed third-party dependencies such as `spring-core`) + modules, plus managed third-party dependencies such as `spring-core`, `datasource-proxy-spring-boot-starter`, + and `spring-boot-admin-starter-client` — the latter two pinned via their own version properties + since they aren't part of the Spring Boot BOM) * Common properties: Java 21 via `maven.compiler.release`; UTF-8 source encoding * `` — pinned versions for `maven-compiler-plugin`, `maven-failsafe-plugin`, `maven-jar-plugin` (with `skipIfEmpty=true`), `maven-surefire-plugin`, diff --git a/src/site/asciidoc/requirements.adoc b/src/site/asciidoc/requirements.adoc index 874c7d2..5a9f3de 100644 --- a/src/site/asciidoc/requirements.adoc +++ b/src/site/asciidoc/requirements.adoc @@ -10,6 +10,17 @@ properties below, then produce the module structure described in this document. == Prompts +=== `appName` + +Prompt for a brief, human-readable application name: capitalized words +separated by spaces (e.g. `Order Service`). No default — every project needs +its own name. Used throughout generated content in three forms: + +* Class names: spaces removed (e.g. `OrderService`). +* Generated property values: spaces removed and lowercased (e.g. `orderservice` + — used for `spring.application.name`). +* Prose (POM ``/``, Javadoc): unmodified. + === `package` — base package Prompt for the base Java package for the application (e.g. `com.example.myapp`). @@ -51,9 +62,12 @@ Whether to generate the Spring Boot wiring. app-composed module carries a `@Configuration` class, and Spring Boot starters are assigned per module by role — `common-domain` carries the core `spring-boot-starter` that the plain `domain-*` and service modules inherit transitively. The `app` module - carries the `Application` class and adds the role starters its composition calls for: a - `rest` presentation adds `spring-boot-starter-web` and an `AppServletInitializer`; a - `database` integration adds `spring-boot-starter-data-jpa` and `@EnableTransactionManagement`. + carries the `Application` class and is always a monitorable web service — core starter, + `spring-boot-starter-webmvc`, `spring-boot-starter-actuator`, `spring-boot-admin-starter-client`, + and an `AppServletInitializer` (WAR deployment), regardless of `presentationTypes` — plus + `spring-boot-starter-data-jpa` and `@EnableTransactionManagement` when a `database` + integration is present. `acceptance-tests` additionally gets shared REST/GraphQL + acceptance-test base classes (see <>). * `false`: omit all Spring artifacts — configs, classes, and POM items — producing a plain multi-module Java project with the same module structure. @@ -65,13 +79,20 @@ Do not create a `pom.xml` at the project root. === `parent/` * `pom` +* `${appName} Parent`; `${appName}'s parent POM.` * List every sibling module in `` using `../module` relative paths, sorted alphabetically. * List every sibling module in `` pinned to - `${project.version}`, sorted alphabetically by `artifactId`. + `${project.version}`, sorted alphabetically by `artifactId`. `acceptance-tests` is + managed here like every other module, but is never referenced as an actual + `` by any other module. * Inherit `spring-boot-starter-parent` (version 4.1.0) as the parent POM — via `` with an empty ``, not a BOM import — so Spring artifact versions and sensible plugin defaults are managed centrally. +* `datasource-proxy-spring-boot-starter` and `spring-boot-admin-starter-client` are + third-party artifacts outside the Spring Boot BOM, so each gets its own + `dependencyManagement` entry with an explicit version pinned via a property + (`datasource-proxy-spring-boot-starter.version`, `spring-boot-admin-starter-client.version`). * Properties: Java 21 via `maven.compiler.release`; UTF-8 source encoding via `project.build.sourceEncoding`. * `` — declare pinned versions for `maven-compiler-plugin`, @@ -97,9 +118,23 @@ Shared domain classes. No dependencies on other generated modules. === `common-testing` -Shared test infrastructure. - -* Dependency: `common-domain` +Shared test infrastructure. `${appName} Common Testing`; +`${appName}'s common testing.`. + +* Dependencies: `common-domain` and every other domain module (`domain-{type}-{name}`, + `domain-service`/`domain-service-{name}`, `domain-{type}` presentation domain modules). + No domain module depends back on `common-testing` — only `service`/`service-{name}` + does — so this stays acyclic. +* If `includeSpring=true`: `spring-boot-starter-test` at *compile* scope (test-support + code other modules compile their tests against); additionally + `spring-boot-starter-graphql-test` at compile scope when a `graphql` presentation + is present (needed for `GraphQlAcceptanceTestBase`, below). +* If `includeSpring=true` and a `rest` presentation is present: generate + `RestAcceptanceTestBase` (`@SpringBootTest(webEnvironment = RANDOM_PORT)`, exposing a + `RestTestClient` bound to the running server via `@LocalServerPort`). +* If `includeSpring=true` and a `graphql` presentation is present: generate + `GraphQlAcceptanceTestBase` (`@SpringBootTest(webEnvironment = RANDOM_PORT)` + + `@AutoConfigureHttpGraphQlTester`, exposing an injected `HttpGraphQlTester`). === Per integration: `domain-{type}-{name}` and `integration-{type}-{name}` @@ -112,59 +147,115 @@ For each `type:name` integration entry, generate two modules clients, etc.). Dependencies: `common-domain`, `domain-{type}-{name}`. Configure `maven-failsafe-plugin` with the `integration-test` and `verify` goals. + If `includeSpring=true` and the type is `database`: also add + `datasource-proxy-spring-boot-starter` (SQL logging). === Per service area: `service` or `service-{name}` -Business logic for a service area. +Business logic for a service area. `${appName} Service[ {Area}]`; +`${appName}'s[ {area}] service tier (business logic).` +(the area name is folded in — trailing for the ``, mid-sentence for the +description — only for named areas; the single default `service` module omits it). Dependencies: `common-domain` and the area's own `domain-service` / -`domain-service-{name}` module. +`domain-service-{name}` module. If `includeSpring=true`: also `common-testing` +(test scope). For each service area, also generate a `domain-service` (single service) or -`domain-service-{name}` module holding that area's domain classes -(dependency: `common-domain`). +`domain-service-{name}` module holding that area's domain classes. +`${appName} Domain Service[ {Area}]`; +`${appName}'s[ {area}] service tier domain classes.`. +Dependency: `common-domain`. If `includeSpring=true`: also `spring-boot-starter-validation` +(no domain module depends on `common-testing`). === Per presentation tier: `domain-{type}` and `presentation-{type}` For each presentation type, generate two modules (e.g. `domain-rest` and `presentation-rest`): -* `domain-{type}` — domain classes for this presentation tier. - Dependency: `common-domain`. -* `presentation-{type}` — request-handling classes (controllers, etc.). - Dependencies: `common-domain`, `domain-{type}`. +* `domain-{type}` — domain classes for this presentation tier (DTOs, etc.), which map + to/from the service-tier domain objects ("the service domain deps"). + Dependencies: `common-domain`, every `domain-service`/`domain-service-{name}` module. + If `includeSpring=true`: also `spring-boot-starter-validation`. + `${appName} Domain {TYPE}`; `${appName}'s {TYPE} presentation + tier domain classes.` ({TYPE} is `REST`/`GraphQL`/capitalized-otherwise). +* `presentation-{type}` — request-handling classes (controllers, etc.), or resolver + classes for `graphql`. + Dependencies: `common-domain`, `domain-{type}`, every service module. + `presentation-graphql` additionally gets `${appName} Presentation GraphQL`; + `${appName}'s GraphQL presentation tier.` (no dedicated + requirement yet for other presentation types' `presentation-{type}` pom name/description). === `app` Assembles the runnable artifact (e.g. Spring Boot uber jar). +`${appName} App`; `${appName}'s application configuration +including building its deployment assembly.`. Depend on all generated modules except `acceptance-tests`, `common-testing`, -and `app` itself. Add the role starters its composition calls for: `spring-boot-starter-web` -when a `rest` presentation is present, `spring-boot-starter-data-jpa` when a `database` -integration is present. +and `app` itself. + +If `includeSpring=true`, `app` is always a monitorable web service regardless of +`presentationTypes`: it adds the core `spring-boot-starter`, `spring-boot-starter-webmvc`, +`spring-boot-starter-actuator`, and `spring-boot-admin-starter-client` (actuator needs a +web server to expose its endpoints over HTTP), and always generates an +`AppServletInitializer` (WAR deployment) class. A `database` integration additionally +adds `spring-boot-starter-data-jpa` and `@EnableTransactionManagement` on `Application`. Contains the Spring Boot `Application` (`@SpringBootApplication`) class in the `.config` sub-package, which `@Import`s the `@Configuration` class that every app-composed module -carries in its own `.config` sub-package. `Application` additionally carries -`@EnableTransactionManagement` when a `database` integration is present, and an -`AppServletInitializer` (WAR deployment) class is generated when a `rest` presentation is present. - +carries in its own `.config` sub-package — each referenced by simple name via its own +import statement (not inline fully-qualified names). + +Also creates `application.properties` in `src/main/resources` (actuator endpoints +exposed, health details shown, git/env/process info contributors, tracing, Spring Boot +Admin client registration, logging defaults, plus JPA and SQL logging via datasource-proxy +when a `database` integration is present; `spring.application.name` is `appName` in its +generated-property form, e.g. `orderservice`) +plus six empty per-environment files: `application-ci.properties`, `application-dev.properties`, +`application-local.properties`, `application-prod.properties`, `application-qa.properties`, +`application-uat.properties`. + +[[acceptance-tests-section]] === `acceptance-tests` Functional tests that exercise the application end-to-end. +`${appName} Acceptance Tests`; `${appName}'s acceptance +tests.`. * Dependencies: `app`, `common-domain`, all `domain-{type}-{name}` modules, all `domain-service` / `domain-service-{name}` modules, - all `domain-{type}` (presentation) modules. -* Do not depend on service (business-logic) modules or integration implementation modules. + all `domain-{type}` (presentation) modules (all labeled "this app" in the pom). + Do not depend on service (business-logic) modules or integration implementation modules. +* Also depends on `common-testing` (test scope) unconditionally, and — if + `includeSpring=true` and a `database` integration is present — `datasource-proxy-spring-boot-starter` + (SQL logging). +* If `includeSpring=true` and a `graphql` presentation is present: also + `org.springframework.graphql:spring-graphql-test` (test scope). * Configure `maven-failsafe-plugin` with the `integration-test` and `verify` goals. +* If `includeSpring=true`: generate `{appNameClass}AcceptanceTestConfiguration` + (`@Configuration`, package `{base.package}.at.config`) and `src/test/resources/logback-spring.xml` + (includes Boot's base logback config; exists to keep this module's Spring runtime from + picking up `app`'s logging configuration). +** If a `rest` presentation is present: generate `AppRestAcceptanceTestBase` (package + `{base.package}.at`), extending `common-testing`'s `RestAcceptanceTestBase`, + `@ContextConfiguration`'d with the AT configuration class plus every generated + module's `@Configuration` class (reachable transitively via the `app` dependency), + and — only when a `database` integration is present — `@EnableAutoConfiguration` + excluding `DataSourceAutoConfiguration`/`XADataSourceAutoConfiguration`. +** If a `graphql` presentation is present: generate `AppGraphQlAcceptanceTestBase` + analogously, extending `GraphQlAcceptanceTestBase`. [[source-structure]] == Source Structure Each module's source root is the package derived from the base package prompt. -Place a `package-info.java` in each module's primary leaf package under both -`src/main/java` and `src/test/java`, with a Javadoc comment describing the package's -purpose; the test-side comment is prefixed with "Tests for ...". Each app-composed -module additionally carries a `@Configuration` class in a `.config` sub-package. -The package for each module is: +Place a `package-info.java` in each module's primary leaf package under +`src/main/java` only — never under `src/test/java`, since Java rejects two +`package-info.java` files compiled for the same package (`src/test/java` packages are +still scaffolded as empty directories for test classes to land in). `acceptance-tests` +is the one module whose test package holds real test content (the actual `*AT` +acceptance-test classes), so its lone `src/main/java` package-info Javadoc describes +"Functional acceptance tests for the application." rather than supporting infrastructure. +Each app-composed module additionally carries a `@Configuration` class in a `.config` +sub-package. The package for each module is: [cols="1,1"] |=== diff --git a/src/site/asciidoc/usage.adoc b/src/site/asciidoc/usage.adoc index 5c7558e..3d18d07 100644 --- a/src/site/asciidoc/usage.adoc +++ b/src/site/asciidoc/usage.adoc @@ -15,7 +15,8 @@ mvn archetype:generate \ Replace `VERSION` with the archetype release you want to use. -Maven first prompts for the standard Maven coordinates: +Maven prompts for `appName` and `integrations` first — the two archetype-specific properties +with no default — then the standard Maven coordinates: [cols="1,2"] |=== @@ -27,9 +28,32 @@ Maven first prompts for the standard Maven coordinates: |`package` |Base Java package (e.g. `com.example.myservice`) |=== -It then prompts for the three archetype-specific properties below. +`serviceAreas`, `presentationTypes`, and `includeSpring` also have defaults, so none of them +are prompted interactively; override any of them with a `-D` flag, as shown in the full +command line example below. + +All five archetype-specific properties are documented below, in prompt / declaration order: +`appName`, `integrations`, `serviceAreas`, `presentationTypes`, `includeSpring`. The generated project is created in `./{artifactId}/` relative to the current directory. +== `appName` + +Brief, human-readable application name: capitalized words separated by spaces. + +[source] +---- +Order Service +---- + +`appName` drives generated content in three ways: + +* Class names remove the spaces, e.g. `OrderService`. +* Generated property values remove the spaces and lowercase everything, e.g. `orderservice` + (used for `spring.application.name`). +* Prose (POM ``/``, Javadoc) uses it unmodified, e.g. "Order Service". + +No default — every project needs its own name. + == `integrations` Comma-separated `type:name` pairs, one per external data source or system. @@ -78,6 +102,26 @@ Supported types include `rest`, `graphql`, `soap`, `grpc`, and any other label y Each type produces a `domain-{type}` module and a `presentation-{type}` module. +== `includeSpring` (default: `true`) + +Whether to generate the Spring Boot wiring. + +[source] +---- +true +---- + +* `true` (default): every app-composed module gets a `@Configuration` class plus the Spring Boot + starter suited to its role, the parent inherits `spring-boot-starter-parent`, and `app` gets the + `Application` class — always a monitorable web service (core starter, webmvc, actuator, Spring + Boot Admin client, `AppServletInitializer`) regardless of `presentationTypes` — plus + `spring-boot-starter-data-jpa` and `@EnableTransactionManagement` when a `database` integration + is present. +* `false`: omit all Spring artifacts (configs, classes, and pom items). + +Not prompted interactively (non-empty default); override with `-DincludeSpring=false` on the +command line to omit Spring entirely. + == Examples === Minimal project @@ -86,12 +130,13 @@ Each type produces a `domain-{type}` module and a `presentation-{type}` module. |=== |Property |Value +|`appName` |`My Service` |`integrations` |(blank) |`serviceAreas` |(blank) |`presentationTypes` |(blank) |=== -Produces: `acceptance-tests`, `app`, `common-domain`, `common-testing`, `service`. +Produces: `acceptance-tests`, `app`, `common-domain`, `common-testing`, `domain-service`, `service`. === Typical single-service REST API backed by a database @@ -99,6 +144,7 @@ Produces: `acceptance-tests`, `app`, `common-domain`, `common-testing`, `service |=== |Property |Value +|`appName` |`My Service` |`integrations` |`database:users` |`serviceAreas` |(blank) |`presentationTypes` |`rest` @@ -114,6 +160,7 @@ Produces: `acceptance-tests`, `app`, `common-domain`, `common-testing`, |=== |Property |Value +|`appName` |`My Service` |`integrations` |`database:users,rest:orders,graphql:catalog` |`serviceAreas` |`orders,inventory` |`presentationTypes` |`rest,graphql` @@ -142,6 +189,7 @@ mvn archetype:generate -B \ -DartifactId=my-service \ -Dversion=1.0.0-SNAPSHOT \ -Dpackage=com.example.myservice \ + -DappName="My Service" \ -Dintegrations=database:users,rest:orders,graphql:catalog \ -DserviceAreas=orders,inventory \ -DpresentationTypes=rest,graphql diff --git a/src/test/resources/projects/basic/archetype.properties b/src/test/resources/projects/basic/archetype.properties index 9910a3a..2b41a92 100644 --- a/src/test/resources/projects/basic/archetype.properties +++ b/src/test/resources/projects/basic/archetype.properties @@ -2,6 +2,7 @@ groupId=com.example artifactId=basic-test version=1.0.0-SNAPSHOT package=com.example.basic +appName=Basic Test integrations=database:main serviceAreas=, presentationTypes=rest diff --git a/src/test/resources/projects/basic/verify.groovy b/src/test/resources/projects/basic/verify.groovy index 5e125e3..b199b40 100644 --- a/src/test/resources/projects/basic/verify.groovy +++ b/src/test/resources/projects/basic/verify.groovy @@ -1,5 +1,5 @@ // ── Test: basic ─────────────────────────────────────────────────────────────── -// integrations=database:main serviceAreas=, presentationTypes=rest +// appName=Basic Test integrations=database:main serviceAreas=, presentationTypes=rest // Covers: db abbreviation, single service named "service", single presentation, // src skeleton, parent/ module structure, app/acceptance-tests deps @@ -21,6 +21,7 @@ def check = { String rel -> // own artifactId, the parent dependencyManagement, and the parent artifactId), and the generated // project's real Maven build — run before this script — would fail on any inconsistent reference. def aid = 'basic-test' +def appName = 'Basic Test' def raw = { String rel -> new File(basedir, rel).text } @@ -60,6 +61,8 @@ assert parentPom.contains('${project.version}') : "dependencyManagement assert parentPom.contains('com.example') : 'parent must have correct groupId' assert parentPom.contains('basic-test-parent') : 'parent must have correct artifactId' assert parentPom.contains('1.0.0-SNAPSHOT') : 'parent must have correct version' +assert parentPom.contains("${appName} Parent") : 'parent must be " Parent"' +assert parentPom.contains("${appName}'s parent POM.") : "parent must be \"'s parent POM.\"" assert parentPom.contains('21') : 'parent must declare java.version=21' assert parentPom.contains('${java.version}') \ : 'parent must use maven.compiler.release' @@ -73,6 +76,13 @@ assert parentPom.contains('maven-jar-plugin') : 'p assert parentPom.contains('maven-surefire-plugin') : 'parent must declare maven-surefire-plugin in pluginManagement' assert parentPom.contains('true') : 'maven-jar-plugin must configure skipIfEmpty=true' +// third-party deps outside the Spring Boot BOM (datasource-proxy, Spring Boot Admin client) are +// each pinned via their own managed version property +assert parentPom.contains('') : 'parent must pin a datasource-proxy-spring-boot-starter.version property' +assert parentPom.contains('') : 'parent must pin a spring-boot-admin-starter-client.version property' +assert parentPom.contains('datasource-proxy-spring-boot-starter') : 'parent dependencyManagement must manage datasource-proxy-spring-boot-starter' +assert parentPom.contains('spring-boot-admin-starter-client') : 'parent dependencyManagement must manage spring-boot-admin-starter-client' + // modernizer-maven-plugin: pinned, managed in pluginManagement, activated in build/plugins assert parentPom.contains('3.4.0') \ : 'parent must pin modernizer-maven-plugin version' @@ -111,6 +121,10 @@ def dmIds = dmBlock.findAll(/[^<]+<\/artifactId>/).collect { it.repl def dmModuleIds = dmIds.findAll { it.startsWith("${aid}-") }.collect { it.substring(aid.length() + 1) } assert dmModuleIds.sort() == modules.sort() : "parent must manage exactly the sibling modules" assert dmModuleIds.size() == modules.size() : "parent wrong entry count: expected ${modules.size()}, got ${dmModuleIds.size()}" +// acceptance-tests must never appear as an actual of any other module — only managed +modules.findAll { it != 'acceptance-tests' }.each { m -> + assert !text("${m}/pom.xml").contains('acceptance-tests') : "${m} must not depend on acceptance-tests" +} // ── 6. Child module references parent/pom.xml ─────────────────────── @@ -125,34 +139,29 @@ modules.each { m -> : "${m}/pom.xml must declare its own artifactId as '${aid}-${m}'" } -// ── 7. Source directory skeletons ──────────────────────────────────────────── +// ── 7. Source directory skeletons — package-info.java only under src/main/java ── +// Java rejects two package-info.java files compiled for the same package, so no module ever gets +// one under src/test/java (exhaustive check across every module, done here once for the archetype). def p = 'com/example/basic' def pkg = p.replace('/', '.') [ "common-domain/src/main/java/${p}/common/domain/package-info.java", - "common-domain/src/test/java/${p}/common/domain/package-info.java", "common-testing/src/main/java/${p}/common/testing/package-info.java", "domain-db-main/src/main/java/${p}/domain/db/main/package-info.java", - "domain-db-main/src/test/java/${p}/domain/db/main/package-info.java", "integration-db-main/src/main/java/${p}/integration/db/main/package-info.java", - "integration-db-main/src/test/java/${p}/integration/db/main/package-info.java", "service/src/main/java/${p}/service/package-info.java", - "service/src/test/java/${p}/service/package-info.java", "domain-rest/src/main/java/${p}/domain/rest/package-info.java", - "domain-rest/src/test/java/${p}/domain/rest/package-info.java", "presentation-rest/src/main/java/${p}/presentation/rest/package-info.java", - "presentation-rest/src/test/java/${p}/presentation/rest/package-info.java", "app/src/main/java/${p}/app/package-info.java", - "app/src/test/java/${p}/app/package-info.java", - "acceptance-tests/src/test/java/${p}/at/package-info.java", + "acceptance-tests/src/main/java/${p}/at/package-info.java", ].each { check(it) } assert text("common-domain/src/main/java/${p}/common/domain/package-info.java").contains("package ${pkg}.common.domain;") \ : 'common-domain package-info.java has wrong package declaration' assert text("service/src/main/java/${p}/service/package-info.java").contains("package ${pkg}.service;") \ : 'service package-info.java has wrong package declaration' -assert text("acceptance-tests/src/test/java/${p}/at/package-info.java").contains("package ${pkg}.at;") \ +assert text("acceptance-tests/src/main/java/${p}/at/package-info.java").contains("package ${pkg}.at;") \ : 'acceptance-tests package-info.java has wrong package declaration' // ── package-info.java Javadoc comments ─────────────────────────────────────── @@ -166,33 +175,52 @@ assert text("acceptance-tests/src/test/java/${p}/at/package-info.java").contains "presentation-rest/src/main/java/${p}/presentation/rest/package-info.java", "service/src/main/java/${p}/service/package-info.java", "app/src/main/java/${p}/app/package-info.java", - "acceptance-tests/src/test/java/${p}/at/package-info.java", + "acceptance-tests/src/main/java/${p}/at/package-info.java", ].each { path -> def content = text(path) assert content.startsWith('/**') : "${path} must start with a Javadoc comment" } -// common-testing and acceptance-tests now carry package-info in both source roots -assert new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() \ - : 'common-testing must have test Java package-info.java' -assert new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() \ - : 'acceptance-tests must have main Java package-info.java' +// no module has a src/test/java/**/package-info.java — exhaustive, every module in this project +modules.each { m -> + def testJavaDir = new File(basedir, "${m}/src/test/java") + if (testJavaDir.exists()) { + testJavaDir.eachFileRecurse { f -> + assert f.name != 'package-info.java' : "${m} must not have a src/test/java package-info.java: ${f}" + } + } +} // ── 8. Module dependency wiring ─────────────────────────────────────────────── +// common-testing depends on every domain module. No domain module depends back on +// common-testing (only service/service-{name} does), so this stays acyclic. assert text('common-testing/pom.xml').contains('common-domain') \ : 'common-testing must depend on common-domain' +assert text('common-testing/pom.xml').contains('domain-db-main') \ + : 'common-testing must depend on the integration domain module domain-db-main' +assert text('common-testing/pom.xml').contains('domain-service') \ + : 'common-testing must depend on domain-service' +assert text('common-testing/pom.xml').contains('domain-rest') \ + : 'common-testing must depend on domain-rest' +assert !text('domain-service/pom.xml').contains('common-testing') \ + : 'domain-service must not depend on common-testing (no domain module does)' assert text('domain-db-main/pom.xml').contains('common-domain') \ : 'domain-db-main missing common-domain dep' assert text('domain-rest/pom.xml').contains('common-domain') \ : 'domain-rest missing common-domain dep' +// domain-rest ("the service domain deps") depends on domain-service too +assert text('domain-rest/pom.xml').contains('domain-service') \ + : 'domain-rest missing domain-service dep (the service domain deps)' assert text('service/pom.xml').contains('common-domain') \ : 'service missing common-domain dep' def intPom = text('integration-db-main/pom.xml') assert intPom.contains('common-domain') : 'integration-db-main missing common-domain' assert intPom.contains('domain-db-main') : 'integration-db-main missing domain-db-main' +assert intPom.contains('datasource-proxy-spring-boot-starter') \ + : 'integration-db-main (a database integration) missing datasource-proxy dep' def presPom = text('presentation-rest/pom.xml') assert presPom.contains('common-domain') : 'presentation-rest missing common-domain' @@ -208,13 +236,17 @@ assert !appPom.contains('acceptance-tests') : 'app must assert !appPom.contains('common-testing') : 'app must not depend on common-testing' assert appPom.count('app') == 1 : 'app must not depend on itself' -// acceptance-tests → app + common-domain + integration domain + presentation domain +// acceptance-tests → app + common-domain + integration domain + presentation domain; + common-testing +// (TEST) and datasource-proxy (sql logging) def atPom = text('acceptance-tests/pom.xml') ['app', 'common-domain', 'domain-db-main', 'domain-rest'].each { dep -> assert atPom.contains("${dep}") : "acceptance-tests missing dep: $dep" } assert !atPom.contains('service') : 'acceptance-tests must not depend on service' assert !atPom.contains('integration-db-main') : 'acceptance-tests must not depend on integration-db-main' +assert atPom.contains('common-testing') : 'acceptance-tests missing common-testing TEST dep' +assert atPom.contains('datasource-proxy-spring-boot-starter') : 'acceptance-tests missing datasource-proxy dep' +assert !atPom.contains('spring-graphql-test') : 'acceptance-tests must not add spring-graphql-test (no graphql presentation)' // ── src directory scaffolding — all four dirs present in every module ───────── @@ -254,16 +286,63 @@ assert psScript.contains('\r\n') : 'PowerShell scr check('domain-service/pom.xml') assert text('domain-service/pom.xml').contains('common-domain') : 'domain-service missing common-domain dep' +assert text('domain-service/pom.xml').contains('spring-boot-starter-validation') : 'domain-service missing spring-boot-starter-validation dep' assert text('service/pom.xml').contains('domain-service') : 'service must depend on domain-service' +assert text('service/pom.xml').contains('common-testing') : 'service missing common-testing TEST dep' check("app/src/main/java/${p}/app/config/Application.java") check("app/src/main/java/${p}/app/config/AppServletInitializer.java") assert parentPom.contains('spring-boot-starter-parent') : 'parent must inherit spring-boot-starter-parent' -assert appPom.contains('spring-boot-starter-web') : 'app must depend on spring-boot-starter-web' +assert appPom.contains('spring-boot-starter') : 'app must depend on the core spring-boot-starter' +assert appPom.contains('spring-boot-starter-webmvc') : 'app must depend on spring-boot-starter-webmvc' +assert appPom.contains('spring-boot-starter-actuator') : 'app must depend on spring-boot-starter-actuator' +assert appPom.contains('spring-boot-admin-starter-client') : 'app must depend on spring-boot-admin-starter-client' +assert appPom.contains('spring-boot-starter-data-jpa') : 'app must depend on spring-boot-starter-data-jpa (database integration present)' // #1: each presentation module depends on all service modules assert text('presentation-rest/pom.xml').contains('service') : '#1: presentation-rest must depend on service' // #2: the single default 'service' module depends on every integration module assert text('service/pom.xml').contains('integration-db-main') : '#2: default service must depend on integration-db-main' +// ── acceptance-test infrastructure (REST only — no graphql presentation) ───── + +def atConfigClass = 'BasicTestAcceptanceTestConfiguration' +check("acceptance-tests/src/test/java/${p}/at/config/${atConfigClass}.java") +def atConfig = text("acceptance-tests/src/test/java/${p}/at/config/${atConfigClass}.java") +assert atConfig.contains('@Configuration') : 'AT configuration class must be @Configuration' +assert atConfig.contains("Configuration class for ${appName} ATs.") : 'AT configuration class Javadoc must name the app' + +check('acceptance-tests/src/test/resources/logback-spring.xml') +assert text('acceptance-tests/src/test/resources/logback-spring.xml') + .contains('') \ + : 'logback-spring.xml must include the Boot base logback config' + +check("acceptance-tests/src/test/java/${p}/at/AppRestAcceptanceTestBase.java") +def appRestAt = text("acceptance-tests/src/test/java/${p}/at/AppRestAcceptanceTestBase.java") +assert appRestAt.contains("Base test class for all ${appName} REST acceptance tests.") : 'AppRestAcceptanceTestBase Javadoc must name the app' +assert appRestAt.contains('extends RestAcceptanceTestBase') : 'AppRestAcceptanceTestBase must extend RestAcceptanceTestBase' +assert appRestAt.contains("@ContextConfiguration(classes = { ${atConfigClass}.class,") : 'AppRestAcceptanceTestBase must @ContextConfiguration with the AT config class first' +assert appRestAt.contains('@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class, XADataSourceAutoConfiguration.class })') \ + : 'AppRestAcceptanceTestBase must exclude DataSourceAutoConfiguration/XADataSourceAutoConfiguration (database integration present)' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/AppGraphQlAcceptanceTestBase.java").exists() \ + : 'AppGraphQlAcceptanceTestBase must not be generated (no graphql presentation)' + +check("common-testing/src/main/java/${p}/common/testing/RestAcceptanceTestBase.java") +assert text("common-testing/src/main/java/${p}/common/testing/RestAcceptanceTestBase.java") + .contains('@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)') \ + : 'RestAcceptanceTestBase must be a random-port @SpringBootTest' +assert !new File(basedir, "common-testing/src/main/java/${p}/common/testing/GraphQlAcceptanceTestBase.java").exists() \ + : 'GraphQlAcceptanceTestBase must not be generated (no graphql presentation)' + +// ── app: application.properties + per-environment profile files ───────────── + +def appProps = text('app/src/main/resources/application.properties') +assert appProps.contains('spring.application.name=basictest') : 'spring.application.name must be appName with spaces removed and lowercased' +assert appProps.contains('decorator.datasource.enabled=true') : 'application.properties must configure SQL logging' +['ci', 'dev', 'local', 'prod', 'qa', 'uat'].each { profile -> + def f = new File(basedir, "app/src/main/resources/application-${profile}.properties") + assert f.exists() : "Missing profile properties file: application-${profile}.properties" + assert f.text.isEmpty(): "application-${profile}.properties must be empty" +} + _buildLog.append("\n=== PASSED ===\n") true diff --git a/src/test/resources/projects/blank-presentation-types/archetype.properties b/src/test/resources/projects/blank-presentation-types/archetype.properties index 6cdae3a..3ad2143 100644 --- a/src/test/resources/projects/blank-presentation-types/archetype.properties +++ b/src/test/resources/projects/blank-presentation-types/archetype.properties @@ -2,6 +2,7 @@ groupId=com.example artifactId=blank-pres-test version=1.0.0-SNAPSHOT package=com.example.blankpres +appName=Blank Pres Test integrations=database:main serviceAreas=, presentationTypes=, , diff --git a/src/test/resources/projects/blank-presentation-types/verify.groovy b/src/test/resources/projects/blank-presentation-types/verify.groovy index 7d8899b..4289f18 100644 --- a/src/test/resources/projects/blank-presentation-types/verify.groovy +++ b/src/test/resources/projects/blank-presentation-types/verify.groovy @@ -1,5 +1,5 @@ // ── Test: blank-presentation-types ──────────────────────────────────────────── -// integrations=database:main serviceAreas=, presentationTypes=", ," +// appName=Blank Pres Test integrations=database:main serviceAreas=, presentationTypes=", ," // Covers: a non-empty presentationTypes value whose entries are all blank after // trimming is filtered to an empty list — no domain-{type} or // presentation-{type} modules generated; distinct from no-presentation @@ -22,6 +22,7 @@ def check = { String rel -> // tags; the prefix is verified explicitly via raw() (module's own artifactId + parent DM), and the // generated project's real Maven build — run before this script — fails on any inconsistent reference. def aid = 'blank-pres-test' +def appName = 'Blank Pres Test' def raw = { String rel -> new File(basedir, rel).text } @@ -56,6 +57,8 @@ modules.each { m -> assert parentPom.contains("${aid}-${m}") : "parent missing: $m" } assert parentPom.contains('pom') : "parent must have pom packaging" +assert parentPom.contains("${appName} Parent") : 'parent must be " Parent"' +assert parentPom.contains("${appName}'s parent POM.") : "parent must be \"'s parent POM.\"" def moduleOrder = modules.sort().collect { "../${it}" } def moduleBlock = parentPom.replaceAll(/(?s).*(.*?)<\/modules>.*/, '$1') @@ -79,38 +82,39 @@ modules.each { m -> : "${m}/pom.xml must declare its own artifactId as '${aid}-${m}'" } -// ── 6. Source skeletons ─────────────────────────────────────────────────────── +// ── 6. Source skeletons — package-info.java only under src/main/java ──────── def p = 'com/example/blankpres' def pkg = p.replace('/', '.') [ "common-domain/src/main/java/${p}/common/domain/package-info.java", - "common-domain/src/test/java/${p}/common/domain/package-info.java", "common-testing/src/main/java/${p}/common/testing/package-info.java", "domain-db-main/src/main/java/${p}/domain/db/main/package-info.java", - "domain-db-main/src/test/java/${p}/domain/db/main/package-info.java", "integration-db-main/src/main/java/${p}/integration/db/main/package-info.java", - "integration-db-main/src/test/java/${p}/integration/db/main/package-info.java", "service/src/main/java/${p}/service/package-info.java", - "service/src/test/java/${p}/service/package-info.java", "app/src/main/java/${p}/app/package-info.java", - "app/src/test/java/${p}/app/package-info.java", - "acceptance-tests/src/test/java/${p}/at/package-info.java", + "acceptance-tests/src/main/java/${p}/at/package-info.java", ].each { check(it) } assert text("common-domain/src/main/java/${p}/common/domain/package-info.java").contains("package ${pkg}.common.domain;") : 'common-domain package-info.java has wrong package declaration' assert text("common-testing/src/main/java/${p}/common/testing/package-info.java").contains("package ${pkg}.common.testing;") : 'common-testing package-info.java has wrong package declaration' assert text("service/src/main/java/${p}/service/package-info.java").contains("package ${pkg}.service;") : 'service package-info.java has wrong package declaration' assert text("app/src/main/java/${p}/app/package-info.java").contains("package ${pkg}.app;") : 'app package-info.java has wrong package declaration' -assert text("acceptance-tests/src/test/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' -assert new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() : 'common-testing must have test Java package-info.java' -assert new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() : 'acceptance-tests must have main Java package-info.java' +assert text("acceptance-tests/src/main/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/package-info.java").exists() : 'acceptance-tests must not have a test-side package-info.java' // ── 7. Dependency wiring ───────────────────────────────────────────────────── assert text('domain-db-main/pom.xml').contains('common-domain') : 'domain-db-main missing common-domain dep' assert text('integration-db-main/pom.xml').contains('domain-db-main') : 'integration-db-main missing domain dep' +assert text('integration-db-main/pom.xml').contains('datasource-proxy-spring-boot-starter') \ + : 'integration-db-main (a database integration) missing datasource-proxy dep' assert text('service/pom.xml').contains('common-domain') : 'service missing common-domain dep' +assert text('service/pom.xml').contains('common-testing') : 'service missing common-testing TEST dep' +assert !text('domain-service/pom.xml').contains('common-testing') : 'domain-service must not depend on common-testing (no domain module does)' +assert text('domain-service/pom.xml').contains('spring-boot-starter-validation') : 'domain-service missing spring-boot-starter-validation dep' +assert text('common-testing/pom.xml').contains('domain-db-main') : 'common-testing missing integration domain dep' +assert text('common-testing/pom.xml').contains('domain-service') : 'common-testing must depend on domain-service' def appPom = text('app/pom.xml') ['common-domain', 'domain-db-main', 'integration-db-main', 'service'].each { dep -> @@ -118,6 +122,9 @@ def appPom = text('app/pom.xml') } assert !appPom.contains('domain-rest') : 'app must not reference non-existent presentation domain' assert !appPom.contains('presentation-rest') : 'app must not reference non-existent presentation module' +assert appPom.contains('spring-boot-starter-webmvc') : 'app must depend on spring-boot-starter-webmvc regardless of presentationTypes' +assert appPom.contains('spring-boot-starter-actuator') : 'app must depend on spring-boot-starter-actuator' +assert appPom.contains('spring-boot-admin-starter-client') : 'app must depend on spring-boot-admin-starter-client' def atPom = text('acceptance-tests/pom.xml') ['app', 'common-domain', 'domain-db-main'].each { dep -> @@ -125,6 +132,9 @@ def atPom = text('acceptance-tests/pom.xml') } assert !atPom.contains('domain-rest') : 'acceptance-tests must not reference non-existent presentation domain' assert !atPom.contains('service') : 'acceptance-tests must not depend on service' +assert atPom.contains('common-testing') : 'acceptance-tests missing common-testing TEST dep' +assert atPom.contains('datasource-proxy-spring-boot-starter') : 'acceptance-tests missing datasource-proxy dep' +assert !atPom.contains('spring-graphql-test') : 'acceptance-tests must not add spring-graphql-test (no graphql presentation)' // ── src directory scaffolding — all four dirs present in every module ───────── @@ -144,5 +154,28 @@ modules.findAll { it.startsWith('integration-') }.each { m -> : "${m} must configure maven-failsafe-plugin" } +// ── Acceptance-test infrastructure: AT config + logback exist; no REST/GraphQL +// base classes anywhere (no presentation types at all) ──────────────────── + +def atConfigClass = 'BlankPresTestAcceptanceTestConfiguration' +check("acceptance-tests/src/test/java/${p}/at/config/${atConfigClass}.java") +assert text("acceptance-tests/src/test/java/${p}/at/config/${atConfigClass}.java").contains("Configuration class for ${appName} ATs.") \ + : 'AT configuration class Javadoc must name the app' +check('acceptance-tests/src/test/resources/logback-spring.xml') + +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/AppRestAcceptanceTestBase.java").exists() \ + : 'AppRestAcceptanceTestBase must not be generated (no rest presentation)' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/AppGraphQlAcceptanceTestBase.java").exists() \ + : 'AppGraphQlAcceptanceTestBase must not be generated (no graphql presentation)' +assert !new File(basedir, "common-testing/src/main/java/${p}/common/testing/RestAcceptanceTestBase.java").exists() \ + : 'RestAcceptanceTestBase must not be generated (no rest presentation)' +assert !new File(basedir, "common-testing/src/main/java/${p}/common/testing/GraphQlAcceptanceTestBase.java").exists() \ + : 'GraphQlAcceptanceTestBase must not be generated (no graphql presentation)' + +// ── app: application.properties uses the appName property form ────────────── + +assert text('app/src/main/resources/application.properties').contains('spring.application.name=blankprestest') \ + : 'spring.application.name must be appName with spaces removed and lowercased' + _buildLog.append("\n=== PASSED ===\n") true diff --git a/src/test/resources/projects/blank-service-areas/archetype.properties b/src/test/resources/projects/blank-service-areas/archetype.properties index c3c2410..67cd5d0 100644 --- a/src/test/resources/projects/blank-service-areas/archetype.properties +++ b/src/test/resources/projects/blank-service-areas/archetype.properties @@ -2,6 +2,7 @@ groupId=com.example artifactId=blank-areas-test version=1.0.0-SNAPSHOT package=com.example.blankareas +appName=Blank Areas Test integrations=database:main serviceAreas= , , presentationTypes=rest diff --git a/src/test/resources/projects/blank-service-areas/verify.groovy b/src/test/resources/projects/blank-service-areas/verify.groovy index 677b1b9..d1fb3ca 100644 --- a/src/test/resources/projects/blank-service-areas/verify.groovy +++ b/src/test/resources/projects/blank-service-areas/verify.groovy @@ -1,5 +1,5 @@ // ── Test: blank-service-areas ───────────────────────────────────────────────── -// integrations=database:main serviceAreas=" , ," presentationTypes=rest +// appName=Blank Areas Test integrations=database:main serviceAreas=" , ," presentationTypes=rest // Covers: the serviceModules.isEmpty() fallback — a non-empty serviceAreas that // contains only blank entries after trimming; all produce "service-" (8 chars, // not > 8) so the list is empty, triggering fallback to a single plain "service" @@ -23,6 +23,7 @@ def check = { String rel -> // tags; the prefix is verified explicitly via raw() (module's own artifactId + parent DM), and the // generated project's real Maven build — run before this script — fails on any inconsistent reference. def aid = 'blank-areas-test' +def appName = 'Blank Areas Test' def raw = { String rel -> new File(basedir, rel).text } @@ -62,6 +63,8 @@ modules.each { m -> assert parentPom.contains("${aid}-${m}") : "parent missing: $m" } assert parentPom.contains('pom') : "parent must have pom packaging" +assert parentPom.contains("${appName} Parent") : 'parent must be " Parent"' +assert parentPom.contains("${appName}'s parent POM.") : "parent must be \"'s parent POM.\"" def moduleOrder = modules.sort().collect { "../${it}" } def moduleBlock = parentPom.replaceAll(/(?s).*(.*?)<\/modules>.*/, '$1') @@ -85,50 +88,61 @@ modules.each { m -> : "${m}/pom.xml must declare its own artifactId as '${aid}-${m}'" } -// ── 6. Source skeletons ─────────────────────────────────────────────────────── +// ── 6. Source skeletons — package-info.java only under src/main/java ──────── def p = 'com/example/blankareas' def pkg = p.replace('/', '.') [ "common-domain/src/main/java/${p}/common/domain/package-info.java", - "common-domain/src/test/java/${p}/common/domain/package-info.java", "common-testing/src/main/java/${p}/common/testing/package-info.java", + "domain-service/src/main/java/${p}/domain/service/package-info.java", "domain-db-main/src/main/java/${p}/domain/db/main/package-info.java", - "domain-db-main/src/test/java/${p}/domain/db/main/package-info.java", "integration-db-main/src/main/java/${p}/integration/db/main/package-info.java", - "integration-db-main/src/test/java/${p}/integration/db/main/package-info.java", "domain-rest/src/main/java/${p}/domain/rest/package-info.java", - "domain-rest/src/test/java/${p}/domain/rest/package-info.java", "presentation-rest/src/main/java/${p}/presentation/rest/package-info.java", - "presentation-rest/src/test/java/${p}/presentation/rest/package-info.java", "service/src/main/java/${p}/service/package-info.java", - "service/src/test/java/${p}/service/package-info.java", "app/src/main/java/${p}/app/package-info.java", - "app/src/test/java/${p}/app/package-info.java", - "acceptance-tests/src/test/java/${p}/at/package-info.java", + "acceptance-tests/src/main/java/${p}/at/package-info.java", ].each { check(it) } assert text("common-domain/src/main/java/${p}/common/domain/package-info.java").contains("package ${pkg}.common.domain;") : 'common-domain package-info.java has wrong package declaration' assert text("common-testing/src/main/java/${p}/common/testing/package-info.java").contains("package ${pkg}.common.testing;") : 'common-testing package-info.java has wrong package declaration' assert text("service/src/main/java/${p}/service/package-info.java").contains("package ${pkg}.service;") : 'service package-info.java has wrong package declaration' assert text("app/src/main/java/${p}/app/package-info.java").contains("package ${pkg}.app;") : 'app package-info.java has wrong package declaration' -assert text("acceptance-tests/src/test/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' -assert new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() : 'common-testing must have test Java package-info.java' -assert new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() : 'acceptance-tests must have main Java package-info.java' +assert text("acceptance-tests/src/main/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/package-info.java").exists() : 'acceptance-tests must not have a test-side package-info.java' // ── 7. Dependency wiring ───────────────────────────────────────────────────── assert text('domain-db-main/pom.xml').contains('common-domain') : 'domain-db-main missing common-domain dep' assert text('integration-db-main/pom.xml').contains('domain-db-main') : 'integration-db-main missing domain-db-main dep' +assert text('integration-db-main/pom.xml').contains('datasource-proxy-spring-boot-starter') \ + : 'integration-db-main (a database integration) missing datasource-proxy dep' assert text('domain-rest/pom.xml').contains('common-domain') : 'domain-rest missing common-domain dep' +assert text('domain-rest/pom.xml').contains('domain-service') : 'domain-rest missing domain-service dep (the service domain deps)' assert text('presentation-rest/pom.xml').contains('domain-rest') : 'presentation-rest missing domain-rest dep' assert text('service/pom.xml').contains('common-domain') : 'service missing common-domain dep' +assert text('service/pom.xml').contains('common-testing') : 'service missing common-testing TEST dep' +assert !text('domain-service/pom.xml').contains('common-testing') : 'domain-service must not depend on common-testing (no domain module does)' +assert text('domain-service/pom.xml').contains('spring-boot-starter-validation') : 'domain-service missing spring-boot-starter-validation dep' +assert text('common-testing/pom.xml').contains('domain-db-main') : 'common-testing missing integration domain dep' +assert text('common-testing/pom.xml').contains('domain-service') : 'common-testing must depend on domain-service' +assert text('common-testing/pom.xml').contains('domain-rest') : 'common-testing must depend on domain-rest' def appPom = text('app/pom.xml') ['common-domain', 'domain-db-main', 'domain-rest', 'integration-db-main', 'presentation-rest', 'service'].each { dep -> assert appPom.contains("${dep}") : "app missing dep: $dep" } +assert appPom.contains('spring-boot-starter') : 'app must depend on the core spring-boot-starter' +assert appPom.contains('spring-boot-starter-webmvc') : 'app must depend on spring-boot-starter-webmvc' +assert appPom.contains('spring-boot-starter-actuator') : 'app must depend on spring-boot-starter-actuator' +assert appPom.contains('spring-boot-admin-starter-client'): 'app must depend on spring-boot-admin-starter-client' +assert appPom.contains('spring-boot-starter-data-jpa') : 'app must depend on spring-boot-starter-data-jpa' + +def atPom = text('acceptance-tests/pom.xml') +assert atPom.contains('common-testing') : 'acceptance-tests missing common-testing TEST dep' +assert atPom.contains('datasource-proxy-spring-boot-starter') : 'acceptance-tests missing datasource-proxy dep' // ── src directory scaffolding — all four dirs present in every module ───────── @@ -148,5 +162,12 @@ modules.findAll { it.startsWith('integration-') }.each { m -> : "${m} must configure maven-failsafe-plugin" } +// ── Acceptance-test infrastructure (REST only) ─────────────────────────────── + +check("acceptance-tests/src/test/java/${p}/at/AppRestAcceptanceTestBase.java") +check("common-testing/src/main/java/${p}/common/testing/RestAcceptanceTestBase.java") +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/AppGraphQlAcceptanceTestBase.java").exists() \ + : 'AppGraphQlAcceptanceTestBase must not be generated (no graphql presentation)' + _buildLog.append("\n=== PASSED ===\n") true diff --git a/src/test/resources/projects/full/archetype.properties b/src/test/resources/projects/full/archetype.properties index f81f09b..e2f3ff6 100644 --- a/src/test/resources/projects/full/archetype.properties +++ b/src/test/resources/projects/full/archetype.properties @@ -2,6 +2,7 @@ groupId=com.example artifactId=full-test version=1.0.0-SNAPSHOT package=com.example.full +appName=Full Test integrations=database:users,graphql:catalog serviceAreas=orders,notifications presentationTypes=rest,graphql diff --git a/src/test/resources/projects/full/verify.groovy b/src/test/resources/projects/full/verify.groovy index cf5344c..64bad8a 100644 --- a/src/test/resources/projects/full/verify.groovy +++ b/src/test/resources/projects/full/verify.groovy @@ -1,5 +1,5 @@ // ── Test: full ──────────────────────────────────────────────────────────────── -// integrations=database:users,graphql:catalog +// appName=Full Test integrations=database:users,graphql:catalog // serviceAreas=orders,notifications presentationTypes=rest,graphql // Covers: all features together; graphql as both integration type (graphql:catalog) // and presentation type (graphql) — two distinct modules, no name collision @@ -21,6 +21,7 @@ def check = { String rel -> // tags; the prefix is verified explicitly via raw() (module's own artifactId + parent DM), and the // generated project's real Maven build — run before this script — fails on any inconsistent reference. def aid = 'full-test' +def appName = 'Full Test' def raw = { String rel -> new File(basedir, rel).text } @@ -93,37 +94,27 @@ modules.each { m -> : "${m}/pom.xml must declare its own artifactId as '${aid}-${m}'" } -// ── 7. Source skeletons ─────────────────────────────────────────────────────── +// ── 7. Source skeletons — package-info.java only under src/main/java ───────── +// Java rejects two package-info.java files for the same package, so src/test/java never gets one +// (see basic/verify.groovy for the exhaustive main-only/no-test-side assertions). def p = 'com/example/full' def pkg = p.replace('/', '.') [ "common-domain/src/main/java/${p}/common/domain/package-info.java", - "common-domain/src/test/java/${p}/common/domain/package-info.java", "common-testing/src/main/java/${p}/common/testing/package-info.java", "domain-db-users/src/main/java/${p}/domain/db/users/package-info.java", - "domain-db-users/src/test/java/${p}/domain/db/users/package-info.java", "integration-db-users/src/main/java/${p}/integration/db/users/package-info.java", - "integration-db-users/src/test/java/${p}/integration/db/users/package-info.java", "domain-graphql-catalog/src/main/java/${p}/domain/graphql/catalog/package-info.java", - "domain-graphql-catalog/src/test/java/${p}/domain/graphql/catalog/package-info.java", "integration-graphql-catalog/src/main/java/${p}/integration/graphql/catalog/package-info.java", - "integration-graphql-catalog/src/test/java/${p}/integration/graphql/catalog/package-info.java", "domain-graphql/src/main/java/${p}/domain/graphql/package-info.java", - "domain-graphql/src/test/java/${p}/domain/graphql/package-info.java", "presentation-graphql/src/main/java/${p}/presentation/graphql/package-info.java", - "presentation-graphql/src/test/java/${p}/presentation/graphql/package-info.java", "domain-rest/src/main/java/${p}/domain/rest/package-info.java", - "domain-rest/src/test/java/${p}/domain/rest/package-info.java", "presentation-rest/src/main/java/${p}/presentation/rest/package-info.java", - "presentation-rest/src/test/java/${p}/presentation/rest/package-info.java", "service-orders/src/main/java/${p}/service/orders/package-info.java", - "service-orders/src/test/java/${p}/service/orders/package-info.java", "service-notifications/src/main/java/${p}/service/notifications/package-info.java", - "service-notifications/src/test/java/${p}/service/notifications/package-info.java", "app/src/main/java/${p}/app/package-info.java", - "app/src/test/java/${p}/app/package-info.java", - "acceptance-tests/src/test/java/${p}/at/package-info.java", + "acceptance-tests/src/main/java/${p}/at/package-info.java", ].each { check(it) } assert text("common-domain/src/main/java/${p}/common/domain/package-info.java").contains("package ${pkg}.common.domain;") : 'common-domain package-info.java has wrong package declaration' @@ -131,19 +122,19 @@ assert text("common-testing/src/main/java/${p}/common/testing/package-info.java" assert text("service-orders/src/main/java/${p}/service/orders/package-info.java").contains("package ${pkg}.service.orders;") : 'service-orders package-info.java has wrong package declaration' assert text("service-notifications/src/main/java/${p}/service/notifications/package-info.java").contains("package ${pkg}.service.notifications;") : 'service-notifications package-info.java has wrong package declaration' assert text("app/src/main/java/${p}/app/package-info.java").contains("package ${pkg}.app;") : 'app package-info.java has wrong package declaration' -assert text("acceptance-tests/src/test/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' -assert new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() : 'common-testing must have test Java package-info.java' -assert new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() : 'acceptance-tests must have main Java package-info.java' +assert text("acceptance-tests/src/main/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' + +// No module gets a src/test/java/**/package-info.java — only src/main/java does (spot-checked +// exhaustively across every module in basic/verify.groovy; here just the two AT-relevant dirs). +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/package-info.java").exists() : 'acceptance-tests must not have a test-side package-info.java' +assert !new File(basedir, "common-domain/src/test/java/${p}/common/domain/package-info.java").exists() : 'common-domain must not have a test-side package-info.java' // ── #1 acceptance-tests package-info wording ───────────────────────────────── -// The test package holds the functional tests themselves (no "Tests for " prefix); the main -// package holds supporting infrastructure for those tests. -def atTestPkgInfo = text("acceptance-tests/src/test/java/${p}/at/package-info.java") -assert atTestPkgInfo.contains('Functional acceptance tests for the application.') : '#1: acceptance-tests test package-info must describe the functional tests' -assert !atTestPkgInfo.contains('Tests for') : '#1: acceptance-tests test package-info must not carry the "Tests for " prefix' +// Only src/main/java carries a package-info.java for acceptance-tests (its test package holds the +// *AT classes themselves); that lone package-info describes the functional tests, not "supporting +// infrastructure for" them. def atMainPkgInfo = text("acceptance-tests/src/main/java/${p}/at/package-info.java") -assert atMainPkgInfo.contains('Supporting classes and infrastructure for the functional acceptance tests.') \ - : '#1: acceptance-tests main package-info must describe supporting infrastructure' +assert atMainPkgInfo.contains('Functional acceptance tests for the application.') : '#1: acceptance-tests package-info must describe the functional tests' // ── 8. common-domain wiring for domain/service modules ─────────────────────── @@ -164,6 +155,25 @@ def presGraphqlPom = text('presentation-graphql/pom.xml') assert presGraphqlPom.contains('domain-graphql') : 'presentation-graphql missing domain-graphql dep' assert !presGraphqlPom.contains('domain-graphql-catalog') : 'presentation-graphql must not depend on integration domain' +// presentation-tier domain modules ("the service domain deps") depend on every domain-service* module +['domain-graphql', 'domain-rest'].each { m -> + ['domain-service-orders', 'domain-service-notifications'].each { svcDom -> + assert text("${m}/pom.xml").contains("${svcDom}") : "${m} must depend on ${svcDom} (the service domain deps)" + } +} + +// common-testing depends on every domain module. No domain module depends back on +// common-testing (only service/service-{name} does), so this stays acyclic. +def commonTestingPom = text('common-testing/pom.xml') +['domain-db-users', 'domain-graphql-catalog', 'domain-service-orders', 'domain-service-notifications', + 'domain-graphql', 'domain-rest'].each { m -> + assert commonTestingPom.contains("${m}") : "common-testing missing domain dep: ${m}" +} +['domain-service-orders', 'domain-service-notifications', 'domain-graphql', 'domain-rest', + 'domain-db-users', 'domain-graphql-catalog'].each { m -> + assert !text("${m}/pom.xml").contains('common-testing') : "${m} must not depend on common-testing (no domain module does)" +} + // ── 10. app deps ────────────────────────────────────────────────────────────── def appPom = text('app/pom.xml') @@ -186,6 +196,9 @@ def atPom = text('acceptance-tests/pom.xml') } assert !atPom.contains('service-orders') : 'acceptance-tests must not depend on service-orders' assert !atPom.contains('integration-graphql-catalog') : 'acceptance-tests must not depend on integration impl' +assert atPom.contains('common-testing') : 'acceptance-tests missing common-testing dep' +assert atPom.contains('spring-graphql-test') : 'acceptance-tests missing spring-graphql-test dep (graphql presentation present)' +assert atPom.contains('datasource-proxy-spring-boot-starter') : 'acceptance-tests missing datasource-proxy dep' // ── src directory scaffolding — all four dirs present in every module ───────── @@ -211,12 +224,17 @@ modules.findAll { it.startsWith('integration-') }.each { m -> def area = m.replaceFirst('domain-service-', '') check("${m}/pom.xml") assert text("${m}/pom.xml").contains('common-domain') : "${m} missing common-domain dep" + assert !text("${m}/pom.xml").contains('common-testing') : "${m} must not depend on common-testing (no domain module does)" + assert text("${m}/pom.xml").contains('spring-boot-starter-validation') : "${m} missing spring-boot-starter-validation dep" check("${m}/src/main/java/${p}/domain/service/${area}/package-info.java") - check("${m}/src/test/java/${p}/domain/service/${area}/package-info.java") + assert !new File(basedir, "${m}/src/test/java/${p}/domain/service/${area}/package-info.java").exists() : "${m} must not have a test-side package-info.java" assert atPom.contains("${m}") : "acceptance-tests must depend on ${m}" } assert text('service-orders/pom.xml').contains('domain-service-orders') : 'service-orders must depend on domain-service-orders' assert text('service-notifications/pom.xml').contains('domain-service-notifications') : 'service-notifications must depend on domain-service-notifications' +['service-orders', 'service-notifications'].each { m -> + assert text("${m}/pom.xml").contains('common-testing') : "${m} missing common-testing TEST dep" +} // ── Spring Boot wiring (#4) ────────────────────────────────────────────────── @@ -227,8 +245,10 @@ def appMain = text("${appConfig}/Application.java") assert appMain.contains('@SpringBootApplication') : 'Application must be @SpringBootApplication' assert appMain.contains('@EnableTransactionManagement') : 'Application must enable transaction management' assert appMain.contains('@Import({') : 'Application must @Import module configs' -assert appMain.contains('com.example.full.domain.service.orders.config.DomainServiceOrdersConfiguration.class') \ - : 'Application must @Import each module config class' +assert appMain.contains('import com.example.full.domain.service.orders.config.DomainServiceOrdersConfiguration;') \ + : 'Application must import each module config class' +assert appMain.contains(' DomainServiceOrdersConfiguration.class,') \ + : 'Application @Import block must reference config classes by simple name (shorter lines)' assert text("${appConfig}/AppServletInitializer.java").contains('extends SpringBootServletInitializer') \ : 'AppServletInitializer must extend SpringBootServletInitializer' @@ -254,7 +274,10 @@ assert cdConfig.contains("import ${p.replace('/', '.')}.common.domain.CommonDoma // parent imports the Spring Boot BOM; app pulls the starters assert parentPom.contains('spring-boot-starter-parent') : 'parent must inherit spring-boot-starter-parent' assert parentPom.contains('4.1.0') : 'parent must pin Spring Boot 4.1.0' -assert appPom.contains('spring-boot-starter-web') : 'app must depend on spring-boot-starter-web' +assert appPom.contains('spring-boot-starter') : 'app must depend on the core spring-boot-starter' +assert appPom.contains('spring-boot-starter-webmvc') : 'app must depend on spring-boot-starter-webmvc' +assert appPom.contains('spring-boot-starter-actuator') : 'app must depend on spring-boot-starter-actuator' +assert appPom.contains('spring-boot-admin-starter-client') : 'app must depend on spring-boot-admin-starter-client' assert appPom.contains('spring-boot-starter-data-jpa') : 'app must depend on spring-boot-starter-data-jpa' // ── #4–#7 parent-managed and inherited Spring dependencies ─────────────────── @@ -263,6 +286,12 @@ def dmSection = (parentPom =~ /(?s).*?<\/dependencyManagem assert dmSection.contains('spring-core') : '#4: dependencyManagement must manage spring-core' assert dmSection =~ /(?s)spring-core<\/artifactId>\s*\s*\s*commons-logging<\/groupId>/ \ : '#4: spring-core must exclude commons-logging' +// datasource-proxy-spring-boot-starter and spring-boot-admin-starter-client are third-party (not in +// the Spring Boot BOM), so each needs its own managed version, pinned via a property +assert dmSection.contains('datasource-proxy-spring-boot-starter') : 'dependencyManagement must manage datasource-proxy-spring-boot-starter' +assert dmSection.contains('spring-boot-admin-starter-client') : 'dependencyManagement must manage spring-boot-admin-starter-client' +assert parentPom.contains('') : 'parent must declare a datasource-proxy-spring-boot-starter.version property' +assert parentPom.contains('') : 'parent must declare a spring-boot-admin-starter-client.version property' // #5 parent adds spring-boot-starter-test (test scope) for every module, no commons-logging assert parentPom.contains('spring-boot-starter-test') : '#5: parent must add spring-boot-starter-test for all modules' assert parentPom =~ /(?s)spring-boot-starter-test<\/artifactId>\s*test<\/scope>/ : '#5: spring-boot-starter-test must be test-scoped' @@ -272,6 +301,8 @@ assert parentPom.contains('jspecify') // #7 common-testing declares spring-boot-starter-test at compile scope assert raw('common-testing/pom.xml') =~ /(?s)spring-boot-starter-test<\/artifactId>\s*compile<\/scope>/ \ : '#7: common-testing must declare spring-boot-starter-test at compile scope' +assert raw('common-testing/pom.xml') =~ /(?s)spring-boot-starter-graphql-test<\/artifactId>\s*compile<\/scope>/ \ + : 'common-testing must declare spring-boot-starter-graphql-test at compile scope (graphql presentation present)' // ── per-module Spring dependencies (#3): the blanket spring-context is gone; common-domain // carries the core starter (so "plain" modules inherit @Configuration support transitively) @@ -289,15 +320,15 @@ assert text('integration-db-users/pom.xml').contains('spring-boot-st : 'database integration must declare spring-boot-starter-data-jpa' assert text('integration-graphql-catalog/pom.xml').contains('spring-boot-starter-graphql') \ : 'graphql integration must declare spring-boot-starter-graphql' -// plain domain/service modules declare no Spring dependency directly (inherited via common-domain) -['domain-rest', 'domain-service-orders', 'service-orders'].each { m -> +// plain service (business-logic) modules declare no Spring dependency directly (inherited via +// common-domain); domain-rest/domain-service-orders now DO (spring-boot-starter-validation), so +// they are intentionally excluded from this list. +['service-orders', 'service-notifications'].each { m -> assert !text("${m}/pom.xml").contains('org.springframework') : "${m} must not declare a Spring dependency directly" } -// ── package-info Javadoc conventions (#2 test wording, #6 infrastructure, #7 GraphQL) ── +// ── package-info Javadoc conventions (#6 infrastructure, #7 GraphQL) ───────── -assert text("common-domain/src/test/java/${p}/common/domain/package-info.java").contains('Tests for') \ - : 'test package-info Javadoc must begin "Tests for ..."' assert text("common-testing/src/main/java/${p}/common/testing/package-info.java").contains('infrastructure') \ : 'common-testing package-info must say infrastructure, not utilities' assert !text('common-testing/pom.xml').contains('utilities') : 'common-testing pom must not say utilities' @@ -332,19 +363,95 @@ assert depPom.indexOf('') < depPom.indexOf('common-doma } // #4: domain-service-* describe the service tier (with the area name), not "the application" -assert text('domain-service-orders/pom.xml').contains('Domain classes for the orders service tier.') : '#4: domain-service-orders pom description' assert text("domain-service-orders/src/main/java/${p}/domain/service/orders/package-info.java").contains('Domain classes for the orders service tier.') : '#4: domain-service-orders package-info' // #5: acceptance-tests and common-testing are managed in dependencyManagement at test scope ['acceptance-tests', 'common-testing'].each { m -> assert dmBlock =~ /(?s)${aid}-${m}<\/artifactId>\s*[^<]+<\/version>\s*test<\/scope>/ : "#5: ${m} must be managed at test" } +// acceptance-tests must never appear as a of any other module +modules.findAll { it != 'acceptance-tests' }.each { m -> + assert !text("${m}/pom.xml").contains('acceptance-tests') : "${m} must not depend on acceptance-tests" +} // #7: GraphQL presentation uses resolver wording, not request-handling -assert text('presentation-graphql/pom.xml').contains('Resolver classes for the GraphQL presentation tier.') : '#7: presentation-graphql pom description' assert text("presentation-graphql/src/main/java/${p}/presentation/graphql/package-info.java").contains('Resolver classes for the GraphQL presentation tier.') : '#7: presentation-graphql main package-info' -assert text("presentation-graphql/src/test/java/${p}/presentation/graphql/package-info.java").contains('resolver classes for the GraphQL presentation tier.') : '#7: presentation-graphql test package-info' assert !text('presentation-graphql/pom.xml').contains('Request-handling') : '#7: presentation-graphql must not use request-handling wording' +// ── appName-derived pom name/description ───────────────────────────────────── + +assert parentPom.contains("${appName} Parent") : 'parent must be " Parent"' +assert parentPom.contains("${appName}'s parent POM.") : "parent must be \"'s parent POM.\"" +assert text('app/pom.xml') != null && raw('app/pom.xml').contains("${appName} App") : 'app must be " App"' +assert raw('app/pom.xml').contains("${appName}'s application configuration including building its deployment assembly.") \ + : 'app must use the appName-derived sentence' +assert raw('acceptance-tests/pom.xml').contains("${appName} Acceptance Tests") : 'acceptance-tests wrong' +assert raw('acceptance-tests/pom.xml').contains("${appName}'s acceptance tests.") : 'acceptance-tests wrong' +assert raw('common-testing/pom.xml').contains("${appName} Common Testing") : 'common-testing wrong' +assert raw('common-testing/pom.xml').contains("${appName}'s common testing.") : 'common-testing wrong' +assert raw('domain-graphql/pom.xml').contains("${appName} Domain GraphQL") : 'domain-graphql wrong' +assert raw('domain-graphql/pom.xml').contains("${appName}'s GraphQL presentation tier domain classes.") : 'domain-graphql wrong' +assert raw('domain-rest/pom.xml').contains("${appName} Domain REST") : 'domain-rest wrong' +assert raw('domain-rest/pom.xml').contains("${appName}'s REST presentation tier domain classes.") : 'domain-rest wrong' +assert raw('presentation-graphql/pom.xml').contains("${appName} Presentation GraphQL") : 'presentation-graphql wrong' +assert raw('presentation-graphql/pom.xml').contains("${appName}'s GraphQL presentation tier.") : 'presentation-graphql wrong' +assert raw('domain-service-orders/pom.xml').contains("${appName} Domain Service Orders") : 'domain-service-orders wrong' +assert raw('domain-service-orders/pom.xml').contains("${appName}'s orders service tier domain classes.") : 'domain-service-orders wrong' +assert raw('service-orders/pom.xml').contains("${appName} Service Orders") : 'service-orders wrong' +assert raw('service-orders/pom.xml').contains("${appName}'s orders service tier (business logic).") : 'service-orders wrong' +// presentation-rest / common-domain / per-integration modules keep their prior (non-appName) description +assert !raw('presentation-rest/pom.xml').contains('') : 'presentation-rest has no dedicated appName requirement' +assert !raw('common-domain/pom.xml').contains('') : 'common-domain has no dedicated appName requirement' + +// spring.application.name uses the appName "property" form: spaces removed, lowercased +def appProps = text('app/src/main/resources/application.properties') +assert appProps.contains('spring.application.name=fulltest') : 'spring.application.name must be appName with spaces removed and lowercased' + +// ── acceptance-test infrastructure: AT base classes, AT config, logback ────── + +def atConfigClass = 'FullTestAcceptanceTestConfiguration' +check("acceptance-tests/src/test/java/${p}/at/config/${atConfigClass}.java") +def atConfig = text("acceptance-tests/src/test/java/${p}/at/config/${atConfigClass}.java") +assert atConfig.contains('@Configuration') : 'AT configuration class must be @Configuration' +assert atConfig.contains("Configuration class for ${appName} ATs.") : 'AT configuration class Javadoc must name the app' + +check('acceptance-tests/src/test/resources/logback-spring.xml') +def logbackSpring = text('acceptance-tests/src/test/resources/logback-spring.xml') +assert logbackSpring.contains('') \ + : 'logback-spring.xml must include the Boot base logback config' + +check("acceptance-tests/src/test/java/${p}/at/AppRestAcceptanceTestBase.java") +def appRestAt = text("acceptance-tests/src/test/java/${p}/at/AppRestAcceptanceTestBase.java") +assert appRestAt.contains("Base test class for all ${appName} REST acceptance tests.") : 'AppRestAcceptanceTestBase Javadoc must name the app' +assert appRestAt.contains('extends RestAcceptanceTestBase') : 'AppRestAcceptanceTestBase must extend RestAcceptanceTestBase' +assert appRestAt.contains("@ContextConfiguration(classes = { ${atConfigClass}.class,") : 'AppRestAcceptanceTestBase must @ContextConfiguration with the AT config class first' +assert appRestAt.contains('DomainServiceOrdersConfiguration.class') : 'AppRestAcceptanceTestBase @ContextConfiguration must include every generated module config (reachable transitively via app)' +assert appRestAt.contains('@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class, XADataSourceAutoConfiguration.class })') \ + : 'AppRestAcceptanceTestBase must exclude DataSourceAutoConfiguration/XADataSourceAutoConfiguration (database integration present)' + +check("acceptance-tests/src/test/java/${p}/at/AppGraphQlAcceptanceTestBase.java") +def appGraphQlAt = text("acceptance-tests/src/test/java/${p}/at/AppGraphQlAcceptanceTestBase.java") +assert appGraphQlAt.contains("Base test class for all ${appName} GraphQL acceptance tests.") : 'AppGraphQlAcceptanceTestBase Javadoc must name the app' +assert appGraphQlAt.contains('extends GraphQlAcceptanceTestBase') : 'AppGraphQlAcceptanceTestBase must extend GraphQlAcceptanceTestBase' + +// RestAcceptanceTestBase / GraphQlAcceptanceTestBase: shared base classes in common-testing +check("common-testing/src/main/java/${p}/common/testing/RestAcceptanceTestBase.java") +def restAtBase = text("common-testing/src/main/java/${p}/common/testing/RestAcceptanceTestBase.java") +assert restAtBase.contains('@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)') : 'RestAcceptanceTestBase must be a random-port @SpringBootTest' + +check("common-testing/src/main/java/${p}/common/testing/GraphQlAcceptanceTestBase.java") +def graphQlAtBase = text("common-testing/src/main/java/${p}/common/testing/GraphQlAcceptanceTestBase.java") +assert graphQlAtBase.contains('@AutoConfigureHttpGraphQlTester') : 'GraphQlAcceptanceTestBase must @AutoConfigureHttpGraphQlTester' + +// ── app: application.properties + per-environment profile files ───────────── + +['ci', 'dev', 'local', 'prod', 'qa', 'uat'].each { profile -> + def f = new File(basedir, "app/src/main/resources/application-${profile}.properties") + assert f.exists() : "Missing profile properties file: application-${profile}.properties" + assert f.text.isEmpty() : "application-${profile}.properties must be empty" +} +assert appProps.contains('decorator.datasource.enabled=true') : 'application.properties must configure SQL logging' +assert appProps.contains('management.endpoints.web.exposure.include=*') : 'application.properties must expose all actuator endpoints' + _buildLog.append("\n=== PASSED ===\n") true diff --git a/src/test/resources/projects/malformed-integrations/archetype.properties b/src/test/resources/projects/malformed-integrations/archetype.properties index 229f24d..9a72943 100644 --- a/src/test/resources/projects/malformed-integrations/archetype.properties +++ b/src/test/resources/projects/malformed-integrations/archetype.properties @@ -2,6 +2,7 @@ groupId=com.example artifactId=malformed-test version=1.0.0-SNAPSHOT package=com.example.malformed +appName=Malformed Test integrations=database:main,,nocolon,rest:orders serviceAreas=, presentationTypes=rest diff --git a/src/test/resources/projects/malformed-integrations/verify.groovy b/src/test/resources/projects/malformed-integrations/verify.groovy index 58a351f..ebe493f 100644 --- a/src/test/resources/projects/malformed-integrations/verify.groovy +++ b/src/test/resources/projects/malformed-integrations/verify.groovy @@ -1,5 +1,5 @@ // ── Test: malformed-integrations ───────────────────────────────────────────── -// integrations=database:main,,nocolon,rest:orders serviceAreas=, presentationTypes=rest +// appName=Malformed Test integrations=database:main,,nocolon,rest:orders serviceAreas=, presentationTypes=rest // Covers: double-comma (empty entry) and no-colon entry are both silently skipped; // valid surrounding entries are still processed correctly @@ -20,6 +20,7 @@ def check = { String rel -> // tags; the prefix is verified explicitly via raw() (module's own artifactId + parent DM), and the // generated project's real Maven build — run before this script — fails on any inconsistent reference. def aid = 'malformed-test' +def appName = 'Malformed Test' def raw = { String rel -> new File(basedir, rel).text } @@ -61,6 +62,8 @@ modules.each { m -> } assert !parentPom.contains('../domain-nocolon') : "Skipped entry must not appear in parent " assert parentPom.contains('pom') : "parent must have pom packaging" +assert parentPom.contains("${appName} Parent") : 'parent must be " Parent"' +assert parentPom.contains("${appName}'s parent POM.") : "parent must be \"'s parent POM.\"" def moduleOrder = modules.sort().collect { "../${it}" } def moduleBlock = parentPom.replaceAll(/(?s).*(.*?)<\/modules>.*/, '$1') @@ -84,48 +87,42 @@ modules.each { m -> : "${m}/pom.xml must declare its own artifactId as '${aid}-${m}'" } -// ── 6. Source skeletons ─────────────────────────────────────────────────────── +// ── 6. Source skeletons — package-info.java only under src/main/java ──────── def p = 'com/example/malformed' def pkg = p.replace('/', '.') [ "common-domain/src/main/java/${p}/common/domain/package-info.java", - "common-domain/src/test/java/${p}/common/domain/package-info.java", "common-testing/src/main/java/${p}/common/testing/package-info.java", "domain-db-main/src/main/java/${p}/domain/db/main/package-info.java", - "domain-db-main/src/test/java/${p}/domain/db/main/package-info.java", "integration-db-main/src/main/java/${p}/integration/db/main/package-info.java", - "integration-db-main/src/test/java/${p}/integration/db/main/package-info.java", "domain-rest-orders/src/main/java/${p}/domain/rest/orders/package-info.java", - "domain-rest-orders/src/test/java/${p}/domain/rest/orders/package-info.java", "integration-rest-orders/src/main/java/${p}/integration/rest/orders/package-info.java", - "integration-rest-orders/src/test/java/${p}/integration/rest/orders/package-info.java", "domain-rest/src/main/java/${p}/domain/rest/package-info.java", - "domain-rest/src/test/java/${p}/domain/rest/package-info.java", "presentation-rest/src/main/java/${p}/presentation/rest/package-info.java", - "presentation-rest/src/test/java/${p}/presentation/rest/package-info.java", "service/src/main/java/${p}/service/package-info.java", - "service/src/test/java/${p}/service/package-info.java", "app/src/main/java/${p}/app/package-info.java", - "app/src/test/java/${p}/app/package-info.java", - "acceptance-tests/src/test/java/${p}/at/package-info.java", + "acceptance-tests/src/main/java/${p}/at/package-info.java", ].each { check(it) } assert text("common-domain/src/main/java/${p}/common/domain/package-info.java").contains("package ${pkg}.common.domain;") : 'common-domain package-info.java has wrong package declaration' assert text("common-testing/src/main/java/${p}/common/testing/package-info.java").contains("package ${pkg}.common.testing;") : 'common-testing package-info.java has wrong package declaration' assert text("domain-rest/src/main/java/${p}/domain/rest/package-info.java").contains("package ${pkg}.domain.rest;") : 'domain-rest package-info.java has wrong package declaration' -assert text("service/src/main/java/${p}/service/package-info.java").contains("package ${pkg}.service;") : 'service package-info.java has wrong package declaration' -assert text("app/src/main/java/${p}/app/package-info.java").contains("package ${pkg}.app;") : 'app package-info.java has wrong package declaration' -assert text("acceptance-tests/src/test/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' -assert new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() : 'common-testing must have test Java package-info.java' -assert new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() : 'acceptance-tests must have main Java package-info.java' +assert text("service/src/main/java/${p}/service/package-info.java").contains("package ${pkg}.service;") : 'service package-info.java has wrong package declaration' +assert text("app/src/main/java/${p}/app/package-info.java").contains("package ${pkg}.app;") : 'app package-info.java has wrong package declaration' +assert text("acceptance-tests/src/main/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/package-info.java").exists() : 'acceptance-tests must not have a test-side package-info.java' // ── 7. Dependency wiring ───────────────────────────────────────────────────── assert text('domain-db-main/pom.xml').contains('common-domain') : 'domain-db-main missing common-domain dep' assert text('domain-rest-orders/pom.xml').contains('common-domain') : 'domain-rest-orders missing common-domain dep' assert text('integration-db-main/pom.xml').contains('domain-db-main') : 'integration-db-main missing domain dep' +assert text('integration-db-main/pom.xml').contains('datasource-proxy-spring-boot-starter') \ + : 'integration-db-main (a database integration) missing datasource-proxy dep' assert text('integration-rest-orders/pom.xml').contains('domain-rest-orders') : 'integration-rest-orders missing domain dep' +assert !text('integration-rest-orders/pom.xml').contains('datasource-proxy-spring-boot-starter') \ + : 'integration-rest-orders (not a database integration) must not get the datasource-proxy dep' def appPom = text('app/pom.xml') ['common-domain', 'domain-db-main', 'domain-rest', 'domain-rest-orders', @@ -133,6 +130,11 @@ def appPom = text('app/pom.xml') assert appPom.contains("${dep}") : "app missing dep: $dep" } assert !appPom.contains('domain-nocolon') : 'app must not reference skipped entry' +assert appPom.contains('spring-boot-starter-data-jpa') : 'app must depend on spring-boot-starter-data-jpa (database integration present)' + +def atPom = text('acceptance-tests/pom.xml') +assert atPom.contains('common-testing') : 'acceptance-tests missing common-testing TEST dep' +assert atPom.contains('datasource-proxy-spring-boot-starter') : 'acceptance-tests missing datasource-proxy dep' // ── src directory scaffolding — all four dirs present in every module ───────── diff --git a/src/test/resources/projects/minimal/archetype.properties b/src/test/resources/projects/minimal/archetype.properties index e14215b..9b9283a 100644 --- a/src/test/resources/projects/minimal/archetype.properties +++ b/src/test/resources/projects/minimal/archetype.properties @@ -2,6 +2,7 @@ groupId=com.example artifactId=minimal-test version=1.0.0-SNAPSHOT package=com.example.minimal +appName=Minimal Test integrations=, serviceAreas=, presentationTypes=, diff --git a/src/test/resources/projects/minimal/verify.groovy b/src/test/resources/projects/minimal/verify.groovy index 2440853..1cafa39 100644 --- a/src/test/resources/projects/minimal/verify.groovy +++ b/src/test/resources/projects/minimal/verify.groovy @@ -1,5 +1,5 @@ // ── Test: minimal ───────────────────────────────────────────────────────────── -// integrations= serviceAreas=, presentationTypes= (integrations and presentation empty) +// appName=Minimal Test integrations= serviceAreas=, presentationTypes= (integrations and presentation empty) // Covers: absolute minimal project — no integration modules, no presentation // modules; atDeps collapses to [app, common-domain]; appDeps to // [common-domain, service] @@ -21,6 +21,7 @@ def check = { String rel -> // tags; the prefix is verified explicitly via raw() (module's own artifactId + parent DM), and the // generated project's real Maven build — run before this script — fails on any inconsistent reference. def aid = 'minimal-test' +def appName = 'Minimal Test' def raw = { String rel -> new File(basedir, rel).text } @@ -54,6 +55,8 @@ modules.each { m -> assert parentPom.contains("${aid}-${m}") : "parent missing: $m" } assert parentPom.contains('pom') : "parent must have pom packaging" +assert parentPom.contains("${appName} Parent") : 'parent must be " Parent"' +assert parentPom.contains("${appName}'s parent POM.") : "parent must be \"'s parent POM.\"" def moduleOrder = modules.sort().collect { "../${it}" } def moduleBlock = parentPom.replaceAll(/(?s).*(.*?)<\/modules>.*/, '$1') @@ -77,33 +80,32 @@ modules.each { m -> : "${m}/pom.xml must declare its own artifactId as '${aid}-${m}'" } -// ── 5. Source skeletons ─────────────────────────────────────────────────────── +// ── 5. Source skeletons — package-info.java only under src/main/java ──────── def p = 'com/example/minimal' def pkg = p.replace('/', '.') [ "common-domain/src/main/java/${p}/common/domain/package-info.java", - "common-domain/src/test/java/${p}/common/domain/package-info.java", "common-testing/src/main/java/${p}/common/testing/package-info.java", "service/src/main/java/${p}/service/package-info.java", - "service/src/test/java/${p}/service/package-info.java", "app/src/main/java/${p}/app/package-info.java", - "app/src/test/java/${p}/app/package-info.java", - "acceptance-tests/src/test/java/${p}/at/package-info.java", + "acceptance-tests/src/main/java/${p}/at/package-info.java", ].each { check(it) } assert text("common-domain/src/main/java/${p}/common/domain/package-info.java").contains("package ${pkg}.common.domain;") : 'common-domain package-info.java has wrong package declaration' assert text("common-testing/src/main/java/${p}/common/testing/package-info.java").contains("package ${pkg}.common.testing;") : 'common-testing package-info.java has wrong package declaration' assert text("service/src/main/java/${p}/service/package-info.java").contains("package ${pkg}.service;") : 'service package-info.java has wrong package declaration' assert text("app/src/main/java/${p}/app/package-info.java").contains("package ${pkg}.app;") : 'app package-info.java has wrong package declaration' -assert text("acceptance-tests/src/test/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' -assert new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() : 'common-testing must have test Java package-info.java' -assert new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() : 'acceptance-tests must have main Java package-info.java' +assert text("acceptance-tests/src/main/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/package-info.java").exists() : 'acceptance-tests must not have a test-side package-info.java' +assert !new File(basedir, "common-domain/src/test/java/${p}/common/domain/package-info.java").exists() : 'common-domain must not have a test-side package-info.java' // ── 6. Dependency wiring ───────────────────────────────────────────────────── assert text('service/pom.xml').contains('common-domain') : 'service missing common-domain dep' +assert text('service/pom.xml').contains('common-testing') : 'service missing common-testing TEST dep' assert text('common-testing/pom.xml').contains('common-domain') : 'common-testing missing common-domain dep' +assert text('common-testing/pom.xml').contains('domain-service') : 'common-testing missing domain-service dep' // app depends only on common-domain and service def appPom = text('app/pom.xml') @@ -116,14 +118,24 @@ assert !appPom.contains('domain-db-main') : 'app mus assert !appPom.contains('acceptance-tests') : 'app must not depend on acceptance-tests' assert !appPom.contains('common-testing') : 'app must not depend on common-testing' assert appPom.count('app') == 1 : 'app must not depend on itself' +// no database integration: no spring-boot-starter-data-jpa; the web/actuator/admin stack is +// unconditional regardless of presentationTypes/integrations +assert !appPom.contains('spring-boot-starter-data-jpa') : 'no database integration: app must not declare spring-boot-starter-data-jpa' +assert appPom.contains('spring-boot-starter') : 'app must depend on the core spring-boot-starter' +assert appPom.contains('spring-boot-starter-webmvc') : 'app must depend on spring-boot-starter-webmvc regardless of presentationTypes' +assert appPom.contains('spring-boot-starter-actuator') : 'app must depend on spring-boot-starter-actuator' +assert appPom.contains('spring-boot-admin-starter-client'): 'app must depend on spring-boot-admin-starter-client' -// acceptance-tests depends only on app + common-domain (no domain modules exist) +// acceptance-tests depends only on app + common-domain (no domain modules exist), + common-testing def atPom = text('acceptance-tests/pom.xml') ['app', 'common-domain'].each { dep -> assert atPom.contains("${dep}") : "acceptance-tests missing dep: $dep" } assert !atPom.contains('service') : 'acceptance-tests must not depend on service' assert !atPom.contains('domain-rest') : 'acceptance-tests must not reference non-existent domain module' +assert atPom.contains('common-testing') : 'acceptance-tests missing common-testing TEST dep' +// no database integration: datasource-proxy (sql logging) must not be added +assert !atPom.contains('datasource-proxy-spring-boot-starter') : 'acceptance-tests must not get the datasource-proxy dep' // ── src directory scaffolding — all four dirs present in every module ───────── @@ -147,23 +159,43 @@ modules.findAll { it.startsWith('integration-') }.each { m -> check('domain-service/pom.xml') assert text('domain-service/pom.xml').contains('common-domain') : 'domain-service missing common-domain dep' +assert !text('domain-service/pom.xml').contains('common-testing') : 'domain-service must not depend on common-testing (no domain module does)' +assert text('domain-service/pom.xml').contains('spring-boot-starter-validation') : 'domain-service missing spring-boot-starter-validation dep' assert text('service/pom.xml').contains('domain-service') : 'service must depend on domain-service' check("domain-service/src/main/java/${p}/domain/service/package-info.java") -check("domain-service/src/test/java/${p}/domain/service/package-info.java") +assert !new File(basedir, "domain-service/src/test/java/${p}/domain/service/package-info.java").exists() \ + : 'domain-service must not have a test-side package-info.java' check("app/src/main/java/${p}/app/config/Application.java") def appMain = text("app/src/main/java/${p}/app/config/Application.java") assert appMain.contains('@SpringBootApplication') : 'Application must be @SpringBootApplication' -assert appMain.contains("${pkg}.domain.service.config.DomainServiceConfiguration.class") : 'Application must @Import DomainServiceConfiguration' +assert appMain.contains('import com.example.minimal.domain.service.config.DomainServiceConfiguration;') \ + : 'Application must import DomainServiceConfiguration' +assert appMain.contains(' DomainServiceConfiguration.class,') : 'Application must @Import DomainServiceConfiguration by simple name' assert parentPom.contains('spring-boot-starter-parent') : 'parent must inherit spring-boot-starter-parent' -// minimal has neither a 'rest' presentation nor a 'database' integration, so the app declares -// no role starter, omits @EnableTransactionManagement, and generates no AppServletInitializer -assert !new File(basedir, "app/src/main/java/${p}/app/config/AppServletInitializer.java").exists() \ - : 'no rest presentation: AppServletInitializer must not be generated' -assert !appMain.contains('@EnableTransactionManagement') : 'no database integration: Application must not @EnableTransactionManagement' -assert !appPom.contains('spring-boot-starter-web') : 'no rest presentation: app must not declare spring-boot-starter-web' -assert !appPom.contains('spring-boot-starter-data-jpa') : 'no database integration: app must not declare spring-boot-starter-data-jpa' -assert text("common-testing/src/test/java/${p}/common/testing/package-info.java").contains('Tests for') \ - : 'test package-info Javadoc must begin "Tests for ..."' +// minimal has neither a 'rest' presentation nor a 'database' integration, so Application omits +// @EnableTransactionManagement, but AppServletInitializer is still generated (app is always a +// servlet web app now, regardless of presentationTypes) +check("app/src/main/java/${p}/app/config/AppServletInitializer.java") +assert !appMain.contains('@EnableTransactionManagement') : 'no database integration: Application must not @EnableTransactionManagement' + +// ── acceptance-test infrastructure exists (includeSpring=true) even with no presentation types ── + +def atConfigClass = 'MinimalTestAcceptanceTestConfiguration' +check("acceptance-tests/src/test/java/${p}/at/config/${atConfigClass}.java") +assert text("acceptance-tests/src/test/java/${p}/at/config/${atConfigClass}.java").contains("Configuration class for ${appName} ATs.") \ + : 'AT configuration class Javadoc must name the app' +check('acceptance-tests/src/test/resources/logback-spring.xml') +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/AppRestAcceptanceTestBase.java").exists() \ + : 'AppRestAcceptanceTestBase must not be generated (no rest presentation)' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/AppGraphQlAcceptanceTestBase.java").exists() \ + : 'AppGraphQlAcceptanceTestBase must not be generated (no graphql presentation)' +assert !new File(basedir, "common-testing/src/main/java/${p}/common/testing/RestAcceptanceTestBase.java").exists() \ + : 'RestAcceptanceTestBase must not be generated (no rest presentation)' +assert !new File(basedir, "common-testing/src/main/java/${p}/common/testing/GraphQlAcceptanceTestBase.java").exists() \ + : 'GraphQlAcceptanceTestBase must not be generated (no graphql presentation)' + +assert text('app/src/main/resources/application.properties').contains('spring.application.name=minimaltest') \ + : 'spring.application.name must be appName with spaces removed and lowercased' _buildLog.append("\n=== PASSED ===\n") true diff --git a/src/test/resources/projects/multi-integration/archetype.properties b/src/test/resources/projects/multi-integration/archetype.properties index 9f5f7bb..a9137dd 100644 --- a/src/test/resources/projects/multi-integration/archetype.properties +++ b/src/test/resources/projects/multi-integration/archetype.properties @@ -2,6 +2,7 @@ groupId=com.example artifactId=multi-integration-test version=1.0.0-SNAPSHOT package=com.example.multiint +appName=Multi Integration Test integrations=database:users,rest:orders,jms:events serviceAreas=, presentationTypes=rest diff --git a/src/test/resources/projects/multi-integration/verify.groovy b/src/test/resources/projects/multi-integration/verify.groovy index 6deba96..1730a56 100644 --- a/src/test/resources/projects/multi-integration/verify.groovy +++ b/src/test/resources/projects/multi-integration/verify.groovy @@ -1,5 +1,6 @@ // ── Test: multi-integration ─────────────────────────────────────────────────── -// integrations=database:users,rest:orders,jms:events serviceAreas=, presentationTypes=rest +// appName=Multi Integration Test integrations=database:users,rest:orders,jms:events +// serviceAreas=, presentationTypes=rest // Covers: multiple integration types, db abbreviation, non-db types as-is, // integration domain-rest-orders vs presentation domain-rest (no collision) @@ -20,6 +21,7 @@ def check = { String rel -> // tags; the prefix is verified explicitly via raw() (module's own artifactId + parent DM), and the // generated project's real Maven build — run before this script — fails on any inconsistent reference. def aid = 'multi-integration-test' +def appName = 'Multi Integration Test' def raw = { String rel -> new File(basedir, rel).text } @@ -67,6 +69,8 @@ modules.each { m -> assert parentPom.contains("${aid}-${m}") : "parent missing: $m" } assert parentPom.contains('pom') : "parent must have pom packaging" +assert parentPom.contains("${appName} Parent") : 'parent must be " Parent"' +assert parentPom.contains("${appName}'s parent POM.") : "parent must be \"'s parent POM.\"" def moduleOrder = modules.sort().collect { "../${it}" } def moduleBlock = parentPom.replaceAll(/(?s).*(.*?)<\/modules>.*/, '$1') @@ -90,35 +94,24 @@ modules.each { m -> : "${m}/pom.xml must declare its own artifactId as '${aid}-${m}'" } -// ── 7. Source skeletons for each integration type ──────────────────────────── +// ── 7. Source skeletons for each integration type — package-info.java only under src/main/java ── def p = 'com/example/multiint' def pkg = p.replace('/', '.') [ "common-domain/src/main/java/${p}/common/domain/package-info.java", - "common-domain/src/test/java/${p}/common/domain/package-info.java", "common-testing/src/main/java/${p}/common/testing/package-info.java", "domain-db-users/src/main/java/${p}/domain/db/users/package-info.java", - "domain-db-users/src/test/java/${p}/domain/db/users/package-info.java", "integration-db-users/src/main/java/${p}/integration/db/users/package-info.java", - "integration-db-users/src/test/java/${p}/integration/db/users/package-info.java", "domain-rest-orders/src/main/java/${p}/domain/rest/orders/package-info.java", - "domain-rest-orders/src/test/java/${p}/domain/rest/orders/package-info.java", "integration-rest-orders/src/main/java/${p}/integration/rest/orders/package-info.java", - "integration-rest-orders/src/test/java/${p}/integration/rest/orders/package-info.java", "domain-jms-events/src/main/java/${p}/domain/jms/events/package-info.java", - "domain-jms-events/src/test/java/${p}/domain/jms/events/package-info.java", "integration-jms-events/src/main/java/${p}/integration/jms/events/package-info.java", - "integration-jms-events/src/test/java/${p}/integration/jms/events/package-info.java", "domain-rest/src/main/java/${p}/domain/rest/package-info.java", - "domain-rest/src/test/java/${p}/domain/rest/package-info.java", "presentation-rest/src/main/java/${p}/presentation/rest/package-info.java", - "presentation-rest/src/test/java/${p}/presentation/rest/package-info.java", "service/src/main/java/${p}/service/package-info.java", - "service/src/test/java/${p}/service/package-info.java", "app/src/main/java/${p}/app/package-info.java", - "app/src/test/java/${p}/app/package-info.java", - "acceptance-tests/src/test/java/${p}/at/package-info.java", + "acceptance-tests/src/main/java/${p}/at/package-info.java", ].each { check(it) } assert text("common-domain/src/main/java/${p}/common/domain/package-info.java").contains("package ${pkg}.common.domain;") : 'common-domain package-info.java has wrong package declaration' @@ -126,9 +119,8 @@ assert text("common-testing/src/main/java/${p}/common/testing/package-info.java" assert text("domain-db-users/src/main/java/${p}/domain/db/users/package-info.java").contains("package ${pkg}.domain.db.users;") : 'domain-db-users package-info.java has wrong package declaration' assert text("service/src/main/java/${p}/service/package-info.java").contains("package ${pkg}.service;") : 'service package-info.java has wrong package declaration' assert text("app/src/main/java/${p}/app/package-info.java").contains("package ${pkg}.app;") : 'app package-info.java has wrong package declaration' -assert text("acceptance-tests/src/test/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' -assert new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() : 'common-testing must have test Java package-info.java' -assert new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() : 'acceptance-tests must have main Java package-info.java' +assert text("acceptance-tests/src/main/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/package-info.java").exists() : 'acceptance-tests must not have a test-side package-info.java' // ── 8. Integration module dependencies ─────────────────────────────────────── @@ -140,8 +132,14 @@ assert text('domain-db-users/pom.xml').contains('common-domaincommon-domain') : 'domain-rest-orders missing common-domain dep' assert text('domain-jms-events/pom.xml').contains('common-domain') : 'domain-jms-events missing common-domain dep' assert text('domain-rest/pom.xml').contains('common-domain') : 'domain-rest (presentation) missing common-domain dep' +assert text('domain-rest/pom.xml').contains('domain-service') : 'domain-rest (presentation) missing domain-service dep (the service domain deps)' assert text('service/pom.xml').contains('common-domain') : 'service missing common-domain dep' +// only the database integration gets the SQL logging dependency +assert text('integration-db-users/pom.xml').contains('datasource-proxy-spring-boot-starter') : 'integration-db-users missing datasource-proxy dep' +assert !text('integration-rest-orders/pom.xml').contains('datasource-proxy-spring-boot-starter') : 'integration-rest-orders must not get the datasource-proxy dep' +assert !text('integration-jms-events/pom.xml').contains('datasource-proxy-spring-boot-starter') : 'integration-jms-events must not get the datasource-proxy dep' + // rest integration is a client (spring-boot-starter-restclient), not the servlet web server; // the rest presentation is the server (spring-boot-starter-web) def restIntegPom = text('integration-rest-orders/pom.xml') @@ -160,6 +158,7 @@ def appPom = text('app/pom.xml') } assert !appPom.contains('acceptance-tests') : 'app must not depend on acceptance-tests' assert !appPom.contains('common-testing') : 'app must not depend on common-testing' +assert appPom.contains('spring-boot-starter-data-jpa') : 'app must depend on spring-boot-starter-data-jpa (database integration present)' def atPom = text('acceptance-tests/pom.xml') ['app', 'common-domain', 'domain-db-users', 'domain-jms-events', @@ -168,6 +167,8 @@ def atPom = text('acceptance-tests/pom.xml') } assert !atPom.contains('integration-db-users') : 'acceptance-tests must not include integration-db-users' assert !atPom.contains('integration-rest-orders') : 'acceptance-tests must not include integration-rest-orders' +assert atPom.contains('common-testing') : 'acceptance-tests missing common-testing TEST dep' +assert atPom.contains('datasource-proxy-spring-boot-starter') : 'acceptance-tests missing datasource-proxy dep' // ── src directory scaffolding — all four dirs present in every module ───────── diff --git a/src/test/resources/projects/multi-service/archetype.properties b/src/test/resources/projects/multi-service/archetype.properties index 71251ad..f899198 100644 --- a/src/test/resources/projects/multi-service/archetype.properties +++ b/src/test/resources/projects/multi-service/archetype.properties @@ -2,6 +2,7 @@ groupId=com.example artifactId=multi-service-test version=1.0.0-SNAPSHOT package=com.example.multisvc +appName=Multi Service Test integrations=database:main serviceAreas=orders,inventory presentationTypes=rest diff --git a/src/test/resources/projects/multi-service/verify.groovy b/src/test/resources/projects/multi-service/verify.groovy index 500252d..b9e9157 100644 --- a/src/test/resources/projects/multi-service/verify.groovy +++ b/src/test/resources/projects/multi-service/verify.groovy @@ -1,5 +1,6 @@ // ── Test: multi-service ─────────────────────────────────────────────────────── -// integrations=database:main serviceAreas=orders,inventory presentationTypes=rest +// appName=Multi Service Test integrations=database:main serviceAreas=orders,inventory +// presentationTypes=rest // Covers: named serviceAreas path, multiple named service modules, // no plain "service" module when names are provided @@ -20,6 +21,7 @@ def check = { String rel -> // tags; the prefix is verified explicitly via raw() (module's own artifactId + parent DM), and the // generated project's real Maven build — run before this script — fails on any inconsistent reference. def aid = 'multi-service-test' +def appName = 'Multi Service Test' def raw = { String rel -> new File(basedir, rel).text } @@ -59,6 +61,8 @@ modules.each { m -> assert parentPom.contains("${aid}-${m}") : "parent missing: $m" } assert parentPom.contains('pom') : "parent must have pom packaging" +assert parentPom.contains("${appName} Parent") : 'parent must be " Parent"' +assert parentPom.contains("${appName}'s parent POM.") : "parent must be \"'s parent POM.\"" def moduleOrder = modules.sort().collect { "../${it}" } def moduleBlock = parentPom.replaceAll(/(?s).*(.*?)<\/modules>.*/, '$1') @@ -82,29 +86,21 @@ modules.each { m -> : "${m}/pom.xml must declare its own artifactId as '${aid}-${m}'" } -// ── 6. Source skeletons ─────────────────────────────────────────────────────── +// ── 6. Source skeletons — package-info.java only under src/main/java ──────── def p = 'com/example/multisvc' def pkg = p.replace('/', '.') [ "common-domain/src/main/java/${p}/common/domain/package-info.java", - "common-domain/src/test/java/${p}/common/domain/package-info.java", "common-testing/src/main/java/${p}/common/testing/package-info.java", "domain-db-main/src/main/java/${p}/domain/db/main/package-info.java", - "domain-db-main/src/test/java/${p}/domain/db/main/package-info.java", "integration-db-main/src/main/java/${p}/integration/db/main/package-info.java", - "integration-db-main/src/test/java/${p}/integration/db/main/package-info.java", "domain-rest/src/main/java/${p}/domain/rest/package-info.java", - "domain-rest/src/test/java/${p}/domain/rest/package-info.java", "presentation-rest/src/main/java/${p}/presentation/rest/package-info.java", - "presentation-rest/src/test/java/${p}/presentation/rest/package-info.java", "service-orders/src/main/java/${p}/service/orders/package-info.java", - "service-orders/src/test/java/${p}/service/orders/package-info.java", "service-inventory/src/main/java/${p}/service/inventory/package-info.java", - "service-inventory/src/test/java/${p}/service/inventory/package-info.java", "app/src/main/java/${p}/app/package-info.java", - "app/src/test/java/${p}/app/package-info.java", - "acceptance-tests/src/test/java/${p}/at/package-info.java", + "acceptance-tests/src/main/java/${p}/at/package-info.java", ].each { check(it) } assert text("common-domain/src/main/java/${p}/common/domain/package-info.java").contains("package ${pkg}.common.domain;") : 'common-domain package-info.java has wrong package declaration' @@ -112,9 +108,8 @@ assert text("common-testing/src/main/java/${p}/common/testing/package-info.java" assert text("service-inventory/src/main/java/${p}/service/inventory/package-info.java").contains("package ${pkg}.service.inventory;") : 'service-inventory package-info.java has wrong package declaration' assert text("service-orders/src/main/java/${p}/service/orders/package-info.java").contains("package ${pkg}.service.orders;") : 'service-orders package-info.java has wrong package declaration' assert text("app/src/main/java/${p}/app/package-info.java").contains("package ${pkg}.app;") : 'app package-info.java has wrong package declaration' -assert text("acceptance-tests/src/test/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' -assert new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() : 'common-testing must have test Java package-info.java' -assert new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() : 'acceptance-tests must have main Java package-info.java' +assert text("acceptance-tests/src/main/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/package-info.java").exists() : 'acceptance-tests must not have a test-side package-info.java' // ── 7. Service and domain module common-domain wiring ──────────────────────── @@ -122,6 +117,16 @@ assert text('service-orders/pom.xml').contains('common-domaincommon-domain') : 'service-inventory missing common-domain dep' assert text('domain-db-main/pom.xml').contains('common-domain') : 'domain-db-main missing common-domain dep' assert text('domain-rest/pom.xml').contains('common-domain') : 'domain-rest missing common-domain dep' +// domain-rest ("the service domain deps") depends on both named service areas' domain modules +['domain-service-orders', 'domain-service-inventory'].each { svcDom -> + assert text('domain-rest/pom.xml').contains("${svcDom}") : "domain-rest must depend on ${svcDom}" +} + +// appName-derived names/descriptions fold in the area name +assert raw('service-orders/pom.xml').contains("${appName} Service Orders") : 'service-orders must include the area name' +assert raw('service-orders/pom.xml').contains("${appName}'s orders service tier (business logic).") : 'service-orders wrong' +assert raw('service-inventory/pom.xml').contains("${appName} Service Inventory") : 'service-inventory must include the area name' +assert raw('domain-service-orders/pom.xml').contains("${appName} Domain Service Orders") : 'domain-service-orders must include the area name' // ── 8. app depends on both service modules ──────────────────────────────────── @@ -132,6 +137,7 @@ def appPom = text('app/pom.xml') } assert !appPom.contains('service') : "app must not reference non-existent plain 'service'" assert !appPom.contains('acceptance-tests') : 'app must not depend on acceptance-tests' +assert appPom.contains('spring-boot-starter-data-jpa') : 'app must depend on spring-boot-starter-data-jpa (database integration present)' // ── 9. acceptance-tests deps ────────────────────────────────────────────────── @@ -141,6 +147,8 @@ def atPom = text('acceptance-tests/pom.xml') } assert !atPom.contains('service-orders') : 'acceptance-tests must not depend on service-orders' assert !atPom.contains('service-inventory') : 'acceptance-tests must not depend on service-inventory' +assert atPom.contains('common-testing') : 'acceptance-tests missing common-testing TEST dep' +assert atPom.contains('datasource-proxy-spring-boot-starter') : 'acceptance-tests missing datasource-proxy dep' // ── src directory scaffolding — all four dirs present in every module ───────── @@ -165,10 +173,22 @@ modules.findAll { it.startsWith('integration-') }.each { m -> ['domain-service-orders', 'domain-service-inventory'].each { m -> check("${m}/pom.xml") assert text("${m}/pom.xml").contains('common-domain') : "${m} missing common-domain dep" + assert !text("${m}/pom.xml").contains('common-testing') : "${m} must not depend on common-testing (no domain module does)" + assert text("${m}/pom.xml").contains('spring-boot-starter-validation') : "${m} missing spring-boot-starter-validation dep" } assert text('service-orders/pom.xml').contains('domain-service-orders') : 'service-orders must depend on domain-service-orders' assert text('service-inventory/pom.xml').contains('domain-service-inventory') : 'service-inventory must depend on domain-service-inventory' +['service-orders', 'service-inventory'].each { m -> + assert text("${m}/pom.xml").contains('common-testing') : "${m} missing common-testing TEST dep" +} assert parentPom.contains('spring-boot-starter-parent') : 'parent must inherit spring-boot-starter-parent' +// common-testing depends on every domain module, including the named service-area domain modules. +// No domain module depends back on common-testing (only service/service-{name} does). +assert text('common-testing/pom.xml').contains('domain-db-main') : 'common-testing missing integration domain dep' +['domain-service-orders', 'domain-service-inventory', 'domain-rest'].each { m -> + assert text('common-testing/pom.xml').contains("${m}") : "common-testing missing domain dep: ${m}" +} + _buildLog.append("\n=== PASSED ===\n") true diff --git a/src/test/resources/projects/no-integrations/archetype.properties b/src/test/resources/projects/no-integrations/archetype.properties index 9167b43..e13c11a 100644 --- a/src/test/resources/projects/no-integrations/archetype.properties +++ b/src/test/resources/projects/no-integrations/archetype.properties @@ -2,6 +2,7 @@ groupId=com.example artifactId=no-integrations-test version=1.0.0-SNAPSHOT package=com.example.noint +appName=No Integrations Test integrations=, serviceAreas=, presentationTypes=rest diff --git a/src/test/resources/projects/no-integrations/verify.groovy b/src/test/resources/projects/no-integrations/verify.groovy index 313c54f..f8c59a6 100644 --- a/src/test/resources/projects/no-integrations/verify.groovy +++ b/src/test/resources/projects/no-integrations/verify.groovy @@ -1,5 +1,5 @@ // ── Test: no-integrations ───────────────────────────────────────────────────── -// integrations= serviceAreas=, presentationTypes=rest +// appName=No Integrations Test integrations= serviceAreas=, presentationTypes=rest // Covers: empty integrations → no domain-*/integration-* integration modules; // fixed modules + service + presentation still created correctly @@ -20,6 +20,7 @@ def check = { String rel -> // tags; the prefix is verified explicitly via raw() (module's own artifactId + parent DM), and the // generated project's real Maven build — run before this script — fails on any inconsistent reference. def aid = 'no-integrations-test' +def appName = 'No Integrations Test' def raw = { String rel -> new File(basedir, rel).text } @@ -54,6 +55,8 @@ modules.each { m -> assert parentPom.contains("${aid}-${m}") : "parent missing: $m" } assert parentPom.contains('pom') : "parent must have pom packaging" +assert parentPom.contains("${appName} Parent") : 'parent must be " Parent"' +assert parentPom.contains("${appName}'s parent POM.") : "parent must be \"'s parent POM.\"" def moduleOrder = modules.sort().collect { "../${it}" } def moduleBlock = parentPom.replaceAll(/(?s).*(.*?)<\/modules>.*/, '$1') @@ -77,38 +80,34 @@ modules.each { m -> : "${m}/pom.xml must declare its own artifactId as '${aid}-${m}'" } -// ── 6. Source skeletons ─────────────────────────────────────────────────────── +// ── 6. Source skeletons — package-info.java only under src/main/java ──────── def p = 'com/example/noint' def pkg = p.replace('/', '.') [ "common-domain/src/main/java/${p}/common/domain/package-info.java", - "common-domain/src/test/java/${p}/common/domain/package-info.java", "common-testing/src/main/java/${p}/common/testing/package-info.java", "domain-rest/src/main/java/${p}/domain/rest/package-info.java", - "domain-rest/src/test/java/${p}/domain/rest/package-info.java", "presentation-rest/src/main/java/${p}/presentation/rest/package-info.java", - "presentation-rest/src/test/java/${p}/presentation/rest/package-info.java", "service/src/main/java/${p}/service/package-info.java", - "service/src/test/java/${p}/service/package-info.java", "app/src/main/java/${p}/app/package-info.java", - "app/src/test/java/${p}/app/package-info.java", - "acceptance-tests/src/test/java/${p}/at/package-info.java", + "acceptance-tests/src/main/java/${p}/at/package-info.java", ].each { check(it) } assert text("common-domain/src/main/java/${p}/common/domain/package-info.java").contains("package ${pkg}.common.domain;") : 'common-domain package-info.java has wrong package declaration' assert text("common-testing/src/main/java/${p}/common/testing/package-info.java").contains("package ${pkg}.common.testing;") : 'common-testing package-info.java has wrong package declaration' assert text("service/src/main/java/${p}/service/package-info.java").contains("package ${pkg}.service;") : 'service package-info.java has wrong package declaration' assert text("app/src/main/java/${p}/app/package-info.java").contains("package ${pkg}.app;") : 'app package-info.java has wrong package declaration' -assert text("acceptance-tests/src/test/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' -assert new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() : 'common-testing must have test Java package-info.java' -assert new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() : 'acceptance-tests must have main Java package-info.java' +assert text("acceptance-tests/src/main/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/package-info.java").exists() : 'acceptance-tests must not have a test-side package-info.java' // ── 7. Dependency wiring ───────────────────────────────────────────────────── assert text('domain-rest/pom.xml').contains('common-domain') : 'domain-rest missing common-domain dep' +assert text('domain-rest/pom.xml').contains('domain-service') : 'domain-rest missing domain-service dep (the service domain deps)' assert text('presentation-rest/pom.xml').contains('domain-rest') : 'presentation-rest missing domain-rest dep' assert text('service/pom.xml').contains('common-domain') : 'service missing common-domain dep' +assert text('service/pom.xml').contains('common-testing') : 'service missing common-testing TEST dep' def appPom = text('app/pom.xml') ['common-domain', 'domain-rest', 'presentation-rest', 'service'].each { dep -> @@ -116,12 +115,20 @@ def appPom = text('app/pom.xml') } assert !appPom.contains('domain-db-main') : 'app must not reference non-existent integration module' assert !appPom.contains('integration-db-main') : 'app must not reference non-existent integration module' +// no database integration: no spring-boot-starter-data-jpa; web/actuator/admin stack unconditional +assert !appPom.contains('spring-boot-starter-data-jpa') : 'no database integration: app must not declare spring-boot-starter-data-jpa' +assert appPom.contains('spring-boot-starter-webmvc') : 'app must depend on spring-boot-starter-webmvc' +assert appPom.contains('spring-boot-starter-actuator') : 'app must depend on spring-boot-starter-actuator' +assert appPom.contains('spring-boot-admin-starter-client') : 'app must depend on spring-boot-admin-starter-client' def atPom = text('acceptance-tests/pom.xml') ['app', 'common-domain', 'domain-rest'].each { dep -> assert atPom.contains("${dep}") : "acceptance-tests missing dep: $dep" } assert !atPom.contains('service') : 'acceptance-tests must not depend on service' +assert atPom.contains('common-testing') : 'acceptance-tests missing common-testing TEST dep' +// no database integration: datasource-proxy (sql logging) must not be added +assert !atPom.contains('datasource-proxy-spring-boot-starter') : 'acceptance-tests must not get the datasource-proxy dep' // ── src directory scaffolding — all four dirs present in every module ───────── @@ -141,5 +148,11 @@ modules.findAll { it.startsWith('integration-') }.each { m -> : "${m} must configure maven-failsafe-plugin" } +// ── AppServletInitializer is unconditional now (app is always a servlet web app) ─ + +check("app/src/main/java/${p}/app/config/AppServletInitializer.java") +assert !text("app/src/main/java/${p}/app/config/Application.java").contains('@EnableTransactionManagement') \ + : 'no database integration: Application must not @EnableTransactionManagement' + _buildLog.append("\n=== PASSED ===\n") true diff --git a/src/test/resources/projects/no-presentation/archetype.properties b/src/test/resources/projects/no-presentation/archetype.properties index 343787b..deb273a 100644 --- a/src/test/resources/projects/no-presentation/archetype.properties +++ b/src/test/resources/projects/no-presentation/archetype.properties @@ -2,6 +2,7 @@ groupId=com.example artifactId=no-presentation-test version=1.0.0-SNAPSHOT package=com.example.nopres +appName=No Presentation Test integrations=database:main serviceAreas=, presentationTypes=, diff --git a/src/test/resources/projects/no-presentation/verify.groovy b/src/test/resources/projects/no-presentation/verify.groovy index f336e42..cde16cf 100644 --- a/src/test/resources/projects/no-presentation/verify.groovy +++ b/src/test/resources/projects/no-presentation/verify.groovy @@ -1,5 +1,5 @@ // ── Test: no-presentation ───────────────────────────────────────────────────── -// integrations=database:main serviceAreas=, presentationTypes= (empty) +// appName=No Presentation Test integrations=database:main serviceAreas=, presentationTypes= (empty) // Covers: empty presentationTypes → no domain-{type}/presentation-{type} modules; // headless service with only integration + service layers @@ -20,6 +20,7 @@ def check = { String rel -> // tags; the prefix is verified explicitly via raw() (module's own artifactId + parent DM), and the // generated project's real Maven build — run before this script — fails on any inconsistent reference. def aid = 'no-presentation-test' +def appName = 'No Presentation Test' def raw = { String rel -> new File(basedir, rel).text } @@ -56,6 +57,8 @@ modules.each { m -> assert parentPom.contains("${aid}-${m}") : "parent missing: $m" } assert parentPom.contains('pom') : "parent must have pom packaging" +assert parentPom.contains("${appName} Parent") : 'parent must be " Parent"' +assert parentPom.contains("${appName}'s parent POM.") : "parent must be \"'s parent POM.\"" def moduleOrder = modules.sort().collect { "../${it}" } def moduleBlock = parentPom.replaceAll(/(?s).*(.*?)<\/modules>.*/, '$1') @@ -79,23 +82,18 @@ modules.each { m -> : "${m}/pom.xml must declare its own artifactId as '${aid}-${m}'" } -// ── 6. Source skeletons ─────────────────────────────────────────────────────── +// ── 6. Source skeletons — package-info.java only under src/main/java ──────── def p = 'com/example/nopres' def pkg = p.replace('/', '.') [ "common-domain/src/main/java/${p}/common/domain/package-info.java", - "common-domain/src/test/java/${p}/common/domain/package-info.java", "common-testing/src/main/java/${p}/common/testing/package-info.java", "domain-db-main/src/main/java/${p}/domain/db/main/package-info.java", - "domain-db-main/src/test/java/${p}/domain/db/main/package-info.java", "integration-db-main/src/main/java/${p}/integration/db/main/package-info.java", - "integration-db-main/src/test/java/${p}/integration/db/main/package-info.java", "service/src/main/java/${p}/service/package-info.java", - "service/src/test/java/${p}/service/package-info.java", "app/src/main/java/${p}/app/package-info.java", - "app/src/test/java/${p}/app/package-info.java", - "acceptance-tests/src/test/java/${p}/at/package-info.java", + "acceptance-tests/src/main/java/${p}/at/package-info.java", ].each { check(it) } assert text("common-domain/src/main/java/${p}/common/domain/package-info.java").contains("package ${pkg}.common.domain;") : 'common-domain package-info.java has wrong package declaration' @@ -103,15 +101,17 @@ assert text("common-testing/src/main/java/${p}/common/testing/package-info.java" assert text("integration-db-main/src/main/java/${p}/integration/db/main/package-info.java").contains("package ${pkg}.integration.db.main;") : 'integration-db-main package-info.java has wrong package declaration' assert text("service/src/main/java/${p}/service/package-info.java").contains("package ${pkg}.service;") : 'service package-info.java has wrong package declaration' assert text("app/src/main/java/${p}/app/package-info.java").contains("package ${pkg}.app;") : 'app package-info.java has wrong package declaration' -assert text("acceptance-tests/src/test/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' -assert new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() : 'common-testing must have test Java package-info.java' -assert new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() : 'acceptance-tests must have main Java package-info.java' +assert text("acceptance-tests/src/main/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/package-info.java").exists() : 'acceptance-tests must not have a test-side package-info.java' // ── 7. Dependency wiring ───────────────────────────────────────────────────── assert text('domain-db-main/pom.xml').contains('common-domain') : 'domain-db-main missing common-domain dep' assert text('integration-db-main/pom.xml').contains('domain-db-main') : 'integration-db-main missing domain-db-main dep' +assert text('integration-db-main/pom.xml').contains('datasource-proxy-spring-boot-starter') \ + : 'integration-db-main (a database integration) missing datasource-proxy dep' assert text('service/pom.xml').contains('common-domain') : 'service missing common-domain dep' +assert text('service/pom.xml').contains('common-testing') : 'service missing common-testing TEST dep' def appPom = text('app/pom.xml') ['common-domain', 'domain-db-main', 'integration-db-main', 'service'].each { dep -> @@ -120,16 +120,18 @@ def appPom = text('app/pom.xml') assert !appPom.contains('domain-rest') : 'app must not reference non-existent presentation module' assert !appPom.contains('presentation-rest') : 'app must not reference non-existent presentation module' -// ── Spring entry points: database integration but no rest presentation ─────── -// → app gets the data-jpa starter + @EnableTransactionManagement, but no web starter and no -// AppServletInitializer (it is a JPA application, not a servlet web application). +// ── Spring entry points: database integration but no presentation at all ───── +// → app gets the data-jpa starter + @EnableTransactionManagement; AppServletInitializer and the +// web/actuator/admin-client stack are unconditional now (regardless of presentationTypes), so +// they are still present even though this project is headless (no presentation modules). check("app/src/main/java/${p}/app/config/Application.java") def appMain = text("app/src/main/java/${p}/app/config/Application.java") assert appMain.contains('@EnableTransactionManagement') : 'database integration: Application must @EnableTransactionManagement' -assert !new File(basedir, "app/src/main/java/${p}/app/config/AppServletInitializer.java").exists() \ - : 'no rest presentation: AppServletInitializer must not be generated' -assert appPom.contains('spring-boot-starter-data-jpa') : 'database integration: app must declare spring-boot-starter-data-jpa' -assert !appPom.contains('spring-boot-starter-web') : 'no rest presentation: app must not declare spring-boot-starter-web' +check("app/src/main/java/${p}/app/config/AppServletInitializer.java") +assert appPom.contains('spring-boot-starter-data-jpa') : 'database integration: app must declare spring-boot-starter-data-jpa' +assert appPom.contains('spring-boot-starter-webmvc') : 'app must depend on spring-boot-starter-webmvc regardless of presentationTypes' +assert appPom.contains('spring-boot-starter-actuator') : 'app must depend on spring-boot-starter-actuator' +assert appPom.contains('spring-boot-admin-starter-client') : 'app must depend on spring-boot-admin-starter-client' def atPom = text('acceptance-tests/pom.xml') ['app', 'common-domain', 'domain-db-main'].each { dep -> @@ -137,6 +139,8 @@ def atPom = text('acceptance-tests/pom.xml') } assert !atPom.contains('domain-rest') : 'acceptance-tests must not reference non-existent presentation domain' assert !atPom.contains('service') : 'acceptance-tests must not depend on service' +assert atPom.contains('common-testing') : 'acceptance-tests missing common-testing TEST dep' +assert atPom.contains('datasource-proxy-spring-boot-starter') : 'acceptance-tests missing datasource-proxy dep' // ── src directory scaffolding — all four dirs present in every module ───────── @@ -156,5 +160,16 @@ modules.findAll { it.startsWith('integration-') }.each { m -> : "${m} must configure maven-failsafe-plugin" } +// ── Acceptance-test infrastructure: AT config/logback exist; no REST/GraphQL base +// classes anywhere (no presentation types at all) ────────────────────────── + +def atConfigClass = 'NoPresentationTestAcceptanceTestConfiguration' +check("acceptance-tests/src/test/java/${p}/at/config/${atConfigClass}.java") +check('acceptance-tests/src/test/resources/logback-spring.xml') +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/AppRestAcceptanceTestBase.java").exists() \ + : 'AppRestAcceptanceTestBase must not be generated (no rest presentation)' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/AppGraphQlAcceptanceTestBase.java").exists() \ + : 'AppGraphQlAcceptanceTestBase must not be generated (no graphql presentation)' + _buildLog.append("\n=== PASSED ===\n") true diff --git a/src/test/resources/projects/package-differs-from-groupid/archetype.properties b/src/test/resources/projects/package-differs-from-groupid/archetype.properties index ec654bb..2c6d215 100644 --- a/src/test/resources/projects/package-differs-from-groupid/archetype.properties +++ b/src/test/resources/projects/package-differs-from-groupid/archetype.properties @@ -2,6 +2,7 @@ groupId=com.example artifactId=pkg-diff-test version=1.0.0-SNAPSHOT package=org.mycompany.service +appName=Pkg Diff Test integrations=database:users serviceAreas=, presentationTypes=rest diff --git a/src/test/resources/projects/package-differs-from-groupid/verify.groovy b/src/test/resources/projects/package-differs-from-groupid/verify.groovy index f387903..88008c8 100644 --- a/src/test/resources/projects/package-differs-from-groupid/verify.groovy +++ b/src/test/resources/projects/package-differs-from-groupid/verify.groovy @@ -20,6 +20,7 @@ def check = { String rel -> // tags; the prefix is verified explicitly via raw() (module's own artifactId + parent DM), and the // generated project's real Maven build — run before this script — fails on any inconsistent reference. def aid = 'pkg-diff-test' +def appName = 'Pkg Diff Test' def raw = { String rel -> new File(basedir, rel).text } @@ -52,6 +53,8 @@ modules.each { m -> assert parentPom.contains("${aid}-${m}") : "parent missing: $m" } assert parentPom.contains('pom') : "parent must have pom packaging" +assert parentPom.contains("${appName} Parent") : 'parent must be " Parent"' +assert parentPom.contains("${appName}'s parent POM.") : "parent must be \"'s parent POM.\"" def moduleOrder = modules.sort().collect { "../${it}" } def moduleBlock = parentPom.replaceAll(/(?s).*(.*?)<\/modules>.*/, '$1') @@ -76,36 +79,29 @@ modules.each { m -> } // ── 5. Source tree uses 'package' (org.mycompany.service), NOT 'groupId' ────── +// package-info.java only under src/main/java (see basic/verify.groovy for the exhaustive check). def p = 'org/mycompany/service' def pkg = p.replace('/', '.') [ "common-domain/src/main/java/${p}/common/domain/package-info.java", - "common-domain/src/test/java/${p}/common/domain/package-info.java", "common-testing/src/main/java/${p}/common/testing/package-info.java", "domain-db-users/src/main/java/${p}/domain/db/users/package-info.java", - "domain-db-users/src/test/java/${p}/domain/db/users/package-info.java", "integration-db-users/src/main/java/${p}/integration/db/users/package-info.java", - "integration-db-users/src/test/java/${p}/integration/db/users/package-info.java", "domain-rest/src/main/java/${p}/domain/rest/package-info.java", - "domain-rest/src/test/java/${p}/domain/rest/package-info.java", "presentation-rest/src/main/java/${p}/presentation/rest/package-info.java", - "presentation-rest/src/test/java/${p}/presentation/rest/package-info.java", "service/src/main/java/${p}/service/package-info.java", - "service/src/test/java/${p}/service/package-info.java", "app/src/main/java/${p}/app/package-info.java", - "app/src/test/java/${p}/app/package-info.java", - "acceptance-tests/src/test/java/${p}/at/package-info.java", + "acceptance-tests/src/main/java/${p}/at/package-info.java", ].each { check(it) } assert text("common-domain/src/main/java/${p}/common/domain/package-info.java").contains("package ${pkg}.common.domain;") : 'package-info.java must use the package property, not groupId' assert text("common-testing/src/main/java/${p}/common/testing/package-info.java").contains("package ${pkg}.common.testing;") : 'common-testing package-info.java must use the package property' assert text("service/src/main/java/${p}/service/package-info.java").contains("package ${pkg}.service;") : 'service package-info.java must use the package property' assert text("app/src/main/java/${p}/app/package-info.java").contains("package ${pkg}.app;") : 'app package-info.java must use the package property' -assert text("acceptance-tests/src/test/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java must use the package property' -assert new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() : 'common-testing must have test Java package-info.java' -assert new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() : 'acceptance-tests must have main Java package-info.java' +assert text("acceptance-tests/src/main/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java must use the package property' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/package-info.java").exists() : 'acceptance-tests must not have a test-side package-info.java' // ── 6. groupId-based source paths must NOT exist ───────────────────────────── @@ -126,9 +122,19 @@ modules.each { m -> assert text('domain-db-users/pom.xml').contains('common-domain') : 'domain-db-users missing common-domain dep' assert text('integration-db-users/pom.xml').contains('domain-db-users') : 'integration-db-users missing domain dep' +assert text('integration-db-users/pom.xml').contains('datasource-proxy-spring-boot-starter') \ + : 'integration-db-users (a database integration) missing datasource-proxy dep' assert text('domain-rest/pom.xml').contains('common-domain') : 'domain-rest missing common-domain dep' +assert text('domain-rest/pom.xml').contains('domain-service') : 'domain-rest missing domain-service dep (the service domain deps)' assert text('presentation-rest/pom.xml').contains('domain-rest') : 'presentation-rest missing domain-rest dep' assert text('service/pom.xml').contains('common-domain') : 'service missing common-domain dep' +assert text('service/pom.xml').contains('common-testing') : 'service missing common-testing TEST dep' +assert !text('domain-service/pom.xml').contains('common-testing') : 'domain-service must not depend on common-testing (no domain module does)' +assert text('domain-service/pom.xml').contains('spring-boot-starter-validation') : 'domain-service missing spring-boot-starter-validation dep' +assert text('common-testing/pom.xml').contains('domain-service') : 'common-testing must depend on domain-service' +assert text('common-testing/pom.xml').contains('domain-rest') : 'common-testing must depend on domain-rest' +assert text('acceptance-tests/pom.xml').contains('common-testing') : 'acceptance-tests missing common-testing TEST dep' +assert text('acceptance-tests/pom.xml').contains('datasource-proxy-spring-boot-starter') : 'acceptance-tests missing datasource-proxy dep' // ── Failsafe plugin wiring ────────────────────────────────────── diff --git a/src/test/resources/projects/service-integration-match/archetype.properties b/src/test/resources/projects/service-integration-match/archetype.properties index 5d4e455..52a703b 100644 --- a/src/test/resources/projects/service-integration-match/archetype.properties +++ b/src/test/resources/projects/service-integration-match/archetype.properties @@ -2,6 +2,7 @@ groupId=com.example artifactId=svc-int-match-test version=1.0.0-SNAPSHOT package=com.example.svcintmatch +appName=Svc Int Match Test integrations=database:orders,rest:billing serviceAreas=orders,reporting presentationTypes=, diff --git a/src/test/resources/projects/service-integration-match/verify.groovy b/src/test/resources/projects/service-integration-match/verify.groovy index 28beb89..915839f 100644 --- a/src/test/resources/projects/service-integration-match/verify.groovy +++ b/src/test/resources/projects/service-integration-match/verify.groovy @@ -1,5 +1,6 @@ // ── Test: service-integration-match ────────────────────────────────────────── -// integrations=database:orders,rest:billing serviceAreas=orders,reporting presentationTypes=, +// appName=Svc Int Match Test integrations=database:orders,rest:billing serviceAreas=orders,reporting +// presentationTypes=, // Covers (#2): a named service area depends on the integration module sharing its name // (service-orders -> integration-db-orders); a service area with no name match takes no // integration (service-reporting); and an integration with no matching service area @@ -17,6 +18,7 @@ def check = { String rel -> assert new File(basedir, rel).exists() : "Missing: $rel" } def aid = 'svc-int-match-test' +def appName = 'Svc Int Match Test' def raw = { String rel -> new File(basedir, rel).text } @@ -45,17 +47,52 @@ def ordersSvc = text('service-orders/pom.xml') assert ordersSvc.contains('integration-db-orders') : 'service-orders must depend on integration-db-orders (shared name)' assert ordersSvc.contains('domain-service-orders') : 'service-orders must depend on its own domain-service module' assert !ordersSvc.contains('integration-rest-billing') : 'service-orders must not take the unrelated integration-rest-billing' +assert ordersSvc.contains('common-testing') : 'service-orders missing common-testing TEST dep' def reportingSvc = text('service-reporting/pom.xml') assert reportingSvc.contains('domain-service-reporting') : 'service-reporting must depend on its own domain-service module' assert !reportingSvc.contains('integration-') : 'service-reporting has no name match, so it takes no integration' +assert reportingSvc.contains('common-testing') : 'service-reporting missing common-testing TEST dep' -// parent dependencyManagement manages exactly the sibling modules +// domain-service-* modules: spring-boot-starter-validation, but NOT common-testing (no domain +// module depends on common-testing) +['domain-service-orders', 'domain-service-reporting'].each { m -> + assert !text("${m}/pom.xml").contains('common-testing') : "${m} must not depend on common-testing" + assert text("${m}/pom.xml").contains('spring-boot-starter-validation') : "${m} missing spring-boot-starter-validation dep" +} + +// only the database integration gets the SQL logging dependency +assert text('integration-db-orders/pom.xml').contains('datasource-proxy-spring-boot-starter') : 'integration-db-orders missing datasource-proxy dep' +assert !text('integration-rest-billing/pom.xml').contains('datasource-proxy-spring-boot-starter') : 'integration-rest-billing must not get the datasource-proxy dep' + +// common-testing depends on every domain module, including the named service-area domain modules. +// No domain module depends back on common-testing (only service/service-{name} does). +def commonTestingPom = text('common-testing/pom.xml') +['domain-db-orders', 'domain-rest-billing', 'domain-service-orders', 'domain-service-reporting'].each { m -> + assert commonTestingPom.contains("${m}") : "common-testing missing domain dep: ${m}" +} + +// acceptance-tests: common-testing TEST dep + datasource-proxy (sql logging) +def atPom = text('acceptance-tests/pom.xml') +assert atPom.contains('common-testing') : 'acceptance-tests missing common-testing TEST dep' +assert atPom.contains('datasource-proxy-spring-boot-starter') : 'acceptance-tests missing datasource-proxy dep' +assert !atPom.contains('spring-graphql-test') : 'acceptance-tests must not add spring-graphql-test (no graphql presentation)' + +// parent dependencyManagement manages exactly the sibling modules; appName-derived name/description def parentPom = raw('parent/pom.xml') def dmBlock = (parentPom =~ /(?s).*?<\/dependencyManagement>/)[0] def dmIds = dmBlock.findAll(/[^<]+<\/artifactId>/).collect { it.replaceAll(/<\/?artifactId>/, '') } def dmModuleIds = dmIds.findAll { it.startsWith("${aid}-") }.collect { it.substring(aid.length() + 1) } assert dmModuleIds.sort() == modules.sort() : 'parent dependencyManagement must manage exactly the sibling modules' +assert parentPom.contains("${appName} Parent") : 'parent must be " Parent"' +assert parentPom.contains("${appName}'s parent POM.") : "parent must be \"'s parent POM.\"" + +// no presentation types at all: no domain-{type}/presentation-{type} modules, no AT base classes +assert !new File(basedir, 'domain-rest').exists() : 'no presentation modules must be created' +assert !new File(basedir, 'presentation-rest').exists() : 'no presentation modules must be created' +def p = 'com/example/svcintmatch' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/AppRestAcceptanceTestBase.java").exists() \ + : 'AppRestAcceptanceTestBase must not be generated (no presentation types)' // src scaffolding present in every module modules.each { m -> diff --git a/src/test/resources/projects/single-service-fallback/archetype.properties b/src/test/resources/projects/single-service-fallback/archetype.properties index 0449a93..235a402 100644 --- a/src/test/resources/projects/single-service-fallback/archetype.properties +++ b/src/test/resources/projects/single-service-fallback/archetype.properties @@ -2,6 +2,7 @@ groupId=com.example artifactId=fallback-test version=1.0.0-SNAPSHOT package=com.example.fallback +appName=Fallback Test integrations=database:main serviceAreas=, presentationTypes=graphql diff --git a/src/test/resources/projects/single-service-fallback/verify.groovy b/src/test/resources/projects/single-service-fallback/verify.groovy index 153b475..5ff3c84 100644 --- a/src/test/resources/projects/single-service-fallback/verify.groovy +++ b/src/test/resources/projects/single-service-fallback/verify.groovy @@ -1,7 +1,7 @@ // ── Test: single-service-fallback ──────────────────────────────────────────── -// integrations=database:main serviceAreas=, presentationTypes=graphql +// appName=Fallback Test integrations=database:main serviceAreas=, presentationTypes=graphql // Covers: effectively empty serviceAreas (comma-only) → falls back to a single plain -// "service" module; graphql as the sole presentation type +// "service" module; graphql as the sole presentation type (no rest at all) def _testName = new File(basedir, 'project').isDirectory() ? basedir.name : 'unknown' def _projectSubdir = new File(basedir, 'project') @@ -20,6 +20,7 @@ def check = { String rel -> // tags; the prefix is verified explicitly via raw() (module's own artifactId + parent DM), and the // generated project's real Maven build — run before this script — fails on any inconsistent reference. def aid = 'fallback-test' +def appName = 'Fallback Test' def raw = { String rel -> new File(basedir, rel).text } @@ -65,6 +66,8 @@ modules.each { m -> assert parentPom.contains("${aid}-${m}") : "parent missing: $m" } assert parentPom.contains('pom') : "parent must have pom packaging" +assert parentPom.contains("${appName} Parent") : 'parent must be " Parent"' +assert parentPom.contains("${appName}'s parent POM.") : "parent must be \"'s parent POM.\"" def moduleOrder = modules.sort().collect { "../${it}" } def moduleBlock = parentPom.replaceAll(/(?s).*(.*?)<\/modules>.*/, '$1') @@ -88,27 +91,20 @@ modules.each { m -> : "${m}/pom.xml must declare its own artifactId as '${aid}-${m}'" } -// ── 7. Source skeletons ─────────────────────────────────────────────────────── +// ── 7. Source skeletons — package-info.java only under src/main/java ──────── def p = 'com/example/fallback' def pkg = p.replace('/', '.') [ "common-domain/src/main/java/${p}/common/domain/package-info.java", - "common-domain/src/test/java/${p}/common/domain/package-info.java", "common-testing/src/main/java/${p}/common/testing/package-info.java", "domain-db-main/src/main/java/${p}/domain/db/main/package-info.java", - "domain-db-main/src/test/java/${p}/domain/db/main/package-info.java", "integration-db-main/src/main/java/${p}/integration/db/main/package-info.java", - "integration-db-main/src/test/java/${p}/integration/db/main/package-info.java", "domain-graphql/src/main/java/${p}/domain/graphql/package-info.java", - "domain-graphql/src/test/java/${p}/domain/graphql/package-info.java", "presentation-graphql/src/main/java/${p}/presentation/graphql/package-info.java", - "presentation-graphql/src/test/java/${p}/presentation/graphql/package-info.java", "service/src/main/java/${p}/service/package-info.java", - "service/src/test/java/${p}/service/package-info.java", "app/src/main/java/${p}/app/package-info.java", - "app/src/test/java/${p}/app/package-info.java", - "acceptance-tests/src/test/java/${p}/at/package-info.java", + "acceptance-tests/src/main/java/${p}/at/package-info.java", ].each { check(it) } assert text("common-domain/src/main/java/${p}/common/domain/package-info.java").contains("package ${pkg}.common.domain;") : 'common-domain package-info.java has wrong package declaration' @@ -116,15 +112,21 @@ assert text("common-testing/src/main/java/${p}/common/testing/package-info.java" assert text("presentation-graphql/src/main/java/${p}/presentation/graphql/package-info.java").contains("package ${pkg}.presentation.graphql;") : 'presentation-graphql package-info.java has wrong package declaration' assert text("service/src/main/java/${p}/service/package-info.java").contains("package ${pkg}.service;") : 'service package-info.java has wrong package declaration' assert text("app/src/main/java/${p}/app/package-info.java").contains("package ${pkg}.app;") : 'app package-info.java has wrong package declaration' -assert text("acceptance-tests/src/test/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' -assert new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() : 'common-testing must have test Java package-info.java' -assert new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() : 'acceptance-tests must have main Java package-info.java' +assert text("acceptance-tests/src/main/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/package-info.java").exists() : 'acceptance-tests must not have a test-side package-info.java' // ── 8. common-domain wiring for domain and service modules ─────────────────── assert text('domain-db-main/pom.xml').contains('common-domain') : 'domain-db-main missing common-domain dep' assert text('domain-graphql/pom.xml').contains('common-domain') : 'domain-graphql missing common-domain dep' +assert text('domain-graphql/pom.xml').contains('domain-service') : 'domain-graphql missing domain-service dep (the service domain deps)' assert text('service/pom.xml').contains('common-domain') : 'service missing common-domain dep' +assert text('service/pom.xml').contains('common-testing') : 'service missing common-testing TEST dep' +assert !text('domain-service/pom.xml').contains('common-testing') : 'domain-service must not depend on common-testing (no domain module does)' +assert text('domain-service/pom.xml').contains('spring-boot-starter-validation') : 'domain-service missing spring-boot-starter-validation dep' +assert text('common-testing/pom.xml').contains('domain-db-main') : 'common-testing missing integration domain dep' +assert text('common-testing/pom.xml').contains('domain-service') : 'common-testing must depend on domain-service' +assert text('common-testing/pom.xml').contains('domain-graphql') : 'common-testing must depend on domain-graphql' // ── 9. Dependency wiring ───────────────────────────────────────────────────── @@ -136,11 +138,15 @@ def appPom = text('app/pom.xml') 'integration-db-main', 'presentation-graphql', 'service'].each { dep -> assert appPom.contains("${dep}") : "app missing dep: $dep" } +assert appPom.contains('spring-boot-starter-data-jpa') : 'app must depend on spring-boot-starter-data-jpa (database integration present)' def atPom = text('acceptance-tests/pom.xml') ['app', 'common-domain', 'domain-db-main', 'domain-graphql'].each { dep -> assert atPom.contains("${dep}") : "acceptance-tests missing dep: $dep" } +assert atPom.contains('common-testing') : 'acceptance-tests missing common-testing TEST dep' +assert atPom.contains('spring-graphql-test') : 'acceptance-tests missing spring-graphql-test dep (graphql presentation present)' +assert atPom.contains('datasource-proxy-spring-boot-starter') : 'acceptance-tests missing datasource-proxy dep' // ── src directory scaffolding — all four dirs present in every module ───────── @@ -160,5 +166,18 @@ modules.findAll { it.startsWith('integration-') }.each { m -> : "${m} must configure maven-failsafe-plugin" } +// ── Acceptance-test infrastructure (GraphQL only — no rest presentation) ───── + +check("acceptance-tests/src/test/java/${p}/at/AppGraphQlAcceptanceTestBase.java") +def appGraphQlAt = text("acceptance-tests/src/test/java/${p}/at/AppGraphQlAcceptanceTestBase.java") +assert appGraphQlAt.contains('extends GraphQlAcceptanceTestBase') : 'AppGraphQlAcceptanceTestBase must extend GraphQlAcceptanceTestBase' +assert appGraphQlAt.contains('@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class, XADataSourceAutoConfiguration.class })') \ + : 'AppGraphQlAcceptanceTestBase must exclude DataSourceAutoConfiguration/XADataSourceAutoConfiguration (database integration present)' +check("common-testing/src/main/java/${p}/common/testing/GraphQlAcceptanceTestBase.java") +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/AppRestAcceptanceTestBase.java").exists() \ + : 'AppRestAcceptanceTestBase must not be generated (no rest presentation)' +assert !new File(basedir, "common-testing/src/main/java/${p}/common/testing/RestAcceptanceTestBase.java").exists() \ + : 'RestAcceptanceTestBase must not be generated (no rest presentation)' + _buildLog.append("\n=== PASSED ===\n") true diff --git a/src/test/resources/projects/spring-off/archetype.properties b/src/test/resources/projects/spring-off/archetype.properties index fc65208..a701582 100644 --- a/src/test/resources/projects/spring-off/archetype.properties +++ b/src/test/resources/projects/spring-off/archetype.properties @@ -2,6 +2,7 @@ groupId=com.example artifactId=spring-off-test version=1.0.0-SNAPSHOT package=com.example.springoff +appName=Spring Off Test integrations=database:users serviceAreas=, presentationTypes=rest diff --git a/src/test/resources/projects/spring-off/verify.groovy b/src/test/resources/projects/spring-off/verify.groovy index 0a84726..87a8c9b 100644 --- a/src/test/resources/projects/spring-off/verify.groovy +++ b/src/test/resources/projects/spring-off/verify.groovy @@ -1,9 +1,11 @@ // ── Test: spring-off ────────────────────────────────────────────────────────── -// integrations=database:users serviceAreas=, presentationTypes=rest includeSpring=false +// appName=Spring Off Test integrations=database:users serviceAreas=, presentationTypes=rest +// includeSpring=false // Covers: includeSpring=false omits ALL Spring artifacts (parent inheritance, -// @Configuration classes, spring-context, app starters, and the -// Application/AppServletInitializer classes) while still generating the -// full module tree, package-info files, and domain-service wiring. +// @Configuration classes, spring-context, app starters, datasource-proxy / +// spring-boot-admin-starter-client, and the Application/AppServletInitializer +// classes) while still generating the full module tree, package-info files, +// and domain-service wiring. def _testName = new File(basedir, 'project').isDirectory() ? basedir.name : 'unknown' def _projectSubdir = new File(basedir, 'project') @@ -21,6 +23,7 @@ def check = { String rel -> // tags; the prefix is verified explicitly via raw() (module's own artifactId + parent DM), and the // generated project's real Maven build — run before this script — fails on any inconsistent reference. def aid = 'spring-off-test' +def appName = 'Spring Off Test' def raw = { String rel -> new File(basedir, rel).text } @@ -49,13 +52,18 @@ modules.each { m -> } // ── 3. parent/pom.xml: no Spring inheritance; sibling-only dependencyManagement ─ +// appName-derived / are unaffected by includeSpring def parentPom = raw('parent/pom.xml') assert parentPom.contains('pom') : 'parent must have pom packaging' +assert parentPom.contains("${appName} Parent") : 'parent must be " Parent"' +assert parentPom.contains("${appName}'s parent POM.") : "parent must be \"'s parent POM.\"" assert !parentPom.contains('') : 'includeSpring=false: parent must not inherit a parent POM' assert !parentPom.contains('spring-boot') : 'includeSpring=false: no spring-boot references in parent' assert !parentPom.contains('spring-core') : 'includeSpring=false: no spring-core in parent dependencyManagement (#4)' assert !parentPom.contains('jspecify') : 'includeSpring=false: no jspecify in parent dependencies (#6)' +assert !parentPom.contains('datasource-proxy') : 'includeSpring=false: no datasource-proxy-spring-boot-starter anywhere in parent' +assert !parentPom.contains('spring-boot-admin') : 'includeSpring=false: no spring-boot-admin-starter-client anywhere in parent' def moduleOrder = modules.sort().collect { "../${it}" } def moduleBlock = parentPom.replaceAll(/(?s).*(.*?)<\/modules>.*/, '$1') @@ -77,28 +85,50 @@ assert !new File(basedir, "service/src/main/java/${p}/service/config").exists() assert !new File(basedir, "integration-db-users/src/main/java/${p}/integration/db/users/config").exists() : 'includeSpring=false: no integration .config package' assert !new File(basedir, "domain-service/src/main/java/${p}/domain/service/config").exists() : 'includeSpring=false: no domain-service .config package' +// no acceptance-test infrastructure (AT base classes, AT config, logback) when Spring is off +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/AppRestAcceptanceTestBase.java").exists() \ + : 'includeSpring=false: no AppRestAcceptanceTestBase' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/config").exists() \ + : 'includeSpring=false: no acceptance-tests .config package (no AT configuration class)' +assert !new File(basedir, 'acceptance-tests/src/test/resources/logback-spring.xml').exists() \ + : 'includeSpring=false: no logback-spring.xml' +assert !new File(basedir, "common-testing/src/main/java/${p}/common/testing/RestAcceptanceTestBase.java").exists() \ + : 'includeSpring=false: no RestAcceptanceTestBase' + // ── 5. No Spring dependencies anywhere ─────────────────────────────────────── def appPom = text('app/pom.xml') assert !appPom.contains('spring-boot') : 'includeSpring=false: app must not reference spring-boot' modules.each { m -> assert !text("${m}/pom.xml").contains('org.springframework') : "includeSpring=false: ${m} must not reference org.springframework" + assert !text("${m}/pom.xml").contains('datasource-proxy') : "includeSpring=false: ${m} must not reference datasource-proxy-spring-boot-starter" } // common-domain has no other dependencies, so without spring-context it has no block assert !text('common-domain/pom.xml').contains('') : 'includeSpring=false: common-domain must have no dependencies' +// no domain module ever depends on common-testing, regardless of includeSpring +assert !text('domain-service/pom.xml').contains('common-testing') : 'domain-service must not depend on common-testing' +// service's common-testing dep (and domain-service's validation dep) are Spring-gated, so absent here +assert !text('service/pom.xml').contains('common-testing') : 'includeSpring=false: service must not depend on common-testing' +// common-testing's own dependency on every domain module is unconditional (not Spring-gated) +assert text('common-testing/pom.xml').contains('domain-db-users') : 'common-testing missing domain-db-users dep' +assert text('common-testing/pom.xml').contains('domain-rest') : 'common-testing missing domain-rest dep' +assert text('common-testing/pom.xml').contains('domain-service') : 'common-testing missing domain-service dep' -// ── 6. Non-Spring features remain: package-info (both roots) + domain-service wiring ─ +// ── 6. Non-Spring features remain: package-info (main only) + domain-service wiring ─ check("common-domain/src/main/java/${p}/common/domain/package-info.java") -check("common-domain/src/test/java/${p}/common/domain/package-info.java") check("app/src/main/java/${p}/app/package-info.java") -check("app/src/test/java/${p}/app/package-info.java") check("domain-service/src/main/java/${p}/domain/service/package-info.java") -check("domain-service/src/test/java/${p}/domain/service/package-info.java") +assert !new File(basedir, "common-domain/src/test/java/${p}/common/domain/package-info.java").exists() : 'common-domain must not have a test-side package-info.java' +assert !new File(basedir, "app/src/test/java/${p}/app/package-info.java").exists() : 'app must not have a test-side package-info.java' +assert !new File(basedir, "domain-service/src/test/java/${p}/domain/service/package-info.java").exists() : 'domain-service must not have a test-side package-info.java' assert text('service/pom.xml').contains('domain-service') : 'service must still depend on its domain-service module' assert text('domain-service/pom.xml').contains('common-domain') : 'domain-service must depend on common-domain' assert text('integration-db-users/pom.xml').contains('domain-db-users') : 'integration-db-users must depend on its domain module' assert text('presentation-rest/pom.xml').contains('domain-rest') : 'presentation-rest must depend on domain-rest' +assert text('domain-rest/pom.xml').contains('domain-service') : 'domain-rest must depend on domain-service (the service domain deps) regardless of includeSpring' +// acceptance-tests still gets the (Spring-independent) common-testing TEST dep +assert text('acceptance-tests/pom.xml').contains('common-testing') : 'acceptance-tests must still depend on common-testing' // ── 7. src scaffolding + failsafe wiring unchanged ─────────────────────────── diff --git a/src/test/resources/projects/uppercase-types/archetype.properties b/src/test/resources/projects/uppercase-types/archetype.properties index 9fc7c7a..808e7fb 100644 --- a/src/test/resources/projects/uppercase-types/archetype.properties +++ b/src/test/resources/projects/uppercase-types/archetype.properties @@ -2,6 +2,7 @@ groupId=com.example artifactId=upper-test version=1.0.0-SNAPSHOT package=com.example.upper +appName=Upper Test integrations=DATABASE:Users,REST:Orders serviceAreas=, presentationTypes=REST diff --git a/src/test/resources/projects/uppercase-types/verify.groovy b/src/test/resources/projects/uppercase-types/verify.groovy index c3dc079..e626fc3 100644 --- a/src/test/resources/projects/uppercase-types/verify.groovy +++ b/src/test/resources/projects/uppercase-types/verify.groovy @@ -1,5 +1,5 @@ // ── Test: uppercase-types ───────────────────────────────────────────────────── -// integrations=DATABASE:Users,REST:Orders serviceAreas=, presentationTypes=REST +// appName=Upper Test integrations=DATABASE:Users,REST:Orders serviceAreas=, presentationTypes=REST // Covers: integration type and name inputs are normalised to lowercase; // 'DATABASE' is abbreviated to 'db'; all generated module names are lowercase // regardless of what case the user supplied @@ -21,6 +21,7 @@ def check = { String rel -> // tags; the prefix is verified explicitly via raw() (module's own artifactId + parent DM), and the // generated project's real Maven build — run before this script — fails on any inconsistent reference. def aid = 'upper-test' +def appName = 'Upper Test' def raw = { String rel -> new File(basedir, rel).text } @@ -56,6 +57,9 @@ modules.each { m -> assert parentPom.contains("${aid}-${m}") : "parent missing: $m" } assert parentPom.contains('pom') : "parent must have pom packaging" +// appName is used verbatim in prose (unmodified), independent of the uppercase type/name inputs +assert parentPom.contains("${appName} Parent") : 'parent must be " Parent"' +assert parentPom.contains("${appName}'s parent POM.") : "parent must be \"'s parent POM.\"" // POM XML is case-sensitive — uppercase names in the source would appear verbatim here assert !parentPom.contains('../domain-db-Users') : "'Users' must be lowercased to 'users'" @@ -88,31 +92,23 @@ modules.each { m -> : "${m}/pom.xml must declare its own artifactId as '${aid}-${m}'" } -// ── 5. Source skeletons (paths reflect lowercased names) ───────────────────── +// ── 5. Source skeletons (paths reflect lowercased names) — main only ──────── def p = 'com/example/upper' def pkg = p.replace('/', '.') [ "common-domain/src/main/java/${p}/common/domain/package-info.java", - "common-domain/src/test/java/${p}/common/domain/package-info.java", "common-testing/src/main/java/${p}/common/testing/package-info.java", + "domain-service/src/main/java/${p}/domain/service/package-info.java", "domain-db-users/src/main/java/${p}/domain/db/users/package-info.java", - "domain-db-users/src/test/java/${p}/domain/db/users/package-info.java", "integration-db-users/src/main/java/${p}/integration/db/users/package-info.java", - "integration-db-users/src/test/java/${p}/integration/db/users/package-info.java", "domain-rest-orders/src/main/java/${p}/domain/rest/orders/package-info.java", - "domain-rest-orders/src/test/java/${p}/domain/rest/orders/package-info.java", "integration-rest-orders/src/main/java/${p}/integration/rest/orders/package-info.java", - "integration-rest-orders/src/test/java/${p}/integration/rest/orders/package-info.java", "domain-rest/src/main/java/${p}/domain/rest/package-info.java", - "domain-rest/src/test/java/${p}/domain/rest/package-info.java", "presentation-rest/src/main/java/${p}/presentation/rest/package-info.java", - "presentation-rest/src/test/java/${p}/presentation/rest/package-info.java", "service/src/main/java/${p}/service/package-info.java", - "service/src/test/java/${p}/service/package-info.java", "app/src/main/java/${p}/app/package-info.java", - "app/src/test/java/${p}/app/package-info.java", - "acceptance-tests/src/test/java/${p}/at/package-info.java", + "acceptance-tests/src/main/java/${p}/at/package-info.java", ].each { check(it) } assert text("common-domain/src/main/java/${p}/common/domain/package-info.java").contains("package ${pkg}.common.domain;") : 'common-domain package-info.java has wrong package declaration' @@ -121,17 +117,19 @@ assert text("domain-db-users/src/main/java/${p}/domain/db/users/package-info.jav assert text("domain-rest-orders/src/main/java/${p}/domain/rest/orders/package-info.java").contains("package ${pkg}.domain.rest.orders;") : 'domain-rest-orders package-info.java has wrong package declaration' assert text("service/src/main/java/${p}/service/package-info.java").contains("package ${pkg}.service;") : 'service package-info.java has wrong package declaration' assert text("app/src/main/java/${p}/app/package-info.java").contains("package ${pkg}.app;") : 'app package-info.java has wrong package declaration' -assert text("acceptance-tests/src/test/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' -assert new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() : 'common-testing must have test Java package-info.java' -assert new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() : 'acceptance-tests must have main Java package-info.java' +assert text("acceptance-tests/src/main/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/package-info.java").exists() : 'acceptance-tests must not have a test-side package-info.java' // ── 6. Dependency wiring ───────────────────────────────────────────────────── assert text('domain-db-users/pom.xml').contains('common-domain') : 'domain-db-users missing common-domain dep' assert text('integration-db-users/pom.xml').contains('domain-db-users') : 'integration-db-users missing domain dep' +assert text('integration-db-users/pom.xml').contains('datasource-proxy-spring-boot-starter') \ + : 'integration-db-users (a database integration) missing datasource-proxy dep' assert text('domain-rest-orders/pom.xml').contains('common-domain') : 'domain-rest-orders missing common-domain dep' assert text('integration-rest-orders/pom.xml').contains('domain-rest-orders') : 'integration-rest-orders missing domain dep' assert text('domain-rest/pom.xml').contains('common-domain') : 'domain-rest missing common-domain dep' +assert text('domain-rest/pom.xml').contains('domain-service') : 'domain-rest missing domain-service dep (the service domain deps)' assert text('presentation-rest/pom.xml').contains('domain-rest') : 'presentation-rest missing domain-rest dep' assert text('service/pom.xml').contains('common-domain') : 'service missing common-domain dep' @@ -151,6 +149,8 @@ def atPom = text('acceptance-tests/pom.xml') assert !atPom.contains('integration-db-users') : 'acceptance-tests must not include integration-db-users' assert !atPom.contains('integration-rest-orders') : 'acceptance-tests must not include integration-rest-orders' assert !atPom.contains('service') : 'acceptance-tests must not depend on service' +assert atPom.contains('common-testing') : 'acceptance-tests missing common-testing TEST dep' +assert atPom.contains('datasource-proxy-spring-boot-starter') : 'acceptance-tests missing datasource-proxy dep' // ── src directory scaffolding — all four dirs present in every module ───────── diff --git a/src/test/resources/projects/whitespace-tolerance/archetype.properties b/src/test/resources/projects/whitespace-tolerance/archetype.properties index 70425e8..2bd2755 100644 --- a/src/test/resources/projects/whitespace-tolerance/archetype.properties +++ b/src/test/resources/projects/whitespace-tolerance/archetype.properties @@ -2,6 +2,7 @@ groupId=com.example artifactId=whitespace-test version=1.0.0-SNAPSHOT package=com.example.wstest +appName=Whitespace Test integrations=database : main , rest : orders serviceAreas=, presentationTypes=rest diff --git a/src/test/resources/projects/whitespace-tolerance/verify.groovy b/src/test/resources/projects/whitespace-tolerance/verify.groovy index 494e2e9..648df1b 100644 --- a/src/test/resources/projects/whitespace-tolerance/verify.groovy +++ b/src/test/resources/projects/whitespace-tolerance/verify.groovy @@ -1,5 +1,6 @@ // ── Test: whitespace-tolerance ──────────────────────────────────────────────── -// integrations="database : main , rest : orders" serviceAreas=, presentationTypes=rest +// appName=Whitespace Test integrations="database : main , rest : orders" serviceAreas=, +// presentationTypes=rest // Covers: spaces around ':' and ',' are trimmed; generated module names are clean; // no modules with embedded spaces or double-hyphen artifacts @@ -20,6 +21,7 @@ def check = { String rel -> // tags; the prefix is verified explicitly via raw() (module's own artifactId + parent DM), and the // generated project's real Maven build — run before this script — fails on any inconsistent reference. def aid = 'whitespace-test' +def appName = 'Whitespace Test' def raw = { String rel -> new File(basedir, rel).text } @@ -62,6 +64,8 @@ modules.each { m -> assert parentPom.contains("${aid}-${m}") : "parent missing: $m" } assert parentPom.contains('pom') : "parent must have pom packaging" +assert parentPom.contains("${appName} Parent") : 'parent must be " Parent"' +assert parentPom.contains("${appName}'s parent POM.") : "parent must be \"'s parent POM.\"" def moduleOrder = modules.sort().collect { "../${it}" } def moduleBlock = parentPom.replaceAll(/(?s).*(.*?)<\/modules>.*/, '$1') @@ -85,31 +89,22 @@ modules.each { m -> : "${m}/pom.xml must declare its own artifactId as '${aid}-${m}'" } -// ── 6. Source skeletons ─────────────────────────────────────────────────────── +// ── 6. Source skeletons — package-info.java only under src/main/java ──────── def p = 'com/example/wstest' def pkg = p.replace('/', '.') [ "common-domain/src/main/java/${p}/common/domain/package-info.java", - "common-domain/src/test/java/${p}/common/domain/package-info.java", "common-testing/src/main/java/${p}/common/testing/package-info.java", "domain-db-main/src/main/java/${p}/domain/db/main/package-info.java", - "domain-db-main/src/test/java/${p}/domain/db/main/package-info.java", "integration-db-main/src/main/java/${p}/integration/db/main/package-info.java", - "integration-db-main/src/test/java/${p}/integration/db/main/package-info.java", "domain-rest-orders/src/main/java/${p}/domain/rest/orders/package-info.java", - "domain-rest-orders/src/test/java/${p}/domain/rest/orders/package-info.java", "integration-rest-orders/src/main/java/${p}/integration/rest/orders/package-info.java", - "integration-rest-orders/src/test/java/${p}/integration/rest/orders/package-info.java", "domain-rest/src/main/java/${p}/domain/rest/package-info.java", - "domain-rest/src/test/java/${p}/domain/rest/package-info.java", "presentation-rest/src/main/java/${p}/presentation/rest/package-info.java", - "presentation-rest/src/test/java/${p}/presentation/rest/package-info.java", "service/src/main/java/${p}/service/package-info.java", - "service/src/test/java/${p}/service/package-info.java", "app/src/main/java/${p}/app/package-info.java", - "app/src/test/java/${p}/app/package-info.java", - "acceptance-tests/src/test/java/${p}/at/package-info.java", + "acceptance-tests/src/main/java/${p}/at/package-info.java", ].each { check(it) } assert text("common-domain/src/main/java/${p}/common/domain/package-info.java").contains("package ${pkg}.common.domain;") : 'common-domain package-info.java has wrong package declaration' @@ -117,17 +112,19 @@ assert text("common-testing/src/main/java/${p}/common/testing/package-info.java" assert text("domain-rest-orders/src/main/java/${p}/domain/rest/orders/package-info.java").contains("package ${pkg}.domain.rest.orders;") : 'domain-rest-orders package-info.java has wrong package declaration' assert text("service/src/main/java/${p}/service/package-info.java").contains("package ${pkg}.service;") : 'service package-info.java has wrong package declaration' assert text("app/src/main/java/${p}/app/package-info.java").contains("package ${pkg}.app;") : 'app package-info.java has wrong package declaration' -assert text("acceptance-tests/src/test/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' -assert new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() : 'common-testing must have test Java package-info.java' -assert new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() : 'acceptance-tests must have main Java package-info.java' +assert text("acceptance-tests/src/main/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration' +assert !new File(basedir, "acceptance-tests/src/test/java/${p}/at/package-info.java").exists() : 'acceptance-tests must not have a test-side package-info.java' // ── 7. Dependency wiring ───────────────────────────────────────────────────── assert text('domain-db-main/pom.xml').contains('common-domain') : 'domain-db-main missing common-domain dep' assert text('domain-rest-orders/pom.xml').contains('common-domain') : 'domain-rest-orders missing common-domain dep' assert text('integration-db-main/pom.xml').contains('domain-db-main') : 'integration-db-main missing domain dep' +assert text('integration-db-main/pom.xml').contains('datasource-proxy-spring-boot-starter') \ + : 'integration-db-main (a database integration) missing datasource-proxy dep' assert text('integration-rest-orders/pom.xml').contains('domain-rest-orders') : 'integration-rest-orders missing domain dep' assert text('domain-rest/pom.xml').contains('common-domain') : 'domain-rest missing common-domain dep' +assert text('domain-rest/pom.xml').contains('domain-service') : 'domain-rest missing domain-service dep (the service domain deps)' assert text('presentation-rest/pom.xml').contains('domain-rest') : 'presentation-rest missing domain-rest dep' assert text('service/pom.xml').contains('common-domain') : 'service missing common-domain dep' @@ -137,6 +134,10 @@ def appPom = text('app/pom.xml') assert appPom.contains("${dep}") : "app missing dep: $dep" } +def atPom = text('acceptance-tests/pom.xml') +assert atPom.contains('common-testing') : 'acceptance-tests missing common-testing TEST dep' +assert atPom.contains('datasource-proxy-spring-boot-starter') : 'acceptance-tests missing datasource-proxy dep' + // ── src directory scaffolding — all four dirs present in every module ───────── modules.each { m -> diff --git a/test-interactive.ps1 b/test-interactive.ps1 index fffbec4..383ae01 100644 --- a/test-interactive.ps1 +++ b/test-interactive.ps1 @@ -45,12 +45,12 @@ $OutDir = Join-Path $env:TEMP "archetype-test-$(Get-Random)" $AnswersFile = Join-Path $env:TEMP "archetype-answers-$(Get-Random).txt" try { - # ── 1. Install ───────────────────────────────────────────────────────────── + # -- 1. Install ------------------------------------------------------------- Write-Host "==> Installing archetype to local repo..." Invoke-Native "mvn install" { & "$ScriptDir\mvnw.cmd" -f "$ScriptDir\pom.xml" install -q } - # ── Read the archetype version straight from the project POM, so this script - # needs no maintenance when the project version changes ────────────────── + # -- Read the archetype version straight from the project POM, so this script + # needs no maintenance when the project version changes ------------------ # Use cmd /c with a single command string: PowerShell 5.1 mangles arguments # like "-Dexpression=project.version" when passing them to the mvnw.cmd batch # file, so let cmd.exe parse the line (same pattern as the generate step). @@ -61,7 +61,7 @@ try { $ArchetypeVersion = "$ArchetypeVersion".Trim() Write-Host "==> Using archetype version: $ArchetypeVersion" - # ── 2. Write answers to a temp file ─────────────────────────────────────── + # -- 2. Write answers to a temp file --------------------------------------- # Answers fed to each Maven prompt, in the exact order maven-archetype-plugin # 3.4.x prompts. Required properties WITHOUT a non-empty default are prompted # first, in archetype-metadata.xml order, THEN groupId / artifactId / package. @@ -69,12 +69,14 @@ try { # line here: version (1.0.0-SNAPSHOT), serviceAreas (",") and presentationTypes # (rest). The "," default makes serviceAreas resolve to a single "service" module. # + # appName -> My Service # integrations -> database:main # groupId -> com.example # artifactId -> my-service # package -> (Enter: accept default derived from groupId) # confirm -> Y $answers = @( + "My Service", "database:main", "com.example", "my-service", @@ -86,7 +88,7 @@ try { $utf8NoBom = New-Object System.Text.UTF8Encoding $false [System.IO.File]::WriteAllText($AnswersFile, ($answers -join "`n") + "`n", $utf8NoBom) - # ── 3. Generate ──────────────────────────────────────────────────────────── + # -- 3. Generate ------------------------------------------------------------ New-Item -ItemType Directory -Path $OutDir | Out-Null Write-Host "==> Generating project in $OutDir..." @@ -94,7 +96,7 @@ try { cmd /c "cd /d `"$OutDir`" && mvn archetype:generate --no-transfer-progress -DarchetypeGroupId=$ArchetypeGroupId -DarchetypeArtifactId=$ArchetypeArtifactId -DarchetypeVersion=$ArchetypeVersion < `"$AnswersFile`"" } - # ── 4. Show generated layout ─────────────────────────────────────────────── + # -- 4. Show generated layout ----------------------------------------------- Write-Host "" Write-Host "==> Generated project layout:" $projectDir = Join-Path $OutDir "my-service" @@ -102,7 +104,7 @@ try { Sort-Object FullName | ForEach-Object { " " + $_.FullName.Replace($OutDir + "\", "") } - # ── 5. Optionally verify the generated project compiles and tests pass ───── + # -- 5. Optionally verify the generated project compiles and tests pass ----- if ($Verify) { Write-Host "" Write-Host "==> Running mvn verify on generated project..." diff --git a/test-interactive.sh b/test-interactive.sh index b3fc2c5..f73235f 100644 --- a/test-interactive.sh +++ b/test-interactive.sh @@ -50,12 +50,14 @@ cd "$OUT_DIR" # line here: version (1.0.0-SNAPSHOT), serviceAreas (",") and presentationTypes # (rest). The "," default makes serviceAreas resolve to a single "service" module. # +# appName → My Service # integrations → database:main # groupId → com.example # artifactId → my-service # package → (Enter: accept default derived from groupId) # confirm → Y printf '%s\n' \ + "My Service" \ "database:main" \ "com.example" \ "my-service" \