diff --git a/aws/aws-rds-hello-world/src/main/java/io/reflectoring/awshelloworld/HelloWorldController.java b/aws/aws-rds-hello-world/src/main/java/io/reflectoring/awshelloworld/HelloWorldController.java index 304dec2..ccaef3c 100644 --- a/aws/aws-rds-hello-world/src/main/java/io/reflectoring/awshelloworld/HelloWorldController.java +++ b/aws/aws-rds-hello-world/src/main/java/io/reflectoring/awshelloworld/HelloWorldController.java @@ -4,16 +4,16 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController -public class HelloWorldController { +class HelloWorldController { private final UserRepository userRepository; - public HelloWorldController(UserRepository userRepository) { + HelloWorldController(UserRepository userRepository) { this.userRepository = userRepository; } @GetMapping("/hello") - public String helloWorld(){ + String helloWorld(){ Iterable users = userRepository.findAll(); diff --git a/aws/cloudformation/rds-in-private-subnet/database.yml b/aws/cloudformation/rds-in-private-subnet/database.yml index f7784a8..c68f6fe 100644 --- a/aws/cloudformation/rds-in-private-subnet/database.yml +++ b/aws/cloudformation/rds-in-private-subnet/database.yml @@ -25,6 +25,7 @@ Resources: SecretStringTemplate: !Join ['', ['{"username": "', !Ref 'DBUsername' ,'"}']] GenerateStringKey: "password" PasswordLength: 32 + ExcludeCharacters: '"@/\' DBSubnetGroup: Type: AWS::RDS::DBSubnetGroup @@ -75,6 +76,11 @@ Outputs: Value: !GetAtt 'PostgresInstance.Endpoint.Port' Export: Name: !Join [ ':', [ !Ref 'AWS::StackName', 'EndpointPort' ] ] + DBName: + Description: The name of the database that is created within the PostgreSQL instance. + Value: !Ref DBName + Export: + Name: !Join [ ':', [ !Ref 'AWS::StackName', 'DBName' ] ] Secret: Description: Reference to the secret containing the password to the database. Value: !Ref 'Secret' diff --git a/aws/cloudformation/rds-in-private-subnet/rds-in-private-subnet.drawio b/aws/cloudformation/rds-in-private-subnet/rds-in-private-subnet.drawio index b896d1b..e2d0bd4 100644 --- a/aws/cloudformation/rds-in-private-subnet/rds-in-private-subnet.drawio +++ b/aws/cloudformation/rds-in-private-subnet/rds-in-private-subnet.drawio @@ -1 +1 @@ -7VpZb9s4EP41fqyhW/JjfKUBWqyx3t22TwYtMTIRWXRp+sqv36FEXaR8pHESGIhhIJohRQ5nvm84pNOxB8v9PUOrxXca4aRjGdG+Yw87lmXargt/hOaQa3zfyRUxI5HsVCmm5BlLpSG1GxLhdaMjpzThZNVUhjRNccgbOsQY3TW7PdKkOesKxVhTTEOU6NofJOILqTW9XtXwFZN4IacOLD9vWKKis1zJeoEiuqup7FHHHjBKef603A9wIpxX+CV/b3yktTSM4ZRf8sICh9b++cH/MXyeP3nO9+j3bP6lMG6Lko1c8X+TgTSYHwovrChJeeZJtw9feGlgdFxoGQipa7mKQpX9psLUJTFGU6HKflNhqsObyvymamBNoUmN4Q1lfqNmIHztPt3whKR4UGLOAGXMUEQgFgOaUAa6lKbgvf6CLxOQTHjcLQjH0xUKhVd3wBfQPdKUS9SbViFLx4t3ADUr8bzcx4JgXbRbO92Y0c0qm/IBcN/aOtuuQvE6Z/QJFyZ1LNtygsB0xEQkSRRTt5hxAtC/S0gsRuVUTIKklOBHLkYE+0kaf8ukoW1Im2tT3N31/X4A+gitFziS7tHhKhEsZsX7mkrC9x7TJebsAF2KVk+iVeaSQIq7ipieI3WLGiet4j0kk0FcDl3xBR4kZV5AH1tjz2QzT0go2LOZp5h/Mun2mbTG4YYRfphVnacZraS5l3EM9KPe2B551yNaOc+1iWa7TaKZls40M2hhmumZGULehGymr7FtNJiCYorZlgAUVK61pDgtVu6dNwi8ug/NowFSEaWEoxxKCeQ1AhIoAXFbAmK3BcQyup71RgFxtXh0LC8RS57DQywe7lYrSIeIEyBX0caKxm8URboW3IfSELOiBUwrh9NC3Jo/GnQoQvkNzXEyoWuS2WIP55RzujxLxhCCBrY0sk9LJrG7qFrpLIGFzebFOrQ8MHYD13aOJ7orAMZyFMD4QTcI9O2yp0PGfyu4BOfh8iCcnW2bHlqKMKTz9SrziIqSe8TxDh1uDCRErm8WS/PbseG9KTbKRFHDhu9+LDZ6R1L7P2j9dFk81b3RtsZifs2Pwsd+MDLq/BsSBgPlIU8pEx5Q4zI03AFsQC07yGP2eQdEnStw0HqVu+OR7IUd7RUPw2u6YSHO650+iG2VDw7X1wGbYypgayna/ZaNy3+rkt3Sa3YNYDiKceFZAQca0xQlo0pbDwtOoztx0SCCm9DwSaiSeSYXMc5ggxgv+kkuw5tjkpQJQytNHLfvCZhC2ZlGJbIgFuzwsy78EgJU2VIc7uuNw0NdmmBGwI8CWqeLxRwkp/woKQvrijE/n/aFT08Cpl7KGDL6DCewo26bdzFtkJDDTcTpqgJfr4k9KJuaI+SLlC/Vr06Uccr6Vw5kGwo6cydoA2UALZf4Csx6n5i9ALNnoeh+HBQt41pYVI4CtvnOWGwr424Ki3vCfxZzw3MNiSBVQBTCoSZcL3W6F8K193FwtY1eE64GnCPtP0OsU/wGUCDWMbpG7WNehF+IvSj3y27yIu34Cpx2wh2z8kx/eMgtuC6Z9Lr31JnoJo861ssPOeVPJdX1WvHji32l87FalfptRyC/13X10tR1Xl+a7pPNeIb/2n51Dr9+8/m/z/h+88XUwDBhZAsnxc/r5M/r5I+9TtaY1cK/o2Rzex98ndzKNut86v17KO4gHlIoPlJxv3xTCZhFax0z1si3s9/mTl4vvSranuM0o+07ZRDrudXS4x2cqFNeFetbP/O3FoJ/Vny2HNuP0+N9ak1TqTUtp7yrfnGtqWzs2Vi92kcZ98XF5uXHKRCrf+7Iu1f/ImOP/gc= \ No newline at end of file +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 \ No newline at end of file diff --git a/aws/cloudformation/rds-in-private-subnet/rds-in-private-subnet.svg b/aws/cloudformation/rds-in-private-subnet/rds-in-private-subnet.svg deleted file mode 100644 index d49a192..0000000 --- a/aws/cloudformation/rds-in-private-subnet/rds-in-private-subnet.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
VPC
VPC
Public subnet
Public subnet
ECS Service
Application
Load
Balancer
Application...
Internet 
Gateway
Internet...
ECS Task
ECS Task
Internet
Internet
Private subnet
Private subnet
RDS Instance
RDS Instance
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/aws/cloudformation/rds-in-private-subnet/service.yml b/aws/cloudformation/rds-in-private-subnet/service.yml index 4393811..fc40462 100644 --- a/aws/cloudformation/rds-in-private-subnet/service.yml +++ b/aws/cloudformation/rds-in-private-subnet/service.yml @@ -9,12 +9,6 @@ Parameters: DatabaseStackName: Type: String Description: The name of the database stack with the database this service should connect to. - DBName: - Type: String - Description: The database name. - DBUsername: - Type: String - Description: The database username. ServiceName: Type: String Description: A human-readable name for the service. @@ -117,9 +111,13 @@ Resources: - ':' - Fn::ImportValue: !Join [':', [!Ref 'DatabaseStackName', 'EndpointPort']] - '/' - - !Ref 'DBName' + - Fn::ImportValue: !Join [':', [!Ref 'DatabaseStackName', 'DBName']] - Name: SPRING_DATASOURCE_USERNAME - Value: !Ref 'DBUsername' + Value: !Join + - '' + - - '{{resolve:secretsmanager:' + - Fn::ImportValue: !Join [':', [!Ref 'DatabaseStackName', 'Secret']] + - ':SecretString:username}}' - Name: SPRING_DATASOURCE_PASSWORD Value: !Join - '' diff --git a/spring-boot/configuration/build.gradle b/spring-boot/configuration/build.gradle index 54562d1..1d7d91a 100644 --- a/spring-boot/configuration/build.gradle +++ b/spring-boot/configuration/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.springframework.boot' version '2.1.3.RELEASE' + id 'org.springframework.boot' version '2.3.0.RELEASE' id 'java' } @@ -7,7 +7,7 @@ apply plugin: 'io.spring.dependency-management' group = 'io.reflectoring' version = '0.0.1-SNAPSHOT' -sourceCompatibility = '11' +sourceCompatibility = '13' repositories { mavenCentral() @@ -23,6 +23,7 @@ dependencies { testImplementation('org.junit.jupiter:junit-jupiter:5.4.0') testImplementation('org.springframework.boot:spring-boot-starter-test'){ exclude group: 'junit', module: 'junit' + exclude group: 'org.junit.vintage' } } diff --git a/spring-boot/configuration/gradle/wrapper/gradle-wrapper.properties b/spring-boot/configuration/gradle/wrapper/gradle-wrapper.properties index 44e7c4d..21e622d 100644 --- a/spring-boot/configuration/gradle/wrapper/gradle-wrapper.properties +++ b/spring-boot/configuration/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/spring-boot/configuration/gradlew b/spring-boot/configuration/gradlew old mode 100644 new mode 100755 diff --git a/spring-boot/configuration/src/main/java/io/reflectoring/Application.java b/spring-boot/configuration/src/main/java/io/reflectoring/Application.java new file mode 100644 index 0000000..0c90412 --- /dev/null +++ b/spring-boot/configuration/src/main/java/io/reflectoring/Application.java @@ -0,0 +1,14 @@ +package io.reflectoring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.PropertySource; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/spring-boot/configuration/src/main/java/io/reflectoring/validation/AppConfiguration.java b/spring-boot/configuration/src/main/java/io/reflectoring/validation/AppConfiguration.java new file mode 100644 index 0000000..729847a --- /dev/null +++ b/spring-boot/configuration/src/main/java/io/reflectoring/validation/AppConfiguration.java @@ -0,0 +1,26 @@ +package io.reflectoring.validation; + +import io.reflectoring.validation.thirdparty.ThirdPartyComponentProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; + +@Configuration +@EnableConfigurationProperties(AppProperties.class) +class AppConfiguration { + + @Bean + public static ReportEmailAddressValidator configurationPropertiesValidator() { + return new ReportEmailAddressValidator(); + } + + @Bean + @Validated + @ConfigurationProperties(prefix = "app.third-party.properties") + public ThirdPartyComponentProperties thirdPartyComponentProperties() { + return new ThirdPartyComponentProperties(); + } + +} diff --git a/spring-boot/configuration/src/main/java/io/reflectoring/validation/AppProperties.java b/spring-boot/configuration/src/main/java/io/reflectoring/validation/AppProperties.java new file mode 100644 index 0000000..18c2eec --- /dev/null +++ b/spring-boot/configuration/src/main/java/io/reflectoring/validation/AppProperties.java @@ -0,0 +1,53 @@ +package io.reflectoring.validation; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; + +@Validated +@ConfigurationProperties(prefix = "app.properties") +class AppProperties implements Validator { + + @NotBlank + private String name; + + @Valid + private ReportProperties report; + + private static final String APP_BASE_NAME = "Application"; + + public boolean supports(Class clazz) { + return AppProperties.class.isAssignableFrom(clazz); + } + + public void validate(Object target, Errors errors) { + + AppProperties appProperties = (AppProperties) target; + if (!appProperties.getName().endsWith(APP_BASE_NAME)) { + errors.rejectValue("name", "field.name.malformed", + new Object[]{APP_BASE_NAME}, + "The application name must contain [" + APP_BASE_NAME + "] base name"); + } + + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public ReportProperties getReport() { + return report; + } + + public void setReport(ReportProperties report) { + this.report = report; + } +} \ No newline at end of file diff --git a/spring-boot/configuration/src/main/java/io/reflectoring/validation/ReportEmailAddressValidator.java b/spring-boot/configuration/src/main/java/io/reflectoring/validation/ReportEmailAddressValidator.java new file mode 100644 index 0000000..430702c --- /dev/null +++ b/spring-boot/configuration/src/main/java/io/reflectoring/validation/ReportEmailAddressValidator.java @@ -0,0 +1,27 @@ +package io.reflectoring.validation; + +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; + +class ReportEmailAddressValidator implements Validator { + + private static final String EMAIL_DOMAIN = "@analysisapp.com"; + + public boolean supports(Class clazz) { + return ReportProperties.class.isAssignableFrom(clazz); + } + + public void validate(Object target, Errors errors) { + + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "emailAddress", "field.required"); + + ReportProperties reportProperties = (ReportProperties) target; + if (!reportProperties.getEmailAddress().endsWith(EMAIL_DOMAIN)) { + errors.rejectValue("emailAddress", "field.domain.required", + new Object[]{EMAIL_DOMAIN}, + "The email address must contain [" + EMAIL_DOMAIN + "] domain"); + } + + } +} \ No newline at end of file diff --git a/spring-boot/configuration/src/main/java/io/reflectoring/validation/ReportProperties.java b/spring-boot/configuration/src/main/java/io/reflectoring/validation/ReportProperties.java new file mode 100644 index 0000000..a594335 --- /dev/null +++ b/spring-boot/configuration/src/main/java/io/reflectoring/validation/ReportProperties.java @@ -0,0 +1,51 @@ +package io.reflectoring.validation; + +import javax.validation.constraints.Email; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; + +class ReportProperties { + + private Boolean sendEmails = Boolean.FALSE; + + private ReportType type = ReportType.HTML; + + @Min(value = 7) + @Max(value = 30) + private Integer intervalInDays; + + @Email + private String emailAddress; + + public Boolean getSendEmails() { + return sendEmails; + } + + public void setSendEmails(Boolean sendEmails) { + this.sendEmails = sendEmails; + } + + public ReportType getType() { + return type; + } + + public void setType(ReportType type) { + this.type = type; + } + + public Integer getIntervalInDays() { + return intervalInDays; + } + + public void setIntervalInDays(Integer intervalInDays) { + this.intervalInDays = intervalInDays; + } + + public String getEmailAddress() { + return emailAddress; + } + + public void setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + } +} diff --git a/spring-boot/configuration/src/main/java/io/reflectoring/validation/ReportType.java b/spring-boot/configuration/src/main/java/io/reflectoring/validation/ReportType.java new file mode 100644 index 0000000..7ed75b7 --- /dev/null +++ b/spring-boot/configuration/src/main/java/io/reflectoring/validation/ReportType.java @@ -0,0 +1,5 @@ +package io.reflectoring.validation; + +enum ReportType { + HTML, PLAIN_TEXT +} diff --git a/spring-boot/configuration/src/main/java/io/reflectoring/validation/thirdparty/ThirdPartyComponentProperties.java b/spring-boot/configuration/src/main/java/io/reflectoring/validation/thirdparty/ThirdPartyComponentProperties.java new file mode 100644 index 0000000..768842b --- /dev/null +++ b/spring-boot/configuration/src/main/java/io/reflectoring/validation/thirdparty/ThirdPartyComponentProperties.java @@ -0,0 +1,21 @@ +package io.reflectoring.validation.thirdparty; + +import javax.validation.constraints.NotBlank; + +/** + * We assume that this bean comes from another jar file + */ +public class ThirdPartyComponentProperties { + + @NotBlank + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/spring-boot/configuration/src/main/resources/application-validation.properties b/spring-boot/configuration/src/main/resources/application-validation.properties new file mode 100644 index 0000000..30926fc --- /dev/null +++ b/spring-boot/configuration/src/main/resources/application-validation.properties @@ -0,0 +1,7 @@ +app.properties.name=Analysis Application +app.properties.report.send-emails=true +app.properties.report.type=PLAIN_TEXT +app.properties.report.interval-in-days=14 +app.properties.report.email-address=manager@analysisapp.com +# third-party component properties +app.third-party.properties.name=Third Party! \ No newline at end of file diff --git a/spring-boot/configuration/src/main/resources/application.properties b/spring-boot/configuration/src/main/resources/application.properties index bc88f39..0dc5de8 100644 --- a/spring-boot/configuration/src/main/resources/application.properties +++ b/spring-boot/configuration/src/main/resources/application.properties @@ -1,5 +1,4 @@ myapp.mail.enabled=true -myapp myapp.mail.pauseBetweenMails=5s myapp.mail.maxAttachmentSize=1MB myapp.mail.smtpServers[0]=server1 diff --git a/spring-boot/configuration/src/main/java/io/reflectoring/configuration/ConfigurationApplication.java b/spring-boot/configuration/src/test/java/io/reflectoring/configuration/ConfigurationApplication.java similarity index 100% rename from spring-boot/configuration/src/main/java/io/reflectoring/configuration/ConfigurationApplication.java rename to spring-boot/configuration/src/test/java/io/reflectoring/configuration/ConfigurationApplication.java diff --git a/spring-boot/configuration/src/test/java/io/reflectoring/configuration/mail/MailModuleTestWithAllProperties.java b/spring-boot/configuration/src/test/java/io/reflectoring/configuration/mail/MailModuleTestWithAllProperties.java index 731d8f6..f9b6c65 100644 --- a/spring-boot/configuration/src/test/java/io/reflectoring/configuration/mail/MailModuleTestWithAllProperties.java +++ b/spring-boot/configuration/src/test/java/io/reflectoring/configuration/mail/MailModuleTestWithAllProperties.java @@ -1,37 +1,36 @@ package io.reflectoring.configuration.mail; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.util.unit.DataSize; + import java.time.Duration; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.util.unit.DataSize; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(properties = { - "myapp.mail.enabled=asd", - "myapp.mail.defaultSubject=hello", - "myapp.mail.pauseBetweenMails=5s", - "myapp.mail.maxAttachmentSize=1MB", - "myapp.mail.smtpServers[0]=server1", - "myapp.mail.smtpServers[1]=server2", - "myapp.mail.maxAttachmentWeight=5kg" + "myapp.mail.enabled=asd", + "myapp.mail.defaultSubject=hello", + "myapp.mail.pauseBetweenMails=5s", + "myapp.mail.maxAttachmentSize=1MB", + "myapp.mail.smtpServers[0]=server1", + "myapp.mail.smtpServers[1]=server2", + "myapp.mail.maxAttachmentWeight=5kg" }) class MailModuleTestWithAllProperties { - @Autowired(required = false) - private MailModuleProperties mailModuleProperties; + @Autowired(required = false) + private MailModuleProperties mailModuleProperties; - @Test - void propertiesAreLoaded() { - assertThat(mailModuleProperties).isNotNull(); - assertThat(mailModuleProperties.getDefaultSubject()).isEqualTo("hello"); - assertThat(mailModuleProperties.getEnabled()).isTrue(); - assertThat(mailModuleProperties.getPauseBetweenMails()).isEqualByComparingTo(Duration.ofSeconds(5)); - assertThat(mailModuleProperties.getMaxAttachmentSize()).isEqualByComparingTo(DataSize.ofMegabytes(1)); - assertThat(mailModuleProperties.getSmtpServers()).hasSize(2); - assertThat(mailModuleProperties.getMaxAttachmentWeight().getGrams()).isEqualTo(5000L); - } + @Test + void propertiesAreLoaded() { + assertThat(mailModuleProperties).isNotNull(); + assertThat(mailModuleProperties.getDefaultSubject()).isEqualTo("hello"); + assertThat(mailModuleProperties.getEnabled()).isTrue(); + assertThat(mailModuleProperties.getPauseBetweenMails()).isEqualByComparingTo(Duration.ofSeconds(5)); + assertThat(mailModuleProperties.getMaxAttachmentSize()).isEqualByComparingTo(DataSize.ofMegabytes(1)); + assertThat(mailModuleProperties.getSmtpServers()).hasSize(2); + assertThat(mailModuleProperties.getMaxAttachmentWeight().getGrams()).isEqualTo(5000L); + } } \ No newline at end of file diff --git a/spring-boot/configuration/src/test/java/io/reflectoring/validation/PropertiesInvalidInputTest.java b/spring-boot/configuration/src/test/java/io/reflectoring/validation/PropertiesInvalidInputTest.java new file mode 100644 index 0000000..a3b1be1 --- /dev/null +++ b/spring-boot/configuration/src/test/java/io/reflectoring/validation/PropertiesInvalidInputTest.java @@ -0,0 +1,127 @@ +package io.reflectoring.validation; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesBindException; +import org.springframework.boot.context.properties.bind.validation.BindValidationException; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.env.StandardEnvironment; + +import java.util.Properties; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * We create Spring Application dynamically to catch and test application context startup exceptions + */ +class PropertiesInvalidInputTest { + + SpringApplication application; + Properties properties; + + @BeforeEach + void setup() { + // create Spring Application dynamically + application = new SpringApplication(ValidationApplication.class); + + // setting test properties for our Spring Application + properties = new Properties(); + + ConfigurableEnvironment environment = new StandardEnvironment(); + MutablePropertySources propertySources = environment.getPropertySources(); + propertySources.addFirst(new PropertiesPropertySource("application-test", properties)); + application.setEnvironment(environment); + } + + @Test + void whenGivenNameEmpty_thenNotBlankValidationFails() { + + properties.put("app.properties.name", ""); + + assertThatThrownBy(application::run) + .isInstanceOf(ConfigurationPropertiesBindException.class) + .hasRootCauseInstanceOf(BindValidationException.class) + .hasStackTraceContaining("Field error in object 'app.properties' on field 'name'") + .hasStackTraceContaining("[must not be blank]"); + + } + + @Test + void whenGivenNameDoesNotContainBaseName_thenCustomAppPropertiesValidatorFails() { + + properties.put("app.properties.name", "My App"); + + assertThatThrownBy(application::run) + .isInstanceOf(ConfigurationPropertiesBindException.class) + .hasRootCauseInstanceOf(BindValidationException.class) + .hasStackTraceContaining("Field error in object 'app.properties' on field 'name'") + .hasStackTraceContaining("[The application name must contain [Application] base name]"); + + } + + @Test + void whenGivenReportIntervalInDaysMoreThan30_thenMaxValidationFails() { + + properties.put("app.properties.report.interval-in-days", "31"); + + assertThatThrownBy(application::run) + .isInstanceOf(ConfigurationPropertiesBindException.class) + .hasRootCauseInstanceOf(BindValidationException.class) + .hasStackTraceContaining("rejected value [31]"); + + } + + @Test + void whenGivenReportIntervalInDaysLessThan7_thenMinValidationFails() { + + properties.put("app.properties.report.interval-in-days", "6"); + + assertThatThrownBy(application::run) + .isInstanceOf(ConfigurationPropertiesBindException.class) + .hasRootCauseInstanceOf(BindValidationException.class) + .hasStackTraceContaining("rejected value [6]"); + + } + + @Test + void whenGivenReportEmailAddressIsNotWellFormed_thenEmailValidationFails() { + + properties.put("app.properties.report.email-address", "manager.analysisapp.com"); + + assertThatThrownBy(application::run) + .isInstanceOf(ConfigurationPropertiesBindException.class) + .hasRootCauseInstanceOf(BindValidationException.class) + .hasStackTraceContaining("rejected value [manager.analysisapp.com]"); + + } + + @Test + void whenGivenReportEmailAddressDoesNotContainAnalysisappDomain_thenCustomEmailValidatorFails() { + + properties.put("app.properties.report.email-address", "manager@notanalysisapp.com"); + + assertThatThrownBy(application::run) + .isInstanceOf(ConfigurationPropertiesBindException.class) + .hasRootCauseInstanceOf(BindValidationException.class) + .hasStackTraceContaining("Field error in object 'app.properties.report' on field 'emailAddress'") + .hasStackTraceContaining("[The email address must contain [@analysisapp.com] domain]"); + + } + + @Test + void whenGivenThirdPartyComponentNameIsEmpty_thenNotBlankValidationFails() { + + properties.put("app.third-party.properties.name", ""); + + assertThatThrownBy(application::run) + .isInstanceOf(ConfigurationPropertiesBindException.class) + .hasRootCauseInstanceOf(BindValidationException.class) + .hasStackTraceContaining("Field error in object 'app.third-party.properties' on field 'name'") + .hasStackTraceContaining("[must not be blank]"); + + } + +} diff --git a/spring-boot/configuration/src/test/java/io/reflectoring/validation/PropertiesValidInputTest.java b/spring-boot/configuration/src/test/java/io/reflectoring/validation/PropertiesValidInputTest.java new file mode 100644 index 0000000..0ca1cef --- /dev/null +++ b/spring-boot/configuration/src/test/java/io/reflectoring/validation/PropertiesValidInputTest.java @@ -0,0 +1,43 @@ +package io.reflectoring.validation; + +import io.reflectoring.validation.thirdparty.ThirdPartyComponentProperties; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(properties = { + "app.properties.name=My Test Application", + "app.properties.report.send-emails=true", + "app.properties.report.type=PLAIN_TEXT", + "app.properties.report.interval-in-days=14", + "app.properties.report.email-address=manager@analysisapp.com", + "app.third-party.properties.name=Third Party!" +}, classes = {AppConfiguration.class}) +class PropertiesValidInputTest { + + @Autowired + AppProperties appProperties; + + @Autowired + ThirdPartyComponentProperties thirdPartyComponentProperties; + + @Test + void appPropertiesAreLoaded() { + assertThat(appProperties).isNotNull(); + assertThat(appProperties.getName()).isEqualTo("My Test Application"); + assertThat(appProperties.getReport()).isNotNull(); + assertThat(appProperties.getReport().getSendEmails()).isTrue(); + assertThat(appProperties.getReport().getType()).isEqualTo(ReportType.PLAIN_TEXT); + assertThat(appProperties.getReport().getIntervalInDays()).isEqualTo(14); + assertThat(appProperties.getReport().getEmailAddress()).isEqualTo("manager@analysisapp.com"); + } + + @Test + void thirdPartyComponentPropertiesAreLoaded() { + assertThat(thirdPartyComponentProperties).isNotNull(); + assertThat(thirdPartyComponentProperties.getName()).isEqualTo("Third Party!"); + } + +} diff --git a/spring-boot/configuration/src/test/java/io/reflectoring/validation/ValidationApplication.java b/spring-boot/configuration/src/test/java/io/reflectoring/validation/ValidationApplication.java new file mode 100644 index 0000000..15d63a5 --- /dev/null +++ b/spring-boot/configuration/src/test/java/io/reflectoring/validation/ValidationApplication.java @@ -0,0 +1,15 @@ +package io.reflectoring.validation; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.PropertySource; + +@SpringBootApplication +@PropertySource("classpath:application-validation.properties") +public class ValidationApplication { + + public static void main(String[] args) { + SpringApplication.run(ValidationApplication.class, args); + } + +}