From db246159f8b2843e94b2cf23211b8ce461acb36a Mon Sep 17 00:00:00 2001 From: Wim Deblauwe Date: Sat, 24 Feb 2024 11:35:51 +0100 Subject: [PATCH 1/2] Update for upgrade of book 2.0.0 (Spring Boot 3.2.2) --- chapter02/01 - Generated project/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + chapter02/01 - Generated project/mvnw | 219 ++++++--- chapter02/01 - Generated project/mvnw.cmd | 92 +++- chapter02/01 - Generated project/pom.xml | 30 +- .../src/main/resources/application.properties | 1 + .../application/ApplicationTests.java | 9 +- chapter02/02 - Logging config/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + chapter02/02 - Logging config/mvnw | 219 ++++++--- chapter02/02 - Logging config/mvnw.cmd | 92 +++- chapter02/02 - Logging config/pom.xml | 34 +- .../application/ApplicationTests.java | 9 +- chapter03/01 - Generated project/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + chapter03/01 - Generated project/mvnw | 219 ++++++--- chapter03/01 - Generated project/mvnw.cmd | 92 +++- chapter03/01 - Generated project/pom.xml | 39 +- .../example/copsboot/CopsbootApplication.java | 1 + .../src/main/resources/application.properties | 1 + .../copsboot/CopsbootApplicationTests.java | 9 +- chapter04/01 - User domain/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + chapter04/01 - User domain/mvnw | 219 ++++++--- chapter04/01 - User domain/mvnw.cmd | 92 +++- chapter04/01 - User domain/pom.xml | 41 +- .../example/copsboot/CopsbootApplication.java | 1 + .../src/main/resources/application.properties | 1 + .../copsboot/CopsbootApplicationTests.java | 9 +- chapter04/02 - User with JPA/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + chapter04/02 - User with JPA/mvnw | 219 ++++++--- chapter04/02 - User with JPA/mvnw.cmd | 92 +++- chapter04/02 - User with JPA/pom.xml | 133 +++--- .../example/copsboot/CopsbootApplication.java | 1 + .../java/com/example/copsboot/user/User.java | 10 +- .../src/main/resources/application.properties | 1 + .../copsboot/CopsbootApplicationTests.java | 9 +- .../copsboot/user/UserRepositoryTest.java | 18 +- .../03 - User with JPA refactored/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + chapter04/03 - User with JPA refactored/mvnw | 219 ++++++--- .../03 - User with JPA refactored/mvnw.cmd | 92 +++- .../03 - User with JPA refactored/pom.xml | 149 +++--- .../example/copsboot/CopsbootApplication.java | 11 - .../CopsbootApplicationConfiguration.java | 16 + .../java/com/example/copsboot/user/User.java | 10 +- .../example/copsboot/user/UserRepository.java | 2 +- .../com/example/orm/jpa/AbstractEntity.java | 4 +- .../com/example/orm/jpa/AbstractEntityId.java | 2 +- .../src/main/resources/application.properties | 1 + .../copsboot/CopsbootApplicationTests.java | 9 +- .../copsboot/user/UserRepositoryTest.java | 6 +- chapter05/01 - Oauth2/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + chapter05/01 - Oauth2/docker-compose.yaml | 14 + chapter05/01 - Oauth2/mvnw | 219 ++++++--- chapter05/01 - Oauth2/mvnw.cmd | 92 +++- chapter05/01 - Oauth2/pom.xml | 161 +++---- .../example/copsboot/CopsbootApplication.java | 33 +- .../CopsbootApplicationConfiguration.java | 18 + .../copsboot/DevelopmentDbInitializer.java | 30 -- .../security/ApplicationUserDetails.java | 32 -- .../ApplicationUserDetailsService.java | 31 -- .../security/OAuth2ServerConfiguration.java | 104 ----- .../security/WebSecurityConfiguration.java | 24 + .../java/com/example/copsboot/user/User.java | 10 +- .../example/copsboot/user/UserRepository.java | 5 +- .../example/copsboot/user/UserService.java | 5 - .../copsboot/user/UserServiceImpl.java | 23 - .../copsboot/user/web/UserRestController.java | 19 + .../com/example/orm/jpa/AbstractEntity.java | 4 +- .../com/example/orm/jpa/AbstractEntityId.java | 2 +- .../main/java/com/example/orm/jpa/Entity.java | 1 + .../src/main/resources/application.properties | 1 + .../copsboot/CopsbootApplicationTests.java | 9 +- .../ApplicationUserDetailsServiceTest.java | 48 -- .../OAuth2ServerConfigurationTest.java | 64 --- .../copsboot/user/UserRepositoryTest.java | 45 +- .../java/com/example/copsboot/user/Users.java | 51 -- .../user/web/UserRestControllerTest.java | 35 ++ chapter05/02 - Oauth configurable/mvnw | 225 --------- chapter05/02 - Oauth configurable/pom.xml | 95 ---- .../example/copsboot/CopsbootApplication.java | 40 -- .../copsboot/DevelopmentDbInitializer.java | 30 -- .../security/ApplicationUserDetails.java | 32 -- .../ApplicationUserDetailsService.java | 31 -- .../security/OAuth2ServerConfiguration.java | 107 ----- .../security/SecurityConfiguration.java | 27 -- .../java/com/example/copsboot/user/User.java | 53 --- .../example/copsboot/user/UserRepository.java | 11 - .../example/copsboot/user/UserService.java | 5 - .../copsboot/user/UserServiceImpl.java | 23 - .../main/java/com/example/orm/jpa/Entity.java | 11 - .../main/resources/application-dev.properties | 2 - .../src/main/resources/application.properties | 0 .../copsboot/CopsbootApplicationTests.java | 19 - .../ApplicationUserDetailsServiceTest.java | 52 --- .../OAuth2ServerConfigurationTest.java | 64 --- .../copsboot/user/UserRepositoryTest.java | 83 ---- .../java/com/example/copsboot/user/Users.java | 51 -- .../resources/application-test.properties | 2 - .../01 - User rest controller/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + .../docker-compose.yaml | 14 + chapter06/01 - User rest controller/mvnw | 219 ++++++--- chapter06/01 - User rest controller/mvnw.cmd | 92 +++- chapter06/01 - User rest controller/pom.xml | 168 +++---- .../example/copsboot/CopsbootApplication.java | 33 +- .../CopsbootApplicationConfiguration.java | 18 + .../copsboot/DevelopmentDbInitializer.java | 30 -- .../security/ApplicationUserDetails.java | 32 -- .../ApplicationUserDetailsService.java | 31 -- .../security/OAuth2ServerConfiguration.java | 108 ----- .../security/SecurityConfiguration.java | 27 -- .../security/WebSecurityConfiguration.java | 24 + .../example/copsboot/user/AuthServerId.java | 11 + .../user/AuthServerIdAttributeConverter.java | 19 + .../copsboot/user/CreateUserParameters.java | 4 + .../java/com/example/copsboot/user/User.java | 38 +- .../copsboot/user/UserNotFoundException.java | 11 - .../example/copsboot/user/UserRepository.java | 4 +- .../example/copsboot/user/UserService.java | 25 +- .../copsboot/user/UserServiceImpl.java | 30 -- .../copsboot/user/web/CreateUserRequest.java | 16 + .../example/copsboot/user/web/UserDto.java | 19 +- .../copsboot/user/web/UserRestController.java | 54 ++- .../com/example/orm/jpa/AbstractEntity.java | 4 +- .../com/example/orm/jpa/AbstractEntityId.java | 2 +- .../main/java/com/example/orm/jpa/Entity.java | 1 + .../main/resources/application-dev.properties | 2 - .../src/main/resources/application.properties | 1 + .../copsboot/CopsbootApplicationTests.java | 16 +- .../ApplicationUserDetailsServiceTest.java | 52 --- .../OAuth2ServerConfigurationTest.java | 64 --- .../security/SecurityHelperForMockMvc.java | 57 --- .../security/StubUserDetailsService.java | 21 - .../copsboot/user/UserRepositoryTest.java | 53 +-- .../java/com/example/copsboot/user/Users.java | 51 -- .../user/web/UserRestControllerTest.java | 99 +--- .../resources/application-test.properties | 2 - chapter06/02 - Post mapping/mvnw | 225 --------- chapter06/02 - Post mapping/mvnw.cmd | 143 ------ chapter06/02 - Post mapping/pom.xml | 101 ---- .../example/copsboot/CopsbootApplication.java | 40 -- .../copsboot/DevelopmentDbInitializer.java | 30 -- .../infrastructure/SpringProfiles.java | 12 - .../security/ApplicationUserDetails.java | 32 -- .../ApplicationUserDetailsService.java | 31 -- .../security/OAuth2ServerConfiguration.java | 105 ----- .../security/SecurityConfiguration.java | 27 -- .../java/com/example/copsboot/user/User.java | 53 --- .../com/example/copsboot/user/UserId.java | 16 - .../copsboot/user/UserNotFoundException.java | 11 - .../copsboot/user/UserRepositoryCustom.java | 5 - .../copsboot/user/UserRepositoryImpl.java | 18 - .../com/example/copsboot/user/UserRole.java | 7 - .../example/copsboot/user/UserService.java | 9 - .../copsboot/user/UserServiceImpl.java | 30 -- .../user/web/CreateOfficerParameters.java | 18 - .../example/copsboot/user/web/UserDto.java | 21 - .../copsboot/user/web/UserRestController.java | 41 -- .../com/example/orm/jpa/AbstractEntity.java | 64 --- .../com/example/orm/jpa/AbstractEntityId.java | 59 --- .../java/com/example/orm/jpa/EntityId.java | 15 - .../orm/jpa/InMemoryUniqueIdGenerator.java | 10 - .../example/orm/jpa/UniqueIdGenerator.java | 5 - .../example/util/ArtifactForFramework.java | 4 - .../main/resources/application-dev.properties | 2 - .../src/main/resources/application.properties | 0 .../src/main/resources/logback-spring.xml | 17 - .../copsboot/CopsbootApplicationTests.java | 19 - .../ApplicationUserDetailsServiceTest.java | 52 --- .../OAuth2ServerConfigurationTest.java | 64 --- .../security/SecurityHelperForMockMvc.java | 57 --- .../security/StubUserDetailsService.java | 21 - .../copsboot/user/UserRepositoryTest.java | 83 ---- .../java/com/example/copsboot/user/Users.java | 55 --- .../user/web/UserRestControllerTest.java | 128 ----- .../resources/application-test.properties | 2 - .../src/test/resources/logback-test.xml | 24 - chapter06/02 - user role/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + chapter06/02 - user role/docker-compose.yaml | 14 + chapter06/02 - user role/mvnw | 308 ++++++++++++ .../02 - user role}/mvnw.cmd | 92 +++- chapter06/02 - user role/pom.xml | 96 ++++ .../example/copsboot/CopsbootApplication.java | 13 + .../CopsbootApplicationConfiguration.java | 18 + .../infrastructure/SpringProfiles.java | 0 .../security/WebSecurityConfiguration.java | 19 + .../example/copsboot/user/AuthServerId.java | 11 + .../user/AuthServerIdAttributeConverter.java | 19 + .../copsboot/user/CreateUserParameters.java | 4 + .../java/com/example/copsboot/user/User.java | 37 ++ .../com/example/copsboot/user/UserId.java | 0 .../example/copsboot/user/UserRepository.java | 4 +- .../copsboot/user/UserRepositoryCustom.java | 0 .../copsboot/user/UserRepositoryImpl.java | 0 .../com/example/copsboot/user/UserRole.java | 0 .../example/copsboot/user/UserService.java | 28 ++ .../copsboot/user/web/CreateUserRequest.java | 16 + .../example/copsboot/user/web/UserDto.java | 14 + .../copsboot/user/web/UserRestController.java | 52 +++ .../com/example/orm/jpa/AbstractEntity.java | 4 +- .../com/example/orm/jpa/AbstractEntityId.java | 2 +- .../main/java/com/example/orm/jpa/Entity.java | 1 + .../java/com/example/orm/jpa/EntityId.java | 0 .../orm/jpa/InMemoryUniqueIdGenerator.java | 0 .../example/orm/jpa/UniqueIdGenerator.java | 0 .../example/util/ArtifactForFramework.java | 0 .../src/main/resources/application.properties | 3 + .../src/main/resources/logback-spring.xml | 0 .../copsboot/CopsbootApplicationTests.java | 13 + .../copsboot/user/UserRepositoryTest.java | 46 ++ .../user/web/UserRestControllerTest.java | 87 ++++ .../src/test/resources/logback-test.xml | 0 .../03 - Writing API Documentation/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + .../docker-compose.yaml | 14 + chapter06/03 - Writing API Documentation/mvnw | 219 ++++++--- .../03 - Writing API Documentation/mvnw.cmd | 92 +++- .../03 - Writing API Documentation/pom.xml | 363 +++++++-------- .../asciidoc/Copsboot REST API Guide.adoc | 0 .../example/copsboot/CopsbootApplication.java | 33 +- .../CopsbootApplicationConfiguration.java | 18 + .../copsboot/DevelopmentDbInitializer.java | 30 -- .../security/ApplicationUserDetails.java | 32 -- .../ApplicationUserDetailsService.java | 31 -- .../security/OAuth2ServerConfiguration.java | 105 ----- .../security/SecurityConfiguration.java | 27 -- .../security/WebSecurityConfiguration.java | 19 + .../example/copsboot/user/AuthServerId.java | 11 + .../user/AuthServerIdAttributeConverter.java | 19 + .../copsboot/user/CreateUserParameters.java | 4 + .../java/com/example/copsboot/user/User.java | 38 +- .../copsboot/user/UserNotFoundException.java | 11 - .../example/copsboot/user/UserRepository.java | 4 +- .../example/copsboot/user/UserService.java | 25 +- .../copsboot/user/UserServiceImpl.java | 30 -- .../user/web/CreateOfficerParameters.java | 18 - .../copsboot/user/web/CreateUserRequest.java | 16 + .../example/copsboot/user/web/UserDto.java | 19 +- .../copsboot/user/web/UserRestController.java | 55 ++- .../com/example/orm/jpa/AbstractEntity.java | 4 +- .../com/example/orm/jpa/AbstractEntityId.java | 2 +- .../main/java/com/example/orm/jpa/Entity.java | 1 + .../main/resources/application-dev.properties | 2 - .../src/main/resources/application.properties | 3 + .../copsboot/CopsbootApplicationTests.java | 16 +- .../ApplicationUserDetailsServiceTest.java | 52 --- .../OAuth2ServerConfigurationTest.java | 64 --- .../security/SecurityHelperForMockMvc.java | 57 --- .../security/StubUserDetailsService.java | 21 - .../copsboot/user/UserRepositoryTest.java | 53 +-- .../java/com/example/copsboot/user/Users.java | 55 --- .../user/web/UserRestControllerTest.java | 147 +++--- .../resources/application-test.properties | 2 - chapter06/04 - Generating snippets/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + .../docker-compose.yaml | 14 + chapter06/04 - Generating snippets/mvnw | 219 ++++++--- chapter06/04 - Generating snippets/mvnw.cmd | 92 +++- chapter06/04 - Generating snippets/pom.xml | 377 +++++++-------- .../asciidoc/Copsboot REST API Guide.adoc | 2 +- .../src/{main => docs}/asciidoc/_users.adoc | 8 +- .../example/copsboot/CopsbootApplication.java | 33 +- .../CopsbootApplicationConfiguration.java | 18 + .../copsboot/DevelopmentDbInitializer.java | 30 -- .../json/EntityIdJsonSerializer.java | 19 - .../security/ApplicationUserDetails.java | 32 -- .../ApplicationUserDetailsService.java | 31 -- .../security/OAuth2ServerConfiguration.java | 105 ----- .../security/SecurityConfiguration.java | 27 -- .../security/WebSecurityConfiguration.java | 19 + .../example/copsboot/user/AuthServerId.java | 11 + .../user/AuthServerIdAttributeConverter.java | 19 + .../copsboot/user/CreateUserParameters.java | 4 + .../java/com/example/copsboot/user/User.java | 38 +- .../copsboot/user/UserNotFoundException.java | 11 - .../example/copsboot/user/UserRepository.java | 4 +- .../example/copsboot/user/UserService.java | 25 +- .../copsboot/user/UserServiceImpl.java | 30 -- .../user/web/CreateOfficerParameters.java | 18 - .../copsboot/user/web/CreateUserRequest.java | 16 + .../example/copsboot/user/web/UserDto.java | 19 +- .../copsboot/user/web/UserRestController.java | 55 ++- .../com/example/orm/jpa/AbstractEntity.java | 4 +- .../com/example/orm/jpa/AbstractEntityId.java | 2 +- .../main/java/com/example/orm/jpa/Entity.java | 1 + .../main/resources/application-dev.properties | 2 - .../src/main/resources/application.properties | 3 + .../copsboot/CopsbootApplicationTests.java | 16 +- .../ApplicationUserDetailsServiceTest.java | 52 --- .../OAuth2ServerConfigurationTest.java | 64 --- .../security/SecurityHelperForMockMvc.java | 58 --- .../security/StubUserDetailsService.java | 21 - .../copsboot/user/UserRepositoryTest.java | 53 +-- .../java/com/example/copsboot/user/Users.java | 55 --- .../web/UserRestControllerDocumentation.java | 182 +++----- .../user/web/UserRestControllerTest.java | 146 +++--- .../resources/application-test.properties | 2 - .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + .../05 - Refactoring/docker-compose.yaml | 14 + chapter06/05 - Refactoring/mvnw | 219 ++++++--- chapter06/05 - Refactoring/mvnw.cmd | 92 +++- chapter06/05 - Refactoring/pom.xml | 377 +++++++-------- .../asciidoc/Copsboot REST API Guide.adoc | 2 +- .../src/{main => docs}/asciidoc/_users.adoc | 8 +- .../example/copsboot/CopsbootApplication.java | 33 +- .../CopsbootApplicationConfiguration.java | 18 + .../copsboot/DevelopmentDbInitializer.java | 30 -- .../json/EntityIdJsonSerializer.java | 19 - .../security/ApplicationUserDetails.java | 32 -- .../ApplicationUserDetailsService.java | 31 -- .../security/OAuth2ServerConfiguration.java | 105 ----- .../security/SecurityConfiguration.java | 27 -- .../security/WebSecurityConfiguration.java | 19 + .../example/copsboot/user/AuthServerId.java | 11 + .../user/AuthServerIdAttributeConverter.java | 19 + .../copsboot/user/CreateUserParameters.java | 4 + .../java/com/example/copsboot/user/User.java | 38 +- .../copsboot/user/UserNotFoundException.java | 11 - .../example/copsboot/user/UserRepository.java | 4 +- .../example/copsboot/user/UserService.java | 25 +- .../copsboot/user/UserServiceImpl.java | 30 -- .../user/web/CreateOfficerParameters.java | 18 - .../copsboot/user/web/CreateUserRequest.java | 16 + .../example/copsboot/user/web/UserDto.java | 19 +- .../copsboot/user/web/UserRestController.java | 55 ++- .../com/example/orm/jpa/AbstractEntity.java | 4 +- .../com/example/orm/jpa/AbstractEntityId.java | 2 +- .../main/java/com/example/orm/jpa/Entity.java | 1 - .../main/resources/application-dev.properties | 2 - .../src/main/resources/application.properties | 3 + .../copsboot/CopsbootApplicationTests.java | 16 +- .../ApplicationUserDetailsServiceTest.java | 52 --- .../OAuth2ServerConfigurationTest.java | 64 --- .../security/SecurityHelperForMockMvc.java | 58 --- .../security/StubUserDetailsService.java | 21 - .../CopsbootControllerDocumentationTest.java | 30 ++ ...trollerDocumentationTestConfiguration.java | 21 + .../test/CopsbootControllerTest.java | 25 +- .../CopsbootControllerTestConfiguration.java | 26 -- .../copsboot/user/UserRepositoryTest.java | 53 +-- .../java/com/example/copsboot/user/Users.java | 55 --- .../web/UserRestControllerDocumentation.java | 175 ++++--- .../user/web/UserRestControllerTest.java | 117 +++-- .../resources/application-test.properties | 2 - .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + chapter07/01 - postgresql/docker-compose.yaml | 20 + chapter07/01 - postgresql/mvnw | 219 ++++++--- chapter07/01 - postgresql/mvnw.cmd | 92 +++- chapter07/01 - postgresql/pom.xml | 388 +++++++--------- .../asciidoc/Copsboot REST API Guide.adoc | 2 +- .../src/{main => docs}/asciidoc/_users.adoc | 8 +- .../example/copsboot/CopsbootApplication.java | 33 +- .../CopsbootApplicationConfiguration.java | 18 + .../copsboot/DevelopmentDbInitializer.java | 30 -- .../json/EntityIdJsonSerializer.java | 19 - .../security/ApplicationUserDetails.java | 32 -- .../ApplicationUserDetailsService.java | 31 -- .../security/OAuth2ServerConfiguration.java | 105 ----- .../security/SecurityConfiguration.java | 27 -- .../security/WebSecurityConfiguration.java | 19 + .../example/copsboot/user/AuthServerId.java | 11 + .../user/AuthServerIdAttributeConverter.java | 19 + .../copsboot/user/CreateUserParameters.java | 4 + .../java/com/example/copsboot/user/User.java | 38 +- .../copsboot/user/UserNotFoundException.java | 11 - .../example/copsboot/user/UserRepository.java | 4 +- .../example/copsboot/user/UserService.java | 25 +- .../copsboot/user/UserServiceImpl.java | 30 -- .../user/web/CreateOfficerParameters.java | 18 - .../copsboot/user/web/CreateUserRequest.java | 16 + .../example/copsboot/user/web/UserDto.java | 19 +- .../copsboot/user/web/UserRestController.java | 55 ++- .../com/example/orm/jpa/AbstractEntity.java | 4 +- .../com/example/orm/jpa/AbstractEntityId.java | 2 +- .../main/java/com/example/orm/jpa/Entity.java | 1 - .../main/resources/application-dev.properties | 5 - .../resources/application-local.properties | 15 +- .../src/main/resources/application.properties | 3 + .../db/migration/V1.0.0.1__users.sql | 7 + .../migration/h2/V1.0.0.1__authentication.sql | 42 -- .../postgresql/V1.0.0.1__authentication.sql | 52 --- .../migration/postgresql/V1.0.0.2__users.sql | 16 - .../copsboot/CopsbootApplicationTests.java | 16 +- .../ApplicationUserDetailsServiceTest.java | 52 --- .../OAuth2ServerConfigurationTest.java | 64 --- .../security/SecurityHelperForMockMvc.java | 58 --- .../security/StubUserDetailsService.java | 21 - .../CopsbootControllerDocumentationTest.java | 30 ++ ...trollerDocumentationTestConfiguration.java | 21 + .../test/CopsbootControllerTest.java | 25 +- .../CopsbootControllerTestConfiguration.java | 26 -- .../copsboot/user/UserRepositoryTest.java | 56 +-- .../java/com/example/copsboot/user/Users.java | 55 --- .../web/UserRestControllerDocumentation.java | 147 +++--- .../user/web/UserRestControllerTest.java | 117 +++-- .../resources/application-test.properties | 5 - .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + .../02 - testcontainers/docker-compose.yaml | 20 + chapter07/02 - testcontainers/mvnw | 219 ++++++--- chapter07/02 - testcontainers/mvnw.cmd | 92 +++- chapter07/02 - testcontainers/pom.xml | 415 ++++++++--------- .../asciidoc/Copsboot REST API Guide.adoc | 2 +- .../src/{main => docs}/asciidoc/_users.adoc | 8 +- .../example/copsboot/CopsbootApplication.java | 33 +- .../CopsbootApplicationConfiguration.java | 18 + .../copsboot/DevelopmentDbInitializer.java | 30 -- .../infrastructure/SpringProfiles.java | 1 + .../json/EntityIdJsonSerializer.java | 19 - .../security/ApplicationUserDetails.java | 32 -- .../ApplicationUserDetailsService.java | 31 -- .../security/OAuth2ServerConfiguration.java | 105 ----- .../security/SecurityConfiguration.java | 27 -- .../security/WebSecurityConfiguration.java | 19 + .../example/copsboot/user/AuthServerId.java | 11 + .../user/AuthServerIdAttributeConverter.java | 19 + .../copsboot/user/CreateUserParameters.java | 4 + .../java/com/example/copsboot/user/User.java | 38 +- .../copsboot/user/UserNotFoundException.java | 11 - .../example/copsboot/user/UserRepository.java | 4 +- .../example/copsboot/user/UserService.java | 25 +- .../copsboot/user/UserServiceImpl.java | 30 -- .../user/web/CreateOfficerParameters.java | 18 - .../copsboot/user/web/CreateUserRequest.java | 16 + .../example/copsboot/user/web/UserDto.java | 19 +- .../copsboot/user/web/UserRestController.java | 55 ++- .../com/example/orm/jpa/AbstractEntity.java | 4 +- .../com/example/orm/jpa/AbstractEntityId.java | 2 +- .../main/java/com/example/orm/jpa/Entity.java | 1 - .../main/resources/application-dev.properties | 5 - .../resources/application-local.properties | 14 +- .../src/main/resources/application.properties | 3 + .../db/migration/V1.0.0.1__users.sql | 7 + .../migration/h2/V1.0.0.1__authentication.sql | 42 -- .../postgresql/V1.0.0.1__authentication.sql | 52 --- .../migration/postgresql/V1.0.0.2__users.sql | 16 - .../copsboot/CopsbootApplicationTests.java | 15 +- .../ApplicationUserDetailsServiceTest.java | 52 --- .../OAuth2ServerConfigurationTest.java | 64 --- .../security/SecurityHelperForMockMvc.java | 58 --- .../security/StubUserDetailsService.java | 21 - .../CopsbootControllerDocumentationTest.java | 30 ++ ...trollerDocumentationTestConfiguration.java | 21 + .../test/CopsbootControllerTest.java | 25 +- .../CopsbootControllerTestConfiguration.java | 26 -- .../user/UserRepositoryIntegrationTest.java | 49 -- .../copsboot/user/UserRepositoryTest.java | 69 +-- .../java/com/example/copsboot/user/Users.java | 55 --- .../web/UserRestControllerDocumentation.java | 147 +++--- .../user/web/UserRestControllerTest.java | 117 +++-- .../application-integration-test.properties | 9 +- .../application-repository-test.properties | 6 + .../resources/application-test.properties | 5 - .../src/test/resources/logback-test.xml | 10 +- .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + chapter08/01 - builtin/docker-compose.yaml | 20 + chapter08/01 - builtin/mvnw | 219 ++++++--- chapter08/01 - builtin/mvnw.cmd | 92 +++- chapter08/01 - builtin/pom.xml | 415 ++++++++--------- .../asciidoc/Copsboot REST API Guide.adoc | 14 + .../src/docs/asciidoc/_users.adoc | 24 + .../asciidoc/Copsboot REST API Guide.adoc | 14 - .../src/main/asciidoc/_users.adoc | 24 - .../example/copsboot/CopsbootApplication.java | 33 +- .../CopsbootApplicationConfiguration.java | 18 + .../copsboot/DevelopmentDbInitializer.java | 30 -- .../infrastructure/SpringProfiles.java | 1 + .../json/EntityIdJsonSerializer.java | 19 - .../mvc/FieldErrorResponse.java | 9 +- .../security/ApplicationUserDetails.java | 32 -- .../ApplicationUserDetailsService.java | 31 -- .../security/OAuth2ServerConfiguration.java | 105 ----- .../security/SecurityConfiguration.java | 27 -- .../security/WebSecurityConfiguration.java | 19 + .../example/copsboot/user/AuthServerId.java | 11 + .../user/AuthServerIdAttributeConverter.java | 19 + .../copsboot/user/CreateUserParameters.java | 4 + .../java/com/example/copsboot/user/User.java | 38 +- .../copsboot/user/UserNotFoundException.java | 11 - .../example/copsboot/user/UserRepository.java | 4 +- .../example/copsboot/user/UserService.java | 25 +- .../copsboot/user/UserServiceImpl.java | 30 -- .../user/web/CreateOfficerParameters.java | 18 - .../copsboot/user/web/CreateUserRequest.java | 17 + .../example/copsboot/user/web/UserDto.java | 19 +- .../copsboot/user/web/UserRestController.java | 58 +-- .../com/example/orm/jpa/AbstractEntity.java | 4 +- .../com/example/orm/jpa/AbstractEntityId.java | 2 +- .../main/java/com/example/orm/jpa/Entity.java | 1 - .../main/resources/application-dev.properties | 5 - .../resources/application-local.properties | 14 +- .../src/main/resources/application.properties | 5 + .../db/migration/V1.0.0.1__users.sql | 7 + .../migration/h2/V1.0.0.1__authentication.sql | 42 -- .../postgresql/V1.0.0.1__authentication.sql | 52 --- .../migration/postgresql/V1.0.0.2__users.sql | 16 - .../copsboot/CopsbootApplicationTests.java | 15 +- .../ApplicationUserDetailsServiceTest.java | 52 --- .../OAuth2ServerConfigurationTest.java | 64 --- .../security/SecurityHelperForMockMvc.java | 58 --- .../security/StubUserDetailsService.java | 21 - .../CopsbootControllerDocumentationTest.java | 30 ++ ...trollerDocumentationTestConfiguration.java | 21 + .../test/CopsbootControllerTest.java | 25 +- .../CopsbootControllerTestConfiguration.java | 26 -- .../user/UserRepositoryIntegrationTest.java | 49 -- .../copsboot/user/UserRepositoryTest.java | 69 +-- .../java/com/example/copsboot/user/Users.java | 55 --- .../web/UserRestControllerDocumentation.java | 147 +++--- .../user/web/UserRestControllerTest.java | 154 +++--- ...trollerWithResponseBodyValidationTest.java | 53 --- .../application-integration-test.properties | 9 +- .../application-repository-test.properties | 6 + .../resources/application-test.properties | 5 - .../src/test/resources/logback-test.xml | 10 +- .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + .../02 - customfield/docker-compose.yaml | 20 + chapter08/02 - customfield/mvnw | 219 ++++++--- chapter08/02 - customfield/mvnw.cmd | 92 +++- chapter08/02 - customfield/pom.xml | 415 ++++++++--------- .../asciidoc/Copsboot REST API Guide.adoc | 14 + .../src/docs/asciidoc/_users.adoc | 24 + .../asciidoc/Copsboot REST API Guide.adoc | 14 - .../src/main/asciidoc/_users.adoc | 24 - .../example/copsboot/CopsbootApplication.java | 33 +- .../CopsbootApplicationConfiguration.java | 18 + .../copsboot/DevelopmentDbInitializer.java | 30 -- .../infrastructure/SpringProfiles.java | 1 + .../json/EntityIdJsonSerializer.java | 19 - .../mvc/FieldErrorResponse.java | 9 +- .../security/ApplicationUserDetails.java | 32 -- .../ApplicationUserDetailsService.java | 31 -- .../security/OAuth2ServerConfiguration.java | 105 ----- .../security/SecurityConfiguration.java | 27 -- .../security/WebSecurityConfiguration.java | 19 + .../report/CreateReportParameters.java | 8 + .../com/example/copsboot/report/Report.java | 25 +- .../copsboot/report/ReportService.java | 20 +- .../copsboot/report/ReportServiceImpl.java | 30 -- .../report/web/CreateReportParameters.java | 17 - .../report/web/CreateReportRequest.java | 12 + .../copsboot/report/web/ReportDto.java | 22 +- .../report/web/ReportRestController.java | 27 +- .../example/copsboot/user/AuthServerId.java | 11 + .../user/AuthServerIdAttributeConverter.java | 19 + .../copsboot/user/CreateUserParameters.java | 4 + .../java/com/example/copsboot/user/User.java | 38 +- .../user/UserIdAttributeConverter.java | 19 + .../copsboot/user/UserNotFoundException.java | 8 +- .../example/copsboot/user/UserRepository.java | 4 +- .../example/copsboot/user/UserService.java | 30 +- .../copsboot/user/UserServiceImpl.java | 30 -- .../user/web/CreateOfficerParameters.java | 18 - .../copsboot/user/web/CreateUserRequest.java | 17 + .../example/copsboot/user/web/UserDto.java | 19 +- .../copsboot/user/web/UserRestController.java | 58 +-- .../com/example/orm/jpa/AbstractEntity.java | 4 +- .../com/example/orm/jpa/AbstractEntityId.java | 2 +- .../main/java/com/example/orm/jpa/Entity.java | 1 - .../main/resources/application-dev.properties | 5 - .../resources/application-local.properties | 14 +- .../src/main/resources/application.properties | 5 + .../db/migration/V1.0.0.1__users.sql | 7 + .../db/migration/V1.0.0.2__reports.sql | 8 + .../migration/h2/V1.0.0.1__authentication.sql | 42 -- .../postgresql/V1.0.0.1__authentication.sql | 52 --- .../migration/postgresql/V1.0.0.2__users.sql | 16 - .../copsboot/CopsbootApplicationTests.java | 15 +- .../ApplicationUserDetailsServiceTest.java | 52 --- .../OAuth2ServerConfigurationTest.java | 64 --- .../security/SecurityHelperForMockMvc.java | 58 --- .../security/StubUserDetailsService.java | 21 - .../CopsbootControllerDocumentationTest.java | 30 ++ ...trollerDocumentationTestConfiguration.java | 21 + .../test/CopsbootControllerTest.java | 25 +- .../CopsbootControllerTestConfiguration.java | 26 -- .../report/web/ReportRestControllerTest.java | 77 +-- .../user/UserRepositoryIntegrationTest.java | 49 -- .../copsboot/user/UserRepositoryTest.java | 69 +-- .../java/com/example/copsboot/user/Users.java | 55 --- .../web/UserRestControllerDocumentation.java | 147 +++--- .../user/web/UserRestControllerTest.java | 152 +++--- .../application-integration-test.properties | 9 +- .../application-repository-test.properties | 6 + .../resources/application-test.properties | 5 - .../src/test/resources/logback-test.xml | 10 +- .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + .../03 - customfieldfinal/docker-compose.yaml | 20 + chapter08/03 - customfieldfinal/mvnw | 219 ++++++--- chapter08/03 - customfieldfinal/mvnw.cmd | 92 +++- chapter08/03 - customfieldfinal/pom.xml | 415 ++++++++--------- .../asciidoc/Copsboot REST API Guide.adoc | 14 + .../src/docs/asciidoc/_users.adoc | 24 + .../asciidoc/Copsboot REST API Guide.adoc | 14 - .../src/main/asciidoc/_users.adoc | 24 - .../example/copsboot/CopsbootApplication.java | 33 +- .../CopsbootApplicationConfiguration.java | 18 + .../copsboot/DevelopmentDbInitializer.java | 30 -- .../infrastructure/SpringProfiles.java | 1 + .../json/EntityIdJsonSerializer.java | 19 - .../mvc/FieldErrorResponse.java | 9 +- .../security/ApplicationUserDetails.java | 32 -- .../ApplicationUserDetailsService.java | 31 -- .../security/OAuth2ServerConfiguration.java | 105 ----- .../security/SecurityConfiguration.java | 27 -- .../security/WebSecurityConfiguration.java | 19 + .../report/CreateReportParameters.java | 8 + .../com/example/copsboot/report/Report.java | 24 +- .../copsboot/report/ReportService.java | 20 +- .../copsboot/report/ReportServiceImpl.java | 30 -- .../report/web/CreateReportParameters.java | 19 - .../report/web/CreateReportRequest.java | 12 + .../web/ReportDescriptionValidator.java | 4 +- .../copsboot/report/web/ReportDto.java | 22 +- .../report/web/ReportRestController.java | 27 +- .../report/web/ValidReportDescription.java | 6 +- .../example/copsboot/user/AuthServerId.java | 11 + .../user/AuthServerIdAttributeConverter.java | 19 + .../copsboot/user/CreateUserParameters.java | 4 + .../java/com/example/copsboot/user/User.java | 38 +- .../user/UserIdAttributeConverter.java | 19 + .../copsboot/user/UserNotFoundException.java | 8 +- .../example/copsboot/user/UserRepository.java | 4 +- .../example/copsboot/user/UserService.java | 30 +- .../copsboot/user/UserServiceImpl.java | 30 -- .../user/web/CreateOfficerParameters.java | 18 - .../copsboot/user/web/CreateUserRequest.java | 17 + .../example/copsboot/user/web/UserDto.java | 19 +- .../copsboot/user/web/UserRestController.java | 58 +-- .../com/example/orm/jpa/AbstractEntity.java | 4 +- .../com/example/orm/jpa/AbstractEntityId.java | 2 +- .../main/java/com/example/orm/jpa/Entity.java | 1 - .../main/resources/application-dev.properties | 5 - .../resources/application-local.properties | 14 +- .../src/main/resources/application.properties | 5 + .../db/migration/V1.0.0.1__users.sql | 7 + .../db/migration/V1.0.0.2__reports.sql | 8 + .../migration/h2/V1.0.0.1__authentication.sql | 42 -- .../postgresql/V1.0.0.1__authentication.sql | 52 --- .../migration/postgresql/V1.0.0.2__users.sql | 16 - .../copsboot/CopsbootApplicationTests.java | 15 +- .../ApplicationUserDetailsServiceTest.java | 52 --- .../OAuth2ServerConfigurationTest.java | 64 --- .../security/SecurityHelperForMockMvc.java | 58 --- .../security/StubUserDetailsService.java | 21 - .../CopsbootControllerDocumentationTest.java | 30 ++ ...trollerDocumentationTestConfiguration.java | 21 + .../test/CopsbootControllerTest.java | 25 +- .../CopsbootControllerTestConfiguration.java | 26 -- .../com/example/copsboot/report/Reports.java | 15 - .../web/ReportDescriptionValidatorTest.java | 38 +- .../report/web/ReportRestControllerTest.java | 77 +-- .../user/UserRepositoryIntegrationTest.java | 49 -- .../copsboot/user/UserRepositoryTest.java | 69 +-- .../java/com/example/copsboot/user/Users.java | 55 --- .../web/UserRestControllerDocumentation.java | 147 +++--- .../user/web/UserRestControllerTest.java | 152 +++--- .../test/ConstraintViolationSetAssert.java | 15 +- .../application-integration-test.properties | 9 +- .../application-repository-test.properties | 6 + .../resources/application-test.properties | 5 - .../src/test/resources/logback-test.xml | 10 +- .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + .../04 - objectvalidation/docker-compose.yaml | 20 + chapter08/04 - objectvalidation/mvnw | 219 ++++++--- chapter08/04 - objectvalidation/mvnw.cmd | 92 +++- chapter08/04 - objectvalidation/pom.xml | 415 ++++++++--------- .../asciidoc/Copsboot REST API Guide.adoc | 14 + .../src/docs/asciidoc/_users.adoc | 24 + .../asciidoc/Copsboot REST API Guide.adoc | 14 - .../src/main/asciidoc/_users.adoc | 24 - .../example/copsboot/CopsbootApplication.java | 33 +- .../CopsbootApplicationConfiguration.java | 18 + .../copsboot/DevelopmentDbInitializer.java | 30 -- .../infrastructure/SpringProfiles.java | 1 + .../json/EntityIdJsonSerializer.java | 19 - .../mvc/FieldErrorResponse.java | 9 +- .../security/ApplicationUserDetails.java | 32 -- .../ApplicationUserDetailsService.java | 31 -- .../security/OAuth2ServerConfiguration.java | 105 ----- .../security/SecurityConfiguration.java | 27 -- .../security/WebSecurityConfiguration.java | 19 + .../report/CreateReportParameters.java | 8 + .../com/example/copsboot/report/Report.java | 24 +- .../copsboot/report/ReportService.java | 20 +- .../copsboot/report/ReportServiceImpl.java | 30 -- .../report/web/CreateReportParameters.java | 23 - .../web/CreateReportParametersValidator.java | 21 - .../report/web/CreateReportRequest.java | 16 + .../web/CreateReportRequestValidator.java | 21 + .../web/ReportDescriptionValidator.java | 4 +- .../copsboot/report/web/ReportDto.java | 22 +- .../report/web/ReportRestController.java | 27 +- ...ers.java => ValidCreateReportRequest.java} | 10 +- .../report/web/ValidReportDescription.java | 6 +- .../example/copsboot/user/AuthServerId.java | 11 + .../user/AuthServerIdAttributeConverter.java | 19 + .../copsboot/user/CreateUserParameters.java | 4 + .../java/com/example/copsboot/user/User.java | 38 +- .../user/UserIdAttributeConverter.java | 19 + .../copsboot/user/UserNotFoundException.java | 8 +- .../example/copsboot/user/UserRepository.java | 4 +- .../example/copsboot/user/UserService.java | 30 +- .../copsboot/user/UserServiceImpl.java | 30 -- .../user/web/CreateOfficerParameters.java | 18 - .../copsboot/user/web/CreateUserRequest.java | 17 + .../example/copsboot/user/web/UserDto.java | 19 +- .../copsboot/user/web/UserRestController.java | 58 +-- .../com/example/orm/jpa/AbstractEntity.java | 4 +- .../com/example/orm/jpa/AbstractEntityId.java | 2 +- .../main/java/com/example/orm/jpa/Entity.java | 1 - .../main/resources/application-dev.properties | 5 - .../resources/application-local.properties | 14 +- .../src/main/resources/application.properties | 5 + .../db/migration/V1.0.0.1__users.sql | 7 + .../db/migration/V1.0.0.2__reports.sql | 8 + .../migration/h2/V1.0.0.1__authentication.sql | 42 -- .../postgresql/V1.0.0.1__authentication.sql | 52 --- .../migration/postgresql/V1.0.0.2__users.sql | 16 - .../copsboot/CopsbootApplicationTests.java | 15 +- .../ApplicationUserDetailsServiceTest.java | 52 --- .../OAuth2ServerConfigurationTest.java | 64 --- .../security/SecurityHelperForMockMvc.java | 58 --- .../security/StubUserDetailsService.java | 21 - .../CopsbootControllerDocumentationTest.java | 30 ++ ...trollerDocumentationTestConfiguration.java | 21 + .../test/CopsbootControllerTest.java | 25 +- .../CopsbootControllerTestConfiguration.java | 26 -- .../com/example/copsboot/report/Reports.java | 15 - .../CreateReportParametersValidatorTest.java | 60 --- .../web/CreateReportRequestValidatorTest.java | 63 +++ .../web/ReportDescriptionValidatorTest.java | 39 +- .../report/web/ReportRestControllerTest.java | 79 ++-- .../user/UserRepositoryIntegrationTest.java | 49 -- .../copsboot/user/UserRepositoryTest.java | 69 +-- .../java/com/example/copsboot/user/Users.java | 55 --- .../web/UserRestControllerDocumentation.java | 147 +++--- .../user/web/UserRestControllerTest.java | 152 +++--- .../test/ConstraintViolationSetAssert.java | 15 +- .../application-integration-test.properties | 9 +- .../application-repository-test.properties | 6 + .../resources/application-test.properties | 5 - .../src/test/resources/logback-test.xml | 10 +- .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + .../docker-compose.yaml | 20 + chapter08/05 - validatorspringbean/mvnw | 219 ++++++--- chapter08/05 - validatorspringbean/mvnw.cmd | 92 +++- chapter08/05 - validatorspringbean/pom.xml | 415 ++++++++--------- .../asciidoc/Copsboot REST API Guide.adoc | 14 + .../src/docs/asciidoc/_users.adoc | 24 + .../asciidoc/Copsboot REST API Guide.adoc | 14 - .../src/main/asciidoc/_users.adoc | 24 - .../example/copsboot/CopsbootApplication.java | 33 +- .../CopsbootApplicationConfiguration.java | 18 + .../copsboot/DevelopmentDbInitializer.java | 30 -- .../infrastructure/SpringProfiles.java | 1 + .../json/EntityIdJsonSerializer.java | 19 - .../mvc/FieldErrorResponse.java | 9 +- .../security/ApplicationUserDetails.java | 32 -- .../ApplicationUserDetailsService.java | 31 -- .../security/OAuth2ServerConfiguration.java | 105 ----- .../security/SecurityConfiguration.java | 27 -- .../security/WebSecurityConfiguration.java | 19 + .../report/CreateReportParameters.java | 8 + .../com/example/copsboot/report/Report.java | 24 +- .../copsboot/report/ReportService.java | 20 +- .../copsboot/report/ReportServiceImpl.java | 30 -- .../report/web/CreateReportParameters.java | 19 - .../report/web/CreateReportRequest.java | 14 + .../web/CreateReportRequestValidator.java | 21 + .../web/ReportDescriptionValidator.java | 4 +- .../copsboot/report/web/ReportDto.java | 22 +- .../report/web/ReportRestController.java | 32 +- .../report/web/ValidCreateReportRequest.java | 20 + .../report/web/ValidReportDescription.java | 6 +- .../example/copsboot/user/AuthServerId.java | 11 + .../user/AuthServerIdAttributeConverter.java | 19 + .../copsboot/user/CreateUserParameters.java | 4 + .../java/com/example/copsboot/user/User.java | 38 +- .../user/UserIdAttributeConverter.java | 19 + .../copsboot/user/UserNotFoundException.java | 8 +- .../example/copsboot/user/UserRepository.java | 6 +- .../example/copsboot/user/UserService.java | 34 +- .../copsboot/user/UserServiceImpl.java | 35 -- .../user/web/CreateOfficerParameters.java | 19 - .../web/CreateUserParametersValidator.java | 40 -- .../copsboot/user/web/CreateUserRequest.java | 18 + .../user/web/CreateUserRequestValidator.java | 40 ++ .../example/copsboot/user/web/UserDto.java | 19 +- .../copsboot/user/web/UserRestController.java | 58 +-- ...eters.java => ValidCreateUserRequest.java} | 8 +- .../com/example/orm/jpa/AbstractEntity.java | 4 +- .../com/example/orm/jpa/AbstractEntityId.java | 2 +- .../main/java/com/example/orm/jpa/Entity.java | 1 + .../main/resources/application-dev.properties | 5 - .../resources/application-local.properties | 14 +- .../src/main/resources/application.properties | 5 + .../db/migration/V1.0.0.1__users.sql | 7 + .../db/migration/V1.0.0.2__reports.sql | 8 + .../migration/h2/V1.0.0.1__authentication.sql | 42 -- .../postgresql/V1.0.0.1__authentication.sql | 52 --- .../migration/postgresql/V1.0.0.2__users.sql | 16 - .../copsboot/CopsbootApplicationTests.java | 15 +- .../ApplicationUserDetailsServiceTest.java | 52 --- .../OAuth2ServerConfigurationTest.java | 64 --- .../security/SecurityHelperForMockMvc.java | 58 --- .../security/StubUserDetailsService.java | 21 - .../CopsbootControllerDocumentationTest.java | 30 ++ ...trollerDocumentationTestConfiguration.java | 21 + .../test/CopsbootControllerTest.java | 25 +- .../CopsbootControllerTestConfiguration.java | 26 -- .../com/example/copsboot/report/Reports.java | 15 - .../web/CreateReportRequestValidatorTest.java | 63 +++ .../web/ReportDescriptionValidatorTest.java | 38 +- .../report/web/ReportRestControllerTest.java | 77 +-- .../user/UserRepositoryIntegrationTest.java | 49 -- .../copsboot/user/UserRepositoryTest.java | 69 +-- .../java/com/example/copsboot/user/Users.java | 55 --- .../CreateUserParametersValidatorTest.java | 74 --- .../web/CreateUserRequestValidatorTest.java | 65 +++ .../web/UserRestControllerDocumentation.java | 148 +++--- .../user/web/UserRestControllerTest.java | 155 +++---- .../test/ConstraintViolationSetAssert.java | 15 +- .../application-integration-test.properties | 9 +- .../application-repository-test.properties | 6 + .../resources/application-test.properties | 5 - .../src/test/resources/logback-test.xml | 10 +- .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + chapter09/01 - fileupload/docker-compose.yaml | 20 + chapter09/01 - fileupload/mvnw | 219 ++++++--- chapter09/01 - fileupload/mvnw.cmd | 92 +++- chapter09/01 - fileupload/pom.xml | 415 ++++++++--------- .../asciidoc/Copsboot REST API Guide.adoc | 14 + .../src/docs/asciidoc/_users.adoc | 24 + .../asciidoc/Copsboot REST API Guide.adoc | 14 - .../src/main/asciidoc/_users.adoc | 24 - .../example/copsboot/CopsbootApplication.java | 33 +- .../CopsbootApplicationConfiguration.java | 18 + .../copsboot/DevelopmentDbInitializer.java | 30 -- .../infrastructure/SpringProfiles.java | 1 + .../json/EntityIdJsonSerializer.java | 19 - .../mvc/FieldErrorResponse.java | 9 +- .../mvc/RestControllerExceptionHandler.java | 25 +- .../security/ApplicationUserDetails.java | 32 -- .../ApplicationUserDetailsService.java | 31 -- .../security/OAuth2ServerConfiguration.java | 105 ----- .../security/SecurityConfiguration.java | 27 -- .../security/WebSecurityConfiguration.java | 19 + .../report/CreateReportParameters.java | 8 + .../com/example/copsboot/report/Report.java | 24 +- .../copsboot/report/ReportService.java | 21 +- .../copsboot/report/ReportServiceImpl.java | 31 -- .../report/web/CreateReportParameters.java | 26 -- .../report/web/CreateReportRequest.java | 21 + .../web/CreateReportRequestValidator.java | 21 + .../web/ReportDescriptionValidator.java | 7 +- .../copsboot/report/web/ReportDto.java | 22 +- .../report/web/ReportRestController.java | 38 +- .../report/web/ValidCreateReportRequest.java | 20 + .../report/web/ValidReportDescription.java | 6 +- .../example/copsboot/user/AuthServerId.java | 11 + .../user/AuthServerIdAttributeConverter.java | 19 + .../copsboot/user/CreateUserParameters.java | 4 + .../java/com/example/copsboot/user/User.java | 38 +- .../user/UserIdAttributeConverter.java | 19 + .../copsboot/user/UserNotFoundException.java | 8 +- .../example/copsboot/user/UserRepository.java | 6 +- .../example/copsboot/user/UserService.java | 34 +- .../copsboot/user/UserServiceImpl.java | 35 -- .../user/web/CreateOfficerParameters.java | 19 - .../web/CreateUserParametersValidator.java | 40 -- .../copsboot/user/web/CreateUserRequest.java | 18 + .../user/web/CreateUserRequestValidator.java | 40 ++ .../example/copsboot/user/web/UserDto.java | 19 +- .../copsboot/user/web/UserRestController.java | 58 +-- ...eters.java => ValidCreateUserRequest.java} | 8 +- .../com/example/orm/jpa/AbstractEntity.java | 4 +- .../com/example/orm/jpa/AbstractEntityId.java | 2 +- .../main/java/com/example/orm/jpa/Entity.java | 1 + .../main/resources/application-dev.properties | 5 - .../resources/application-local.properties | 14 +- .../src/main/resources/application.properties | 5 + .../db/migration/V1.0.0.1__users.sql | 7 + .../db/migration/V1.0.0.2__reports.sql | 8 + .../migration/h2/V1.0.0.1__authentication.sql | 42 -- .../postgresql/V1.0.0.1__authentication.sql | 52 --- .../migration/postgresql/V1.0.0.2__users.sql | 16 - .../copsboot/CopsbootApplicationTests.java | 15 +- .../ApplicationUserDetailsServiceTest.java | 52 --- .../OAuth2ServerConfigurationTest.java | 64 --- .../security/SecurityHelperForMockMvc.java | 58 --- .../security/StubUserDetailsService.java | 21 - .../CopsbootControllerDocumentationTest.java | 30 ++ ...trollerDocumentationTestConfiguration.java | 21 + .../test/CopsbootControllerTest.java | 25 +- .../CopsbootControllerTestConfiguration.java | 26 -- .../com/example/copsboot/report/Reports.java | 15 - .../web/CreateReportRequestValidatorTest.java | 75 +++ .../web/ReportDescriptionValidatorTest.java | 53 ++- .../report/web/ReportRestControllerTest.java | 87 ++-- .../user/UserRepositoryIntegrationTest.java | 49 -- .../copsboot/user/UserRepositoryTest.java | 69 +-- .../java/com/example/copsboot/user/Users.java | 55 --- .../CreateUserParametersValidatorTest.java | 74 --- .../web/CreateUserRequestValidatorTest.java | 65 +++ .../web/UserRestControllerDocumentation.java | 148 +++--- .../user/web/UserRestControllerTest.java | 155 +++---- .../test/ConstraintViolationSetAssert.java | 15 +- .../application-integration-test.properties | 9 +- .../application-repository-test.properties | 6 + .../resources/application-test.properties | 5 - .../src/test/resources/logback-test.xml | 10 +- .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + chapter09/02 - validation/docker-compose.yaml | 20 + chapter09/02 - validation/mvnw | 219 ++++++--- chapter09/02 - validation/mvnw.cmd | 92 +++- chapter09/02 - validation/pom.xml | 437 +++++++++--------- .../asciidoc/Copsboot REST API Guide.adoc | 14 + .../src/docs/asciidoc/_users.adoc | 24 + .../asciidoc/Copsboot REST API Guide.adoc | 14 - .../src/main/asciidoc/_users.adoc | 24 - .../example/copsboot/CopsbootApplication.java | 33 +- .../CopsbootApplicationConfiguration.java | 18 + .../copsboot/DevelopmentDbInitializer.java | 30 -- .../infrastructure/SpringProfiles.java | 1 + .../json/EntityIdJsonSerializer.java | 19 - .../mvc/FieldErrorResponse.java | 9 +- .../mvc/RestControllerExceptionHandler.java | 50 +- .../security/ApplicationUserDetails.java | 32 -- .../ApplicationUserDetailsService.java | 29 -- .../security/OAuth2ServerConfiguration.java | 105 ----- .../security/SecurityConfiguration.java | 27 -- .../security/WebSecurityConfiguration.java | 19 + .../report/CreateReportParameters.java | 8 + .../com/example/copsboot/report/Report.java | 24 +- .../copsboot/report/ReportService.java | 21 +- .../copsboot/report/ReportServiceImpl.java | 31 -- .../report/web/CreateReportParameters.java | 26 -- .../report/web/CreateReportRequest.java | 21 + .../web/CreateReportRequestValidator.java | 21 + .../web/ReportDescriptionValidator.java | 7 +- .../copsboot/report/web/ReportDto.java | 22 +- .../report/web/ReportRestController.java | 38 +- .../report/web/ValidCreateReportRequest.java | 20 + .../report/web/ValidReportDescription.java | 6 +- .../example/copsboot/user/AuthServerId.java | 11 + .../user/AuthServerIdAttributeConverter.java | 19 + .../copsboot/user/CreateUserParameters.java | 4 + .../java/com/example/copsboot/user/User.java | 38 +- .../user/UserIdAttributeConverter.java | 19 + .../copsboot/user/UserNotFoundException.java | 8 +- .../example/copsboot/user/UserRepository.java | 6 +- .../example/copsboot/user/UserService.java | 34 +- .../copsboot/user/UserServiceImpl.java | 35 -- .../user/web/CreateOfficerParameters.java | 19 - .../web/CreateUserParametersValidator.java | 40 -- .../copsboot/user/web/CreateUserRequest.java | 18 + .../user/web/CreateUserRequestValidator.java | 40 ++ .../example/copsboot/user/web/UserDto.java | 19 +- .../copsboot/user/web/UserRestController.java | 58 +-- ...eters.java => ValidCreateUserRequest.java} | 8 +- .../com/example/orm/jpa/AbstractEntity.java | 4 +- .../com/example/orm/jpa/AbstractEntityId.java | 2 +- .../main/java/com/example/orm/jpa/Entity.java | 1 + .../example/util/ArtifactForFramework.java | 4 - .../main/resources/application-dev.properties | 5 - .../resources/application-local.properties | 14 +- .../src/main/resources/application.properties | 6 +- .../db/migration/V1.0.0.1__users.sql | 7 + .../db/migration/V1.0.0.2__reports.sql | 8 + .../migration/h2/V1.0.0.1__authentication.sql | 42 -- .../postgresql/V1.0.0.1__authentication.sql | 52 --- .../migration/postgresql/V1.0.0.2__users.sql | 16 - .../copsboot/CopsbootApplicationTests.java | 15 +- .../ApplicationUserDetailsServiceTest.java | 52 --- .../OAuth2ServerConfigurationTest.java | 64 --- .../security/SecurityHelperForMockMvc.java | 58 --- .../SecurityHelperForRestAssured.java | 33 -- .../security/StubUserDetailsService.java | 21 - .../CopsbootControllerDocumentationTest.java | 30 ++ ...trollerDocumentationTestConfiguration.java | 21 + .../test/CopsbootControllerTest.java | 25 +- .../CopsbootControllerTestConfiguration.java | 26 -- .../com/example/copsboot/report/Reports.java | 15 - .../web/CreateReportRequestValidatorTest.java | 75 +++ .../report/web/KeycloakAdminClientFacade.java | 66 +++ .../web/ReportDescriptionValidatorTest.java | 53 ++- .../ReportRestControllerIntegrationTest.java | 156 +++++-- .../report/web/ReportRestControllerTest.java | 87 ++-- .../user/UserRepositoryIntegrationTest.java | 49 -- .../copsboot/user/UserRepositoryTest.java | 69 +-- .../java/com/example/copsboot/user/Users.java | 55 --- .../CreateUserParametersValidatorTest.java | 74 --- .../web/CreateUserRequestValidatorTest.java | 65 +++ .../web/UserRestControllerDocumentation.java | 148 +++--- .../user/web/UserRestControllerTest.java | 155 +++---- .../test/ConstraintViolationSetAssert.java | 15 +- .../application-integration-test.properties | 10 +- .../application-repository-test.properties | 6 + .../resources/application-test.properties | 5 - .../src/test/resources/jwt-officer.json | 41 ++ .../src/test/resources/logback-test.xml | 10 +- 1027 files changed, 16094 insertions(+), 23185 deletions(-) create mode 100644 chapter02/01 - Generated project/.gitignore create mode 100644 chapter02/01 - Generated project/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter02/01 - Generated project/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter02/02 - Logging config/.gitignore create mode 100644 chapter02/02 - Logging config/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter02/02 - Logging config/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter03/01 - Generated project/.gitignore create mode 100644 chapter03/01 - Generated project/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter03/01 - Generated project/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter04/01 - User domain/.gitignore create mode 100644 chapter04/01 - User domain/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter04/01 - User domain/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter04/02 - User with JPA/.gitignore create mode 100644 chapter04/02 - User with JPA/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter04/02 - User with JPA/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter04/03 - User with JPA refactored/.gitignore create mode 100644 chapter04/03 - User with JPA refactored/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter04/03 - User with JPA refactored/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java create mode 100644 chapter05/01 - Oauth2/.gitignore create mode 100644 chapter05/01 - Oauth2/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter05/01 - Oauth2/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter05/01 - Oauth2/docker-compose.yaml create mode 100644 chapter05/01 - Oauth2/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java delete mode 100644 chapter05/01 - Oauth2/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java delete mode 100644 chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java delete mode 100644 chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java delete mode 100644 chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java create mode 100644 chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java delete mode 100644 chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserService.java delete mode 100644 chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserServiceImpl.java create mode 100644 chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/web/UserRestController.java delete mode 100644 chapter05/01 - Oauth2/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java delete mode 100644 chapter05/01 - Oauth2/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java delete mode 100644 chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/Users.java create mode 100644 chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java delete mode 100755 chapter05/02 - Oauth configurable/mvnw delete mode 100644 chapter05/02 - Oauth configurable/pom.xml delete mode 100644 chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/CopsbootApplication.java delete mode 100644 chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java delete mode 100644 chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java delete mode 100644 chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java delete mode 100644 chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java delete mode 100644 chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java delete mode 100644 chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/User.java delete mode 100644 chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRepository.java delete mode 100644 chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserService.java delete mode 100644 chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserServiceImpl.java delete mode 100644 chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/Entity.java delete mode 100644 chapter05/02 - Oauth configurable/src/main/resources/application-dev.properties delete mode 100644 chapter05/02 - Oauth configurable/src/main/resources/application.properties delete mode 100644 chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/CopsbootApplicationTests.java delete mode 100644 chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java delete mode 100644 chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java delete mode 100644 chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/user/UserRepositoryTest.java delete mode 100644 chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/user/Users.java delete mode 100644 chapter05/02 - Oauth configurable/src/test/resources/application-test.properties create mode 100644 chapter06/01 - User rest controller/.gitignore create mode 100644 chapter06/01 - User rest controller/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter06/01 - User rest controller/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter06/01 - User rest controller/docker-compose.yaml create mode 100644 chapter06/01 - User rest controller/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java delete mode 100644 chapter06/01 - User rest controller/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java delete mode 100644 chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java delete mode 100644 chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java delete mode 100644 chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java delete mode 100644 chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java create mode 100644 chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java create mode 100644 chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/AuthServerId.java create mode 100644 chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java create mode 100644 chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/CreateUserParameters.java delete mode 100644 chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserNotFoundException.java delete mode 100644 chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserServiceImpl.java create mode 100644 chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java delete mode 100644 chapter06/01 - User rest controller/src/main/resources/application-dev.properties delete mode 100644 chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java delete mode 100644 chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java delete mode 100644 chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java delete mode 100644 chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java delete mode 100644 chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/Users.java delete mode 100644 chapter06/01 - User rest controller/src/test/resources/application-test.properties delete mode 100755 chapter06/02 - Post mapping/mvnw delete mode 100644 chapter06/02 - Post mapping/mvnw.cmd delete mode 100644 chapter06/02 - Post mapping/pom.xml delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/CopsbootApplication.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/User.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserId.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserNotFoundException.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepositoryCustom.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepositoryImpl.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRole.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserService.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserServiceImpl.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/UserDto.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/UserRestController.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/AbstractEntity.java delete mode 100755 chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/AbstractEntityId.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/EntityId.java delete mode 100755 chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/InMemoryUniqueIdGenerator.java delete mode 100755 chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/UniqueIdGenerator.java delete mode 100644 chapter06/02 - Post mapping/src/main/java/com/example/util/ArtifactForFramework.java delete mode 100644 chapter06/02 - Post mapping/src/main/resources/application-dev.properties delete mode 100644 chapter06/02 - Post mapping/src/main/resources/application.properties delete mode 100644 chapter06/02 - Post mapping/src/main/resources/logback-spring.xml delete mode 100644 chapter06/02 - Post mapping/src/test/java/com/example/copsboot/CopsbootApplicationTests.java delete mode 100644 chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java delete mode 100644 chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java delete mode 100644 chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java delete mode 100644 chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java delete mode 100644 chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/UserRepositoryTest.java delete mode 100644 chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/Users.java delete mode 100644 chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java delete mode 100644 chapter06/02 - Post mapping/src/test/resources/application-test.properties delete mode 100644 chapter06/02 - Post mapping/src/test/resources/logback-test.xml create mode 100644 chapter06/02 - user role/.gitignore create mode 100644 chapter06/02 - user role/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter06/02 - user role/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter06/02 - user role/docker-compose.yaml create mode 100755 chapter06/02 - user role/mvnw rename {chapter05/02 - Oauth configurable => chapter06/02 - user role}/mvnw.cmd (54%) create mode 100644 chapter06/02 - user role/pom.xml create mode 100644 chapter06/02 - user role/src/main/java/com/example/copsboot/CopsbootApplication.java create mode 100644 chapter06/02 - user role/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java rename {chapter05/02 - Oauth configurable => chapter06/02 - user role}/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java (100%) create mode 100644 chapter06/02 - user role/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java create mode 100644 chapter06/02 - user role/src/main/java/com/example/copsboot/user/AuthServerId.java create mode 100644 chapter06/02 - user role/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java create mode 100644 chapter06/02 - user role/src/main/java/com/example/copsboot/user/CreateUserParameters.java create mode 100644 chapter06/02 - user role/src/main/java/com/example/copsboot/user/User.java rename {chapter05/02 - Oauth configurable => chapter06/02 - user role}/src/main/java/com/example/copsboot/user/UserId.java (100%) rename chapter06/{02 - Post mapping => 02 - user role}/src/main/java/com/example/copsboot/user/UserRepository.java (75%) rename {chapter05/02 - Oauth configurable => chapter06/02 - user role}/src/main/java/com/example/copsboot/user/UserRepositoryCustom.java (100%) rename {chapter05/02 - Oauth configurable => chapter06/02 - user role}/src/main/java/com/example/copsboot/user/UserRepositoryImpl.java (100%) rename {chapter05/02 - Oauth configurable => chapter06/02 - user role}/src/main/java/com/example/copsboot/user/UserRole.java (100%) create mode 100644 chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserService.java create mode 100644 chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java create mode 100644 chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/UserDto.java create mode 100644 chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/UserRestController.java rename {chapter05/02 - Oauth configurable => chapter06/02 - user role}/src/main/java/com/example/orm/jpa/AbstractEntity.java (94%) rename {chapter05/02 - Oauth configurable => chapter06/02 - user role}/src/main/java/com/example/orm/jpa/AbstractEntityId.java (96%) rename chapter06/{02 - Post mapping => 02 - user role}/src/main/java/com/example/orm/jpa/Entity.java (99%) rename {chapter05/02 - Oauth configurable => chapter06/02 - user role}/src/main/java/com/example/orm/jpa/EntityId.java (100%) rename {chapter05/02 - Oauth configurable => chapter06/02 - user role}/src/main/java/com/example/orm/jpa/InMemoryUniqueIdGenerator.java (100%) rename {chapter05/02 - Oauth configurable => chapter06/02 - user role}/src/main/java/com/example/orm/jpa/UniqueIdGenerator.java (100%) rename {chapter05/02 - Oauth configurable => chapter06/02 - user role}/src/main/java/com/example/util/ArtifactForFramework.java (100%) create mode 100644 chapter06/02 - user role/src/main/resources/application.properties rename {chapter05/02 - Oauth configurable => chapter06/02 - user role}/src/main/resources/logback-spring.xml (100%) create mode 100644 chapter06/02 - user role/src/test/java/com/example/copsboot/CopsbootApplicationTests.java create mode 100644 chapter06/02 - user role/src/test/java/com/example/copsboot/user/UserRepositoryTest.java create mode 100644 chapter06/02 - user role/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java rename {chapter05/02 - Oauth configurable => chapter06/02 - user role}/src/test/resources/logback-test.xml (100%) create mode 100644 chapter06/03 - Writing API Documentation/.gitignore create mode 100644 chapter06/03 - Writing API Documentation/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter06/03 - Writing API Documentation/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter06/03 - Writing API Documentation/docker-compose.yaml rename chapter06/03 - Writing API Documentation/src/{main => docs}/asciidoc/Copsboot REST API Guide.adoc (100%) create mode 100644 chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java delete mode 100644 chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java delete mode 100644 chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java delete mode 100644 chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java delete mode 100644 chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java delete mode 100644 chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java create mode 100644 chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java create mode 100644 chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/AuthServerId.java create mode 100644 chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java create mode 100644 chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/CreateUserParameters.java delete mode 100644 chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserNotFoundException.java delete mode 100644 chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserServiceImpl.java delete mode 100644 chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java create mode 100644 chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java delete mode 100644 chapter06/03 - Writing API Documentation/src/main/resources/application-dev.properties delete mode 100644 chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java delete mode 100644 chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java delete mode 100644 chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java delete mode 100644 chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java delete mode 100644 chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/Users.java delete mode 100644 chapter06/03 - Writing API Documentation/src/test/resources/application-test.properties create mode 100644 chapter06/04 - Generating snippets/.gitignore create mode 100644 chapter06/04 - Generating snippets/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter06/04 - Generating snippets/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter06/04 - Generating snippets/docker-compose.yaml rename chapter06/04 - Generating snippets/src/{main => docs}/asciidoc/Copsboot REST API Guide.adoc (91%) rename chapter06/04 - Generating snippets/src/{main => docs}/asciidoc/_users.adoc (56%) create mode 100644 chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java delete mode 100644 chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java delete mode 100644 chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java delete mode 100644 chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java delete mode 100644 chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java delete mode 100644 chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java delete mode 100644 chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java create mode 100644 chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java create mode 100644 chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/AuthServerId.java create mode 100644 chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java create mode 100644 chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/CreateUserParameters.java delete mode 100644 chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserNotFoundException.java delete mode 100644 chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserServiceImpl.java delete mode 100644 chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java create mode 100644 chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java delete mode 100644 chapter06/04 - Generating snippets/src/main/resources/application-dev.properties delete mode 100644 chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java delete mode 100644 chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java delete mode 100644 chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java delete mode 100644 chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java delete mode 100644 chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/Users.java delete mode 100644 chapter06/04 - Generating snippets/src/test/resources/application-test.properties create mode 100644 chapter06/05 - Refactoring/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter06/05 - Refactoring/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter06/05 - Refactoring/docker-compose.yaml rename chapter06/05 - Refactoring/src/{main => docs}/asciidoc/Copsboot REST API Guide.adoc (91%) rename chapter06/05 - Refactoring/src/{main => docs}/asciidoc/_users.adoc (56%) create mode 100644 chapter06/05 - Refactoring/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java delete mode 100644 chapter06/05 - Refactoring/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java delete mode 100644 chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java delete mode 100644 chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java delete mode 100644 chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java delete mode 100644 chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java delete mode 100644 chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java create mode 100644 chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java create mode 100644 chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/AuthServerId.java create mode 100644 chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java create mode 100644 chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/CreateUserParameters.java delete mode 100644 chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserNotFoundException.java delete mode 100644 chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserServiceImpl.java delete mode 100644 chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java create mode 100644 chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java delete mode 100644 chapter06/05 - Refactoring/src/main/resources/application-dev.properties delete mode 100644 chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java delete mode 100644 chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java delete mode 100644 chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java delete mode 100644 chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java create mode 100644 chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java create mode 100644 chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java delete mode 100644 chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java delete mode 100644 chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/Users.java delete mode 100644 chapter06/05 - Refactoring/src/test/resources/application-test.properties create mode 100644 chapter07/01 - postgresql/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter07/01 - postgresql/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter07/01 - postgresql/docker-compose.yaml rename chapter07/01 - postgresql/src/{main => docs}/asciidoc/Copsboot REST API Guide.adoc (91%) rename chapter07/01 - postgresql/src/{main => docs}/asciidoc/_users.adoc (56%) create mode 100644 chapter07/01 - postgresql/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java delete mode 100644 chapter07/01 - postgresql/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java delete mode 100644 chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java delete mode 100644 chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java delete mode 100644 chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java delete mode 100644 chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java delete mode 100644 chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java create mode 100644 chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java create mode 100644 chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/AuthServerId.java create mode 100644 chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java create mode 100644 chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/CreateUserParameters.java delete mode 100644 chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserNotFoundException.java delete mode 100644 chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserServiceImpl.java delete mode 100644 chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java create mode 100644 chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java delete mode 100644 chapter07/01 - postgresql/src/main/resources/application-dev.properties create mode 100644 chapter07/01 - postgresql/src/main/resources/db/migration/V1.0.0.1__users.sql delete mode 100644 chapter07/01 - postgresql/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql delete mode 100644 chapter07/01 - postgresql/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql delete mode 100644 chapter07/01 - postgresql/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql delete mode 100644 chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java delete mode 100644 chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java delete mode 100644 chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java delete mode 100644 chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java create mode 100644 chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java create mode 100644 chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java delete mode 100644 chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java delete mode 100644 chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/Users.java delete mode 100644 chapter07/01 - postgresql/src/test/resources/application-test.properties create mode 100644 chapter07/02 - testcontainers/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter07/02 - testcontainers/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter07/02 - testcontainers/docker-compose.yaml rename chapter07/02 - testcontainers/src/{main => docs}/asciidoc/Copsboot REST API Guide.adoc (91%) rename chapter07/02 - testcontainers/src/{main => docs}/asciidoc/_users.adoc (56%) create mode 100644 chapter07/02 - testcontainers/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java delete mode 100644 chapter07/02 - testcontainers/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java delete mode 100644 chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java delete mode 100644 chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java delete mode 100644 chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java delete mode 100644 chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java delete mode 100644 chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java create mode 100644 chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java create mode 100644 chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/AuthServerId.java create mode 100644 chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java create mode 100644 chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/CreateUserParameters.java delete mode 100644 chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserNotFoundException.java delete mode 100644 chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserServiceImpl.java delete mode 100644 chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java create mode 100644 chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java delete mode 100644 chapter07/02 - testcontainers/src/main/resources/application-dev.properties create mode 100644 chapter07/02 - testcontainers/src/main/resources/db/migration/V1.0.0.1__users.sql delete mode 100644 chapter07/02 - testcontainers/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql delete mode 100644 chapter07/02 - testcontainers/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql delete mode 100644 chapter07/02 - testcontainers/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql delete mode 100644 chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java delete mode 100644 chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java delete mode 100644 chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java delete mode 100644 chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java create mode 100644 chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java create mode 100644 chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java delete mode 100644 chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java delete mode 100644 chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java delete mode 100644 chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/Users.java create mode 100644 chapter07/02 - testcontainers/src/test/resources/application-repository-test.properties delete mode 100644 chapter07/02 - testcontainers/src/test/resources/application-test.properties create mode 100644 chapter08/01 - builtin/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter08/01 - builtin/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter08/01 - builtin/docker-compose.yaml create mode 100644 chapter08/01 - builtin/src/docs/asciidoc/Copsboot REST API Guide.adoc create mode 100644 chapter08/01 - builtin/src/docs/asciidoc/_users.adoc delete mode 100644 chapter08/01 - builtin/src/main/asciidoc/Copsboot REST API Guide.adoc delete mode 100644 chapter08/01 - builtin/src/main/asciidoc/_users.adoc create mode 100644 chapter08/01 - builtin/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java delete mode 100644 chapter08/01 - builtin/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java delete mode 100644 chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java delete mode 100644 chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java delete mode 100644 chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java delete mode 100644 chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java delete mode 100644 chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java create mode 100644 chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java create mode 100644 chapter08/01 - builtin/src/main/java/com/example/copsboot/user/AuthServerId.java create mode 100644 chapter08/01 - builtin/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java create mode 100644 chapter08/01 - builtin/src/main/java/com/example/copsboot/user/CreateUserParameters.java delete mode 100644 chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserNotFoundException.java delete mode 100644 chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserServiceImpl.java delete mode 100644 chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java create mode 100644 chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java delete mode 100644 chapter08/01 - builtin/src/main/resources/application-dev.properties create mode 100644 chapter08/01 - builtin/src/main/resources/db/migration/V1.0.0.1__users.sql delete mode 100644 chapter08/01 - builtin/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql delete mode 100644 chapter08/01 - builtin/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql delete mode 100644 chapter08/01 - builtin/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql delete mode 100644 chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java delete mode 100644 chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java delete mode 100644 chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java delete mode 100644 chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java create mode 100644 chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java create mode 100644 chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java delete mode 100644 chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java delete mode 100644 chapter08/01 - builtin/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java delete mode 100644 chapter08/01 - builtin/src/test/java/com/example/copsboot/user/Users.java delete mode 100644 chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerWithResponseBodyValidationTest.java create mode 100644 chapter08/01 - builtin/src/test/resources/application-repository-test.properties delete mode 100644 chapter08/01 - builtin/src/test/resources/application-test.properties create mode 100644 chapter08/02 - customfield/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter08/02 - customfield/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter08/02 - customfield/docker-compose.yaml create mode 100644 chapter08/02 - customfield/src/docs/asciidoc/Copsboot REST API Guide.adoc create mode 100644 chapter08/02 - customfield/src/docs/asciidoc/_users.adoc delete mode 100644 chapter08/02 - customfield/src/main/asciidoc/Copsboot REST API Guide.adoc delete mode 100644 chapter08/02 - customfield/src/main/asciidoc/_users.adoc create mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java delete mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java delete mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java delete mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java delete mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java delete mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java delete mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java create mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java create mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/report/CreateReportParameters.java delete mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/report/ReportServiceImpl.java delete mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java create mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java create mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/user/AuthServerId.java create mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java create mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/user/CreateUserParameters.java create mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java delete mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserServiceImpl.java delete mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java create mode 100644 chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java delete mode 100644 chapter08/02 - customfield/src/main/resources/application-dev.properties create mode 100644 chapter08/02 - customfield/src/main/resources/db/migration/V1.0.0.1__users.sql create mode 100644 chapter08/02 - customfield/src/main/resources/db/migration/V1.0.0.2__reports.sql delete mode 100644 chapter08/02 - customfield/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql delete mode 100644 chapter08/02 - customfield/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql delete mode 100644 chapter08/02 - customfield/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql delete mode 100644 chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java delete mode 100644 chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java delete mode 100644 chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java delete mode 100644 chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java create mode 100644 chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java create mode 100644 chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java delete mode 100644 chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java delete mode 100644 chapter08/02 - customfield/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java delete mode 100644 chapter08/02 - customfield/src/test/java/com/example/copsboot/user/Users.java create mode 100644 chapter08/02 - customfield/src/test/resources/application-repository-test.properties delete mode 100644 chapter08/02 - customfield/src/test/resources/application-test.properties create mode 100644 chapter08/03 - customfieldfinal/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter08/03 - customfieldfinal/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter08/03 - customfieldfinal/docker-compose.yaml create mode 100644 chapter08/03 - customfieldfinal/src/docs/asciidoc/Copsboot REST API Guide.adoc create mode 100644 chapter08/03 - customfieldfinal/src/docs/asciidoc/_users.adoc delete mode 100644 chapter08/03 - customfieldfinal/src/main/asciidoc/Copsboot REST API Guide.adoc delete mode 100644 chapter08/03 - customfieldfinal/src/main/asciidoc/_users.adoc create mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java delete mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java delete mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java delete mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java delete mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java delete mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java delete mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java create mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java create mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/CreateReportParameters.java delete mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/ReportServiceImpl.java delete mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java create mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java create mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/AuthServerId.java create mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java create mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/CreateUserParameters.java create mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java delete mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserServiceImpl.java delete mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java create mode 100644 chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java delete mode 100644 chapter08/03 - customfieldfinal/src/main/resources/application-dev.properties create mode 100644 chapter08/03 - customfieldfinal/src/main/resources/db/migration/V1.0.0.1__users.sql create mode 100644 chapter08/03 - customfieldfinal/src/main/resources/db/migration/V1.0.0.2__reports.sql delete mode 100644 chapter08/03 - customfieldfinal/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql delete mode 100644 chapter08/03 - customfieldfinal/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql delete mode 100644 chapter08/03 - customfieldfinal/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql delete mode 100644 chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java delete mode 100644 chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java delete mode 100644 chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java delete mode 100644 chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java create mode 100644 chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java create mode 100644 chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java delete mode 100644 chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java delete mode 100644 chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/Reports.java delete mode 100644 chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java delete mode 100644 chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/Users.java create mode 100644 chapter08/03 - customfieldfinal/src/test/resources/application-repository-test.properties delete mode 100644 chapter08/03 - customfieldfinal/src/test/resources/application-test.properties create mode 100644 chapter08/04 - objectvalidation/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter08/04 - objectvalidation/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter08/04 - objectvalidation/docker-compose.yaml create mode 100644 chapter08/04 - objectvalidation/src/docs/asciidoc/Copsboot REST API Guide.adoc create mode 100644 chapter08/04 - objectvalidation/src/docs/asciidoc/_users.adoc delete mode 100644 chapter08/04 - objectvalidation/src/main/asciidoc/Copsboot REST API Guide.adoc delete mode 100644 chapter08/04 - objectvalidation/src/main/asciidoc/_users.adoc create mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java delete mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java delete mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java delete mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java delete mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java delete mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java delete mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java create mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java create mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/CreateReportParameters.java delete mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/ReportServiceImpl.java delete mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java delete mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportParametersValidator.java create mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java create mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java rename chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/{ValidCreateReportParameters.java => ValidCreateReportRequest.java} (67%) create mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/AuthServerId.java create mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java create mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/CreateUserParameters.java create mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java delete mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserServiceImpl.java delete mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java create mode 100644 chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java delete mode 100644 chapter08/04 - objectvalidation/src/main/resources/application-dev.properties create mode 100644 chapter08/04 - objectvalidation/src/main/resources/db/migration/V1.0.0.1__users.sql create mode 100644 chapter08/04 - objectvalidation/src/main/resources/db/migration/V1.0.0.2__reports.sql delete mode 100644 chapter08/04 - objectvalidation/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql delete mode 100644 chapter08/04 - objectvalidation/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql delete mode 100644 chapter08/04 - objectvalidation/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql delete mode 100644 chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java delete mode 100644 chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java delete mode 100644 chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java delete mode 100644 chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java create mode 100644 chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java create mode 100644 chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java delete mode 100644 chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java delete mode 100644 chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/Reports.java delete mode 100644 chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/CreateReportParametersValidatorTest.java create mode 100644 chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java delete mode 100644 chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java delete mode 100644 chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/Users.java create mode 100644 chapter08/04 - objectvalidation/src/test/resources/application-repository-test.properties delete mode 100644 chapter08/04 - objectvalidation/src/test/resources/application-test.properties create mode 100644 chapter08/05 - validatorspringbean/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter08/05 - validatorspringbean/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter08/05 - validatorspringbean/docker-compose.yaml create mode 100644 chapter08/05 - validatorspringbean/src/docs/asciidoc/Copsboot REST API Guide.adoc create mode 100644 chapter08/05 - validatorspringbean/src/docs/asciidoc/_users.adoc delete mode 100644 chapter08/05 - validatorspringbean/src/main/asciidoc/Copsboot REST API Guide.adoc delete mode 100644 chapter08/05 - validatorspringbean/src/main/asciidoc/_users.adoc create mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java delete mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java delete mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java delete mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java delete mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java delete mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java delete mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java create mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java create mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/CreateReportParameters.java delete mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/ReportServiceImpl.java delete mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java create mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java create mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java create mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java create mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/AuthServerId.java create mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java create mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/CreateUserParameters.java create mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java delete mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserServiceImpl.java delete mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java delete mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java create mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java create mode 100644 chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java rename chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/{ValidCreateUserParameters.java => ValidCreateUserRequest.java} (70%) delete mode 100644 chapter08/05 - validatorspringbean/src/main/resources/application-dev.properties create mode 100644 chapter08/05 - validatorspringbean/src/main/resources/db/migration/V1.0.0.1__users.sql create mode 100644 chapter08/05 - validatorspringbean/src/main/resources/db/migration/V1.0.0.2__reports.sql delete mode 100644 chapter08/05 - validatorspringbean/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql delete mode 100644 chapter08/05 - validatorspringbean/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql delete mode 100644 chapter08/05 - validatorspringbean/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql delete mode 100644 chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java delete mode 100644 chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java delete mode 100644 chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java delete mode 100644 chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java create mode 100644 chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java create mode 100644 chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java delete mode 100644 chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java delete mode 100644 chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/Reports.java create mode 100644 chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java delete mode 100644 chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java delete mode 100644 chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/Users.java delete mode 100644 chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java create mode 100644 chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java create mode 100644 chapter08/05 - validatorspringbean/src/test/resources/application-repository-test.properties delete mode 100644 chapter08/05 - validatorspringbean/src/test/resources/application-test.properties create mode 100644 chapter09/01 - fileupload/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter09/01 - fileupload/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter09/01 - fileupload/docker-compose.yaml create mode 100644 chapter09/01 - fileupload/src/docs/asciidoc/Copsboot REST API Guide.adoc create mode 100644 chapter09/01 - fileupload/src/docs/asciidoc/_users.adoc delete mode 100644 chapter09/01 - fileupload/src/main/asciidoc/Copsboot REST API Guide.adoc delete mode 100644 chapter09/01 - fileupload/src/main/asciidoc/_users.adoc create mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java delete mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java delete mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java delete mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java delete mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java delete mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java delete mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java create mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java create mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/CreateReportParameters.java delete mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/ReportServiceImpl.java delete mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java create mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java create mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java create mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java create mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/AuthServerId.java create mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java create mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/CreateUserParameters.java create mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java delete mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserServiceImpl.java delete mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java delete mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java create mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java create mode 100644 chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java rename chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/{ValidCreateUserParameters.java => ValidCreateUserRequest.java} (70%) delete mode 100644 chapter09/01 - fileupload/src/main/resources/application-dev.properties create mode 100644 chapter09/01 - fileupload/src/main/resources/db/migration/V1.0.0.1__users.sql create mode 100644 chapter09/01 - fileupload/src/main/resources/db/migration/V1.0.0.2__reports.sql delete mode 100644 chapter09/01 - fileupload/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql delete mode 100644 chapter09/01 - fileupload/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql delete mode 100644 chapter09/01 - fileupload/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql delete mode 100644 chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java delete mode 100644 chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java delete mode 100644 chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java delete mode 100644 chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java create mode 100644 chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java create mode 100644 chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java delete mode 100644 chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java delete mode 100644 chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/Reports.java create mode 100644 chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java delete mode 100644 chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java delete mode 100644 chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/Users.java delete mode 100644 chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java create mode 100644 chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java create mode 100644 chapter09/01 - fileupload/src/test/resources/application-repository-test.properties delete mode 100644 chapter09/01 - fileupload/src/test/resources/application-test.properties create mode 100644 chapter09/02 - validation/.mvn/wrapper/maven-wrapper.jar create mode 100644 chapter09/02 - validation/.mvn/wrapper/maven-wrapper.properties create mode 100644 chapter09/02 - validation/docker-compose.yaml create mode 100644 chapter09/02 - validation/src/docs/asciidoc/Copsboot REST API Guide.adoc create mode 100644 chapter09/02 - validation/src/docs/asciidoc/_users.adoc delete mode 100644 chapter09/02 - validation/src/main/asciidoc/Copsboot REST API Guide.adoc delete mode 100644 chapter09/02 - validation/src/main/asciidoc/_users.adoc create mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java delete mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java delete mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java delete mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java delete mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java delete mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java delete mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java create mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java create mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/report/CreateReportParameters.java delete mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/report/ReportServiceImpl.java delete mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java create mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java create mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java create mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java create mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/user/AuthServerId.java create mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java create mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/user/CreateUserParameters.java create mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java delete mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserServiceImpl.java delete mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java delete mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java create mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java create mode 100644 chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java rename chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/{ValidCreateUserParameters.java => ValidCreateUserRequest.java} (70%) delete mode 100644 chapter09/02 - validation/src/main/resources/application-dev.properties create mode 100644 chapter09/02 - validation/src/main/resources/db/migration/V1.0.0.1__users.sql create mode 100644 chapter09/02 - validation/src/main/resources/db/migration/V1.0.0.2__reports.sql delete mode 100644 chapter09/02 - validation/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql delete mode 100644 chapter09/02 - validation/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql delete mode 100644 chapter09/02 - validation/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql delete mode 100644 chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java delete mode 100644 chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java delete mode 100644 chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java delete mode 100644 chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForRestAssured.java delete mode 100644 chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java create mode 100644 chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java create mode 100644 chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java delete mode 100644 chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java delete mode 100644 chapter09/02 - validation/src/test/java/com/example/copsboot/report/Reports.java create mode 100644 chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java create mode 100644 chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/KeycloakAdminClientFacade.java delete mode 100644 chapter09/02 - validation/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java delete mode 100644 chapter09/02 - validation/src/test/java/com/example/copsboot/user/Users.java delete mode 100644 chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java create mode 100644 chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java create mode 100644 chapter09/02 - validation/src/test/resources/application-repository-test.properties delete mode 100644 chapter09/02 - validation/src/test/resources/application-test.properties create mode 100644 chapter09/02 - validation/src/test/resources/jwt-officer.json diff --git a/chapter02/01 - Generated project/.gitignore b/chapter02/01 - Generated project/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter02/01 - Generated project/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter02/01 - Generated project/.mvn/wrapper/maven-wrapper.jar b/chapter02/01 - Generated project/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter02/01 - Generated project/.mvn/wrapper/maven-wrapper.properties b/chapter02/01 - Generated project/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter02/01 - Generated project/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter02/01 - Generated project/mvnw b/chapter02/01 - Generated project/mvnw index 5bf251c..66df285 100755 --- a/chapter02/01 - Generated project/mvnw +++ b/chapter02/01 - Generated project/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter02/01 - Generated project/mvnw.cmd b/chapter02/01 - Generated project/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter02/01 - Generated project/mvnw.cmd +++ b/chapter02/01 - Generated project/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter02/01 - Generated project/pom.xml b/chapter02/01 - Generated project/pom.xml index d3f0867..747977a 100644 --- a/chapter02/01 - Generated project/pom.xml +++ b/chapter02/01 - Generated project/pom.xml @@ -1,29 +1,21 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - com.springbook - application - 0.0.1-SNAPSHOT - jar - - application - Demo project for Spring Boot - org.springframework.boot spring-boot-starter-parent - 2.1.4.RELEASE + 3.2.2 - + com.springbook + application + 0.0.1-SNAPSHOT + application + Demo project for Spring Boot - UTF-8 - UTF-8 - 1.8 + 17 - @@ -32,11 +24,6 @@ - - org.projectlombok - lombok - true - org.springframework.boot spring-boot-starter-test @@ -53,5 +40,4 @@ - diff --git a/chapter02/01 - Generated project/src/main/resources/application.properties b/chapter02/01 - Generated project/src/main/resources/application.properties index e69de29..8b13789 100644 --- a/chapter02/01 - Generated project/src/main/resources/application.properties +++ b/chapter02/01 - Generated project/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/chapter02/01 - Generated project/src/test/java/com/springbook/application/ApplicationTests.java b/chapter02/01 - Generated project/src/test/java/com/springbook/application/ApplicationTests.java index 618c39d..ee8ff1a 100644 --- a/chapter02/01 - Generated project/src/test/java/com/springbook/application/ApplicationTests.java +++ b/chapter02/01 - Generated project/src/test/java/com/springbook/application/ApplicationTests.java @@ -1,16 +1,13 @@ package com.springbook.application; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -public class ApplicationTests { +class ApplicationTests { @Test - public void contextLoads() { + void contextLoads() { } } diff --git a/chapter02/02 - Logging config/.gitignore b/chapter02/02 - Logging config/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter02/02 - Logging config/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter02/02 - Logging config/.mvn/wrapper/maven-wrapper.jar b/chapter02/02 - Logging config/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter02/02 - Logging config/.mvn/wrapper/maven-wrapper.properties b/chapter02/02 - Logging config/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter02/02 - Logging config/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter02/02 - Logging config/mvnw b/chapter02/02 - Logging config/mvnw index 5bf251c..66df285 100755 --- a/chapter02/02 - Logging config/mvnw +++ b/chapter02/02 - Logging config/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter02/02 - Logging config/mvnw.cmd b/chapter02/02 - Logging config/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter02/02 - Logging config/mvnw.cmd +++ b/chapter02/02 - Logging config/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter02/02 - Logging config/pom.xml b/chapter02/02 - Logging config/pom.xml index 3524c80..fb6a34c 100644 --- a/chapter02/02 - Logging config/pom.xml +++ b/chapter02/02 - Logging config/pom.xml @@ -1,42 +1,29 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - com.springbook - application - 0.0.1-SNAPSHOT - jar - - application - Demo project for Spring Boot - org.springframework.boot spring-boot-starter-parent - 2.1.4.RELEASE + 3.2.2 - + com.springbook + application + 0.0.1-SNAPSHOT + application + Demo project for Spring Boot - UTF-8 - UTF-8 - 1.8 + 17 - - + org.springframework.boot spring-boot-starter-web - + - - org.projectlombok - lombok - true - org.springframework.boot spring-boot-starter-test @@ -63,5 +50,4 @@ - diff --git a/chapter02/02 - Logging config/src/test/java/com/springbook/application/ApplicationTests.java b/chapter02/02 - Logging config/src/test/java/com/springbook/application/ApplicationTests.java index 618c39d..ee8ff1a 100644 --- a/chapter02/02 - Logging config/src/test/java/com/springbook/application/ApplicationTests.java +++ b/chapter02/02 - Logging config/src/test/java/com/springbook/application/ApplicationTests.java @@ -1,16 +1,13 @@ package com.springbook.application; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -public class ApplicationTests { +class ApplicationTests { @Test - public void contextLoads() { + void contextLoads() { } } diff --git a/chapter03/01 - Generated project/.gitignore b/chapter03/01 - Generated project/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter03/01 - Generated project/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter03/01 - Generated project/.mvn/wrapper/maven-wrapper.jar b/chapter03/01 - Generated project/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter03/01 - Generated project/.mvn/wrapper/maven-wrapper.properties b/chapter03/01 - Generated project/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter03/01 - Generated project/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter03/01 - Generated project/mvnw b/chapter03/01 - Generated project/mvnw index 5bf251c..66df285 100755 --- a/chapter03/01 - Generated project/mvnw +++ b/chapter03/01 - Generated project/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter03/01 - Generated project/mvnw.cmd b/chapter03/01 - Generated project/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter03/01 - Generated project/mvnw.cmd +++ b/chapter03/01 - Generated project/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter03/01 - Generated project/pom.xml b/chapter03/01 - Generated project/pom.xml index 611e34d..61e2c35 100644 --- a/chapter03/01 - Generated project/pom.xml +++ b/chapter03/01 - Generated project/pom.xml @@ -1,50 +1,46 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar - - copsboot - Demo project for Spring Boot - org.springframework.boot spring-boot-starter-parent - 2.1.4.RELEASE + 3.2.2 - + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot - UTF-8 - UTF-8 - 1.8 + 17 org.springframework.boot - spring-boot-starter-data-jpa + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security org.springframework.boot - spring-boot-starter-security + spring-boot-starter-validation org.springframework.boot - spring-boot-starter-web + spring-boot-starter-web com.h2database - h2 + h2 runtime - org.springframework.boot spring-boot-starter-test @@ -52,7 +48,7 @@ org.springframework.security - spring-security-test + spring-security-test test @@ -67,5 +63,4 @@ - diff --git a/chapter03/01 - Generated project/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter03/01 - Generated project/src/main/java/com/example/copsboot/CopsbootApplication.java index 5774a17..7b031d7 100644 --- a/chapter03/01 - Generated project/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter03/01 - Generated project/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -9,4 +9,5 @@ public class CopsbootApplication { public static void main(String[] args) { SpringApplication.run(CopsbootApplication.class, args); } + } diff --git a/chapter03/01 - Generated project/src/main/resources/application.properties b/chapter03/01 - Generated project/src/main/resources/application.properties index e69de29..8b13789 100644 --- a/chapter03/01 - Generated project/src/main/resources/application.properties +++ b/chapter03/01 - Generated project/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/chapter03/01 - Generated project/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter03/01 - Generated project/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index a9797c6..73e7b68 100644 --- a/chapter03/01 - Generated project/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter03/01 - Generated project/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,16 +1,13 @@ package com.example.copsboot; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -public class CopsbootApplicationTests { +class CopsbootApplicationTests { @Test - public void contextLoads() { + void contextLoads() { } } diff --git a/chapter04/01 - User domain/.gitignore b/chapter04/01 - User domain/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter04/01 - User domain/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter04/01 - User domain/.mvn/wrapper/maven-wrapper.jar b/chapter04/01 - User domain/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter04/01 - User domain/.mvn/wrapper/maven-wrapper.properties b/chapter04/01 - User domain/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter04/01 - User domain/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter04/01 - User domain/mvnw b/chapter04/01 - User domain/mvnw index 5bf251c..66df285 100755 --- a/chapter04/01 - User domain/mvnw +++ b/chapter04/01 - User domain/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter04/01 - User domain/mvnw.cmd b/chapter04/01 - User domain/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter04/01 - User domain/mvnw.cmd +++ b/chapter04/01 - User domain/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter04/01 - User domain/pom.xml b/chapter04/01 - User domain/pom.xml index 7178a28..c3c6b3d 100644 --- a/chapter04/01 - User domain/pom.xml +++ b/chapter04/01 - User domain/pom.xml @@ -1,42 +1,39 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar - - copsboot - Demo project for Spring Boot - org.springframework.boot spring-boot-starter-parent - 2.1.4.RELEASE + 3.2.2 - + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot - UTF-8 - UTF-8 - 1.8 + 17 - + org.springframework.boot - spring-boot-starter-data-jpa + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security org.springframework.boot - spring-boot-starter-security + spring-boot-starter-validation org.springframework.boot - spring-boot-starter-web + spring-boot-starter-web @@ -44,7 +41,6 @@ h2 runtime - org.springframework.boot spring-boot-starter-test @@ -52,11 +48,11 @@ org.springframework.security - spring-security-test + spring-security-test test - + @@ -67,5 +63,4 @@ - diff --git a/chapter04/01 - User domain/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter04/01 - User domain/src/main/java/com/example/copsboot/CopsbootApplication.java index 5774a17..7b031d7 100644 --- a/chapter04/01 - User domain/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter04/01 - User domain/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -9,4 +9,5 @@ public class CopsbootApplication { public static void main(String[] args) { SpringApplication.run(CopsbootApplication.class, args); } + } diff --git a/chapter04/01 - User domain/src/main/resources/application.properties b/chapter04/01 - User domain/src/main/resources/application.properties index e69de29..8b13789 100644 --- a/chapter04/01 - User domain/src/main/resources/application.properties +++ b/chapter04/01 - User domain/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/chapter04/01 - User domain/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter04/01 - User domain/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index a9797c6..73e7b68 100644 --- a/chapter04/01 - User domain/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter04/01 - User domain/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,16 +1,13 @@ package com.example.copsboot; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -public class CopsbootApplicationTests { +class CopsbootApplicationTests { @Test - public void contextLoads() { + void contextLoads() { } } diff --git a/chapter04/02 - User with JPA/.gitignore b/chapter04/02 - User with JPA/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter04/02 - User with JPA/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter04/02 - User with JPA/.mvn/wrapper/maven-wrapper.jar b/chapter04/02 - User with JPA/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter04/02 - User with JPA/.mvn/wrapper/maven-wrapper.properties b/chapter04/02 - User with JPA/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter04/02 - User with JPA/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter04/02 - User with JPA/mvnw b/chapter04/02 - User with JPA/mvnw index 5bf251c..66df285 100755 --- a/chapter04/02 - User with JPA/mvnw +++ b/chapter04/02 - User with JPA/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter04/02 - User with JPA/mvnw.cmd b/chapter04/02 - User with JPA/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter04/02 - User with JPA/mvnw.cmd +++ b/chapter04/02 - User with JPA/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter04/02 - User with JPA/pom.xml b/chapter04/02 - User with JPA/pom.xml index 5d116ae..9497e89 100644 --- a/chapter04/02 - User with JPA/pom.xml +++ b/chapter04/02 - User with JPA/pom.xml @@ -1,79 +1,68 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + 17 + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - - com.h2database - h2 - runtime - - - - - org.assertj - assertj-core - test - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + diff --git a/chapter04/02 - User with JPA/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter04/02 - User with JPA/src/main/java/com/example/copsboot/CopsbootApplication.java index 5774a17..7b031d7 100644 --- a/chapter04/02 - User with JPA/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter04/02 - User with JPA/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -9,4 +9,5 @@ public class CopsbootApplication { public static void main(String[] args) { SpringApplication.run(CopsbootApplication.class, args); } + } diff --git a/chapter04/02 - User with JPA/src/main/java/com/example/copsboot/user/User.java b/chapter04/02 - User with JPA/src/main/java/com/example/copsboot/user/User.java index ef1f991..21d4368 100644 --- a/chapter04/02 - User with JPA/src/main/java/com/example/copsboot/user/User.java +++ b/chapter04/02 - User with JPA/src/main/java/com/example/copsboot/user/User.java @@ -1,8 +1,14 @@ //tag::annotations-part[] package com.example.copsboot.user; -import javax.persistence.*; -import javax.validation.constraints.NotNull; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; import java.util.Set; import java.util.UUID; diff --git a/chapter04/02 - User with JPA/src/main/resources/application.properties b/chapter04/02 - User with JPA/src/main/resources/application.properties index e69de29..8b13789 100644 --- a/chapter04/02 - User with JPA/src/main/resources/application.properties +++ b/chapter04/02 - User with JPA/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/chapter04/02 - User with JPA/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter04/02 - User with JPA/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index a9797c6..73e7b68 100644 --- a/chapter04/02 - User with JPA/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter04/02 - User with JPA/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,16 +1,13 @@ package com.example.copsboot; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -public class CopsbootApplicationTests { +class CopsbootApplicationTests { @Test - public void contextLoads() { + void contextLoads() { } } diff --git a/chapter04/02 - User with JPA/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter04/02 - User with JPA/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 65ccaad..f3d8176 100644 --- a/chapter04/02 - User with JPA/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter04/02 - User with JPA/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -1,7 +1,6 @@ package com.example.copsboot.user; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.test.context.junit4.SpringRunner; @@ -11,23 +10,22 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) //<1> -@DataJpaTest //<2> +@DataJpaTest // <.> public class UserRepositoryTest { @Autowired - private UserRepository repository; //<3> + private UserRepository repository; // <.> @Test - public void testStoreUser() { //<4> + public void testStoreUser() { // <.> HashSet roles = new HashSet<>(); roles.add(UserRole.OFFICER); - User user = repository.save(new User(UUID.randomUUID(), //<5> + User user = repository.save(new User(UUID.randomUUID(), // <.> "alex.foley@beverly-hills.com", "my-secret-pwd", roles)); - assertThat(user).isNotNull(); //<6> + assertThat(user).isNotNull(); // <.> - assertThat(repository.count()).isEqualTo(1L); //<7> + assertThat(repository.count()).isEqualTo(1L); // <.> } -} \ No newline at end of file +} diff --git a/chapter04/03 - User with JPA refactored/.gitignore b/chapter04/03 - User with JPA refactored/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter04/03 - User with JPA refactored/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter04/03 - User with JPA refactored/.mvn/wrapper/maven-wrapper.jar b/chapter04/03 - User with JPA refactored/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter04/03 - User with JPA refactored/.mvn/wrapper/maven-wrapper.properties b/chapter04/03 - User with JPA refactored/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter04/03 - User with JPA refactored/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter04/03 - User with JPA refactored/mvnw b/chapter04/03 - User with JPA refactored/mvnw index 5bf251c..66df285 100755 --- a/chapter04/03 - User with JPA refactored/mvnw +++ b/chapter04/03 - User with JPA refactored/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter04/03 - User with JPA refactored/mvnw.cmd b/chapter04/03 - User with JPA refactored/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter04/03 - User with JPA refactored/mvnw.cmd +++ b/chapter04/03 - User with JPA refactored/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter04/03 - User with JPA refactored/pom.xml b/chapter04/03 - User with JPA refactored/pom.xml index 5cf3142..d13fbb7 100644 --- a/chapter04/03 - User with JPA refactored/pom.xml +++ b/chapter04/03 - User with JPA refactored/pom.xml @@ -1,87 +1,76 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + com.google.guava + guava + ${guava.version} + - copsboot - Demo project for Spring Boot + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - 27.1-jre - - - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-web - - - com.google.guava - guava - ${guava.version} - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - - com.h2database - h2 - runtime - - - - - org.assertj - assertj-core - test - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + diff --git a/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/CopsbootApplication.java index f360096..7b031d7 100644 --- a/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,12 +1,7 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; - -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { @@ -15,10 +10,4 @@ public static void main(String[] args) { SpringApplication.run(CopsbootApplication.class, args); } - //tag::unique-id-generator[] - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - //end::unique-id-generator[] } diff --git a/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..2db0e8c --- /dev/null +++ b/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,16 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import java.util.UUID; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } +} diff --git a/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/user/User.java b/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/user/User.java index af92199..68b1cbd 100644 --- a/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/user/User.java +++ b/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/user/User.java @@ -2,8 +2,14 @@ import com.example.orm.jpa.AbstractEntity; -import javax.persistence.*; -import javax.validation.constraints.NotNull; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; import java.util.Set; import java.util.UUID; diff --git a/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/user/UserRepository.java index e5b6d1f..5109326 100644 --- a/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/user/UserRepository.java @@ -4,6 +4,6 @@ import java.util.UUID; //tag::class[] -public interface UserRepository extends CrudRepository, UserRepositoryCustom { +public interface UserRepository extends CrudRepository, UserRepositoryCustom { } //end::class[] diff --git a/chapter04/03 - User with JPA refactored/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter04/03 - User with JPA refactored/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter04/03 - User with JPA refactored/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter04/03 - User with JPA refactored/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter04/03 - User with JPA refactored/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter04/03 - User with JPA refactored/src/main/java/com/example/orm/jpa/AbstractEntityId.java index db748ae..4902f41 100755 --- a/chapter04/03 - User with JPA refactored/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter04/03 - User with JPA refactored/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter04/03 - User with JPA refactored/src/main/resources/application.properties b/chapter04/03 - User with JPA refactored/src/main/resources/application.properties index e69de29..8b13789 100644 --- a/chapter04/03 - User with JPA refactored/src/main/resources/application.properties +++ b/chapter04/03 - User with JPA refactored/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/chapter04/03 - User with JPA refactored/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter04/03 - User with JPA refactored/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index a9797c6..73e7b68 100644 --- a/chapter04/03 - User with JPA refactored/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter04/03 - User with JPA refactored/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,16 +1,13 @@ package com.example.copsboot; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -public class CopsbootApplicationTests { +class CopsbootApplicationTests { @Test - public void contextLoads() { + void contextLoads() { } } diff --git a/chapter04/03 - User with JPA refactored/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter04/03 - User with JPA refactored/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index c525776..6260f64 100644 --- a/chapter04/03 - User with JPA refactored/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter04/03 - User with JPA refactored/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -2,8 +2,7 @@ import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; @@ -15,7 +14,6 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest public class UserRepositoryTest { @@ -46,4 +44,4 @@ public UniqueIdGenerator generator() { } } //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter05/01 - Oauth2/.gitignore b/chapter05/01 - Oauth2/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter05/01 - Oauth2/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter05/01 - Oauth2/.mvn/wrapper/maven-wrapper.jar b/chapter05/01 - Oauth2/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter05/01 - Oauth2/.mvn/wrapper/maven-wrapper.properties b/chapter05/01 - Oauth2/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter05/01 - Oauth2/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter05/01 - Oauth2/docker-compose.yaml b/chapter05/01 - Oauth2/docker-compose.yaml new file mode 100644 index 0000000..7f8428d --- /dev/null +++ b/chapter05/01 - Oauth2/docker-compose.yaml @@ -0,0 +1,14 @@ +version: '3' +services: + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter05/01 - Oauth2/mvnw b/chapter05/01 - Oauth2/mvnw index 5bf251c..66df285 100755 --- a/chapter05/01 - Oauth2/mvnw +++ b/chapter05/01 - Oauth2/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter05/01 - Oauth2/mvnw.cmd b/chapter05/01 - Oauth2/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter05/01 - Oauth2/mvnw.cmd +++ b/chapter05/01 - Oauth2/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter05/01 - Oauth2/pom.xml b/chapter05/01 - Oauth2/pom.xml index 2d2e059..912a5ae 100644 --- a/chapter05/01 - Oauth2/pom.xml +++ b/chapter05/01 - Oauth2/pom.xml @@ -1,94 +1,83 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + - - - UTF-8 - UTF-8 - 1.8 - - 27.1-jre - - - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - - org.springframework.boot - spring-boot-starter-web - - - com.google.guava - guava - ${guava.version} - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - - com.h2database - h2 - runtime - - - - - org.assertj - assertj-core - test - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 0313f8b..0000000 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { //<1> - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index 7e03add..0000000 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - //tag::resource-server[] - @Configuration - @EnableResourceServer //<1> - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() //<2> - .and() - .antMatcher("/api/**").authorizeRequests() - .anyRequest().authenticated(); //<3> - } - } - //end::resource-server[] - - //tag::authorization-server[] - @Configuration - @EnableAuthorizationServer //<1> - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; //<2> - - @Autowired - private PasswordEncoder passwordEncoder; //<3> - - @Autowired - private TokenStore tokenStore; //<4> - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); //<3> - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() // <5> - .withClient("copsboot-mobile-client") //<6> - .authorizedGrantTypes("password", "refresh_token") //<7> - .scopes("mobile_app") //<8> - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode("ccUyb6vS4S8nxfbKPCrN")); //<9> - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - //end::authorization-server[] - - //tag::web-security[] - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } - //end::web-security[] -} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..3fd01ba --- /dev/null +++ b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,24 @@ +package com.example.copsboot.infrastructure.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class WebSecurityConfiguration { + @Bean + SecurityFilterChain configureSecurityFilterChain(HttpSecurity http) throws Exception { + + http + .authorizeHttpRequests(authorizeRequests -> authorizeRequests + .requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() //<.> + .requestMatchers("/api/**").authenticated() //<.> + .anyRequest().authenticated()) + .oauth2ResourceServer(it -> it.jwt(Customizer.withDefaults())); //<.> + + return http.build(); + } +} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/User.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/User.java index 236cd6d..e6cf8d2 100644 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/User.java +++ b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/User.java @@ -1,13 +1,11 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; -import javax.persistence.*; -import javax.validation.constraints.NotNull; import java.util.Set; - @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { @@ -32,11 +30,11 @@ public User(UserId id, String email, String password, Set roles) { } public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); + return new User(userId, email, encodedPassword, Set.of(UserRole.OFFICER)); } public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + return new User(userId, email, encodedPassword, Set.of(UserRole.CAPTAIN)); } public String getEmail() { diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserRepository.java index 37eda97..c72d5e4 100644 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,8 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] -public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); +public interface UserRepository extends CrudRepository, UserRepositoryCustom { } //end::class[] diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserService.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserService.java deleted file mode 100644 index b87df7e..0000000 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.copsboot.user; - -public interface UserService { - User createOfficer(String email, String password); -} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 6d45ead..0000000 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } -} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/web/UserRestController.java new file mode 100644 index 0000000..4dd76a9 --- /dev/null +++ b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user.web; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +@RequestMapping("/api/users") +public class UserRestController { + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + return Map.of("subject", jwt.getSubject(), //<.> + "claims", jwt.getClaims()); + } +} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/Entity.java b/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/Entity.java index 96cadf0..3a45231 100644 --- a/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/Entity.java @@ -1,5 +1,6 @@ package com.example.orm.jpa; + /** * Interface for entity objects. * diff --git a/chapter05/01 - Oauth2/src/main/resources/application.properties b/chapter05/01 - Oauth2/src/main/resources/application.properties index e69de29..27301ca 100644 --- a/chapter05/01 - Oauth2/src/main/resources/application.properties +++ b/chapter05/01 - Oauth2/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/copsboot diff --git a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index a9797c6..73e7b68 100644 --- a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,16 +1,13 @@ package com.example.copsboot; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -public class CopsbootApplicationTests { +class CopsbootApplicationTests { @Test - public void contextLoads() { + void contextLoads() { } } diff --git a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 41f8990..0000000 --- a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - service.loadUserByUsername("i@donotexist.com"); - } -} \ No newline at end of file diff --git a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 1d79252..0000000 --- a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "copsboot-mobile-client"; - String clientSecret = "ccUyb6vS4S8nxfbKPCrN"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index ad7aa55..c349713 100644 --- a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -2,13 +2,11 @@ import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -17,7 +15,6 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest public class UserRepositoryTest { @@ -29,48 +26,16 @@ public class UserRepositoryTest { public void testStoreUser() { HashSet roles = new HashSet<>(); roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> + User user = repository.save(new User(repository.nextId(), "alex.foley@beverly-hills.com", "my-secret-pwd", roles)); - assertThat(user).isNotNull(); //<6> + assertThat(user).isNotNull(); - assertThat(repository.count()).isEqualTo(1L); //<7> + assertThat(repository.count()).isEqualTo(1L); } //end::testStoreUser[] - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); - - assertThat(optional).isEmpty(); - } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @@ -80,4 +45,4 @@ public UniqueIdGenerator generator() { } } //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/Users.java b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 557cd04..0000000 --- a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } -} diff --git a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java new file mode 100644 index 0000000..8f6b763 --- /dev/null +++ b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -0,0 +1,35 @@ +package com.example.copsboot.user.web; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(UserRestController.class) //<.> +class UserRestControllerTest { + + @Autowired + private MockMvc mockMvc; //<.> + + @Test + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) //<.> + .andExpect(status().isUnauthorized()); //<.> + } + + @Test + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + mockMvc.perform(get("/api/users/me") + .with(jwt())) //<.> + .andExpect(status().isOk()) //<.> + .andExpect(jsonPath("subject").value("user")) //<.> + .andExpect(jsonPath("claims").isMap()) //<.> + .andDo(print()); //<.> + } +} diff --git a/chapter05/02 - Oauth configurable/mvnw b/chapter05/02 - Oauth configurable/mvnw deleted file mode 100755 index 5bf251c..0000000 --- a/chapter05/02 - Oauth configurable/mvnw +++ /dev/null @@ -1,225 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Migwn, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter05/02 - Oauth configurable/pom.xml b/chapter05/02 - Oauth configurable/pom.xml deleted file mode 100644 index 455446f..0000000 --- a/chapter05/02 - Oauth configurable/pom.xml +++ /dev/null @@ -1,95 +0,0 @@ - - - 4.0.0 - - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar - - copsboot - Demo project for Spring Boot - - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - 27.1-jre - - - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - com.google.guava - guava - ${guava.version} - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - - diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/CopsbootApplication.java deleted file mode 100644 index f4e3307..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/CopsbootApplication.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.copsboot; - -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; - -@SpringBootApplication -public class CopsbootApplication { - - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index 04353db..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - //tag::resource-server[] - @Configuration - @EnableResourceServer //<1> - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() //<2> - .and() - .antMatcher("/api/**").authorizeRequests() - .anyRequest().authenticated(); //<3> - } - } - //end::resource-server[] - - //tag::authorization-server[] - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; //<1> - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) //<2> - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); //<3> - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - //end::authorization-server[] - - //tag::web-security[] - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } - //end::web-security[] -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/User.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/User.java deleted file mode 100644 index 236cd6d..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/User.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.copsboot.user; - -import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - - -@Entity -@Table(name = "copsboot_user") -public class User extends AbstractEntity { - - private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; - - protected User() { - - } - - public User(UserId id, String email, String password, Set roles) { - super(id); - this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); - } - - public String getEmail() { - return email; - } - - public String getPassword() { - return password; - } - - public Set getRoles() { - return roles; - } -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRepository.java deleted file mode 100644 index 37eda97..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.data.repository.CrudRepository; - -import java.util.Optional; -import java.util.UUID; -//tag::class[] -public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); -} -//end::class[] diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserService.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserService.java deleted file mode 100644 index b87df7e..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.copsboot.user; - -public interface UserService { - User createOfficer(String email, String password); -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 6d45ead..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/Entity.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/Entity.java deleted file mode 100644 index 96cadf0..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/Entity.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.orm.jpa; - -/** - * Interface for entity objects. - * - * @param the type of {@link EntityId} that will be used in this entity - */ -public interface Entity { - - T getId(); -} diff --git a/chapter05/02 - Oauth configurable/src/main/resources/application-dev.properties b/chapter05/02 - Oauth configurable/src/main/resources/application-dev.properties deleted file mode 100644 index 819196a..0000000 --- a/chapter05/02 - Oauth configurable/src/main/resources/application-dev.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN \ No newline at end of file diff --git a/chapter05/02 - Oauth configurable/src/main/resources/application.properties b/chapter05/02 - Oauth configurable/src/main/resources/application.properties deleted file mode 100644 index e69de29..0000000 diff --git a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/CopsbootApplicationTests.java deleted file mode 100644 index add5a9b..0000000 --- a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -@RunWith(SpringRunner.class) -@SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { - - @Test - public void contextLoads() { - } - -} diff --git a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/user/UserRepositoryTest.java deleted file mode 100644 index ad7aa55..0000000 --- a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.example.copsboot.user; - -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.test.context.junit4.SpringRunner; - -import java.util.HashSet; -import java.util.Locale; -import java.util.Optional; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -public class UserRepositoryTest { - - @Autowired - private UserRepository repository; - - //tag::testStoreUser[] - @Test - public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] - - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); - - assertThat(optional).isEmpty(); - } - //end::find-by-email-tests[] - - //tag::testconfig[] - @TestConfiguration - static class TestConfig { - @Bean - public UniqueIdGenerator generator() { - return new InMemoryUniqueIdGenerator(); - } - } - //end::testconfig[] -} \ No newline at end of file diff --git a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/user/Users.java b/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 557cd04..0000000 --- a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } -} diff --git a/chapter05/02 - Oauth configurable/src/test/resources/application-test.properties b/chapter05/02 - Oauth configurable/src/test/resources/application-test.properties deleted file mode 100644 index 78c3fdb..0000000 --- a/chapter05/02 - Oauth configurable/src/test/resources/application-test.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret \ No newline at end of file diff --git a/chapter06/01 - User rest controller/.gitignore b/chapter06/01 - User rest controller/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter06/01 - User rest controller/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter06/01 - User rest controller/.mvn/wrapper/maven-wrapper.jar b/chapter06/01 - User rest controller/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter06/01 - User rest controller/.mvn/wrapper/maven-wrapper.properties b/chapter06/01 - User rest controller/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter06/01 - User rest controller/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter06/01 - User rest controller/docker-compose.yaml b/chapter06/01 - User rest controller/docker-compose.yaml new file mode 100644 index 0000000..7f8428d --- /dev/null +++ b/chapter06/01 - User rest controller/docker-compose.yaml @@ -0,0 +1,14 @@ +version: '3' +services: + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter06/01 - User rest controller/mvnw b/chapter06/01 - User rest controller/mvnw index 5bf251c..66df285 100755 --- a/chapter06/01 - User rest controller/mvnw +++ b/chapter06/01 - User rest controller/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter06/01 - User rest controller/mvnw.cmd b/chapter06/01 - User rest controller/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter06/01 - User rest controller/mvnw.cmd +++ b/chapter06/01 - User rest controller/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter06/01 - User rest controller/pom.xml b/chapter06/01 - User rest controller/pom.xml index e88aaca..912a5ae 100644 --- a/chapter06/01 - User rest controller/pom.xml +++ b/chapter06/01 - User rest controller/pom.xml @@ -1,101 +1,83 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - 27.1-jre - - - + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - com.google.guava - guava - ${guava.version} - - - - org.projectlombok - lombok - - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index 4ce4da8..0000000 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - //tag::resource-server[] - @Configuration - @EnableResourceServer //<1> - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() //<2> - .and() - .antMatcher("/api/**") - .authorizeRequests() - .anyRequest().authenticated(); //<3> - } - } - //end::resource-server[] - - //tag::authorization-server[] - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; //<1> - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) //<2> - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); //<3> - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - //end::authorization-server[] - - //tag::web-security[] - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } - //end::web-security[] -} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..3fd01ba --- /dev/null +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,24 @@ +package com.example.copsboot.infrastructure.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class WebSecurityConfiguration { + @Bean + SecurityFilterChain configureSecurityFilterChain(HttpSecurity http) throws Exception { + + http + .authorizeHttpRequests(authorizeRequests -> authorizeRequests + .requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() //<.> + .requestMatchers("/api/**").authenticated() //<.> + .anyRequest().authenticated()) + .oauth2ResourceServer(it -> it.jwt(Customizer.withDefaults())); //<.> + + return http.build(); + } +} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/User.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/User.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserNotFoundException.java deleted file mode 100644 index 1f65f04..0000000 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> -public class UserNotFoundException extends RuntimeException { - public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); - } -} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserService.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..61846a5 100644 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,28 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } - Optional getUser(UserId userId); + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } + // end::createUser[] } diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..0d8f0ab --- /dev/null +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,16 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); //<.> + String email = jwt.getClaimAsString("email"); //<.> + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/UserRestController.java index e27f788..6000867 100644 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,30 +1,50 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.*; -@RestController //<1> -@RequestMapping("/api/users") //<2> +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +@RestController +@RequestMapping("/api/users") public class UserRestController { + private final UserService userService; + + public UserRestController(UserService userService) { + this.userService = userService; + } + + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); - private final UserService service; + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); - @Autowired - public UserRestController(UserService service) { //<3> - this.service = service; + return result; } + // end::myself[] - @GetMapping("/me") //<4> - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { //<5> - User user = service.getUser(userDetails.getUserId()) //<6> - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); //<7> + // tag::createUser[] + @PostMapping + @ResponseStatus(HttpStatus.CREATED) //<.> + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @RequestBody CreateUserRequest request) { //<.> + CreateUserParameters parameters = request.toParameters(jwt); //<.> + User user = userService.createUser(parameters); + return UserDto.fromUser(user); //<.> } + // end::createUser[] } diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/Entity.java b/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/Entity.java index 96cadf0..3a45231 100644 --- a/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/Entity.java @@ -1,5 +1,6 @@ package com.example.orm.jpa; + /** * Interface for entity objects. * diff --git a/chapter06/01 - User rest controller/src/main/resources/application-dev.properties b/chapter06/01 - User rest controller/src/main/resources/application-dev.properties deleted file mode 100644 index 819196a..0000000 --- a/chapter06/01 - User rest controller/src/main/resources/application-dev.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN \ No newline at end of file diff --git a/chapter06/01 - User rest controller/src/main/resources/application.properties b/chapter06/01 - User rest controller/src/main/resources/application.properties index e69de29..27301ca 100644 --- a/chapter06/01 - User rest controller/src/main/resources/application.properties +++ b/chapter06/01 - User rest controller/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/copsboot diff --git a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..73e7b68 100644 --- a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,13 @@ package com.example.copsboot; -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index b01a4ed..0000000 --- a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index ad7aa55..b37e583 100644 --- a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -2,13 +2,11 @@ import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -17,7 +15,6 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest public class UserRepositoryTest { @@ -27,50 +24,16 @@ public class UserRepositoryTest { //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - assertThat(repository.count()).isEqualTo(1L); //<7> + assertThat(repository.count()).isEqualTo(1L); } //end::testStoreUser[] - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); - - assertThat(optional).isEmpty(); - } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @@ -80,4 +43,4 @@ public UniqueIdGenerator generator() { } } //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/Users.java b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 557cd04..0000000 --- a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } -} diff --git a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index b1fe1e0..8d3910e 100644 --- a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,104 +1,43 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::webmvctest[] -@RunWith(SpringRunner.class) //<1> -@WebMvcTest(UserRestController.class) //<2> -@ActiveProfiles(SpringProfiles.TEST) //<3> -public class UserRestControllerTest { +@WebMvcTest(UserRestController.class) //<.> +class UserRestControllerTest { @Autowired - private MockMvc mvc; //<4> + private MockMvc mockMvc; //<.> @MockBean - private UserService service; //<5> + private UserService userService; - //end::webmvctest[] - //tag::notauth[] @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) //<1> - .andExpect(status().isUnauthorized()); //<2> + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) //<.> + .andExpect(status().isUnauthorized()); //<.> } - //end::notauth[] - //tag::authofficer[] @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<1> - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); //<2> - - mvc.perform(get("/api/users/me") //<3> - .header(HEADER_AUTHORIZATION, bearer(accessToken))) //<4> - .andExpect(status().isOk()) //<5> - .andExpect(jsonPath("id").exists()) //<6> - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; - } - //end::authofficer[] - - //tag::authcaptain[] - @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); - } - //end::authcaptain[] - - //tag::testconfig[] - @TestConfiguration //<1> - @Import(OAuth2ServerConfiguration.class) //<2> - static class TestConfig { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); //<3> - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); //<5> - } - + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) //<.> + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()) //<.> + .andDo(print()); //<.> } - //end::testconfig[] } diff --git a/chapter06/01 - User rest controller/src/test/resources/application-test.properties b/chapter06/01 - User rest controller/src/test/resources/application-test.properties deleted file mode 100644 index 78c3fdb..0000000 --- a/chapter06/01 - User rest controller/src/test/resources/application-test.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret \ No newline at end of file diff --git a/chapter06/02 - Post mapping/mvnw b/chapter06/02 - Post mapping/mvnw deleted file mode 100755 index 5bf251c..0000000 --- a/chapter06/02 - Post mapping/mvnw +++ /dev/null @@ -1,225 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Migwn, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter06/02 - Post mapping/mvnw.cmd b/chapter06/02 - Post mapping/mvnw.cmd deleted file mode 100644 index 019bd74..0000000 --- a/chapter06/02 - Post mapping/mvnw.cmd +++ /dev/null @@ -1,143 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% diff --git a/chapter06/02 - Post mapping/pom.xml b/chapter06/02 - Post mapping/pom.xml deleted file mode 100644 index 35111c1..0000000 --- a/chapter06/02 - Post mapping/pom.xml +++ /dev/null @@ -1,101 +0,0 @@ - - - 4.0.0 - - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar - - copsboot - Demo project for Spring Boot - - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - 27.1-jre - - - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - com.google.guava - guava - ${guava.version} - - - - org.projectlombok - lombok - - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - - diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/CopsbootApplication.java deleted file mode 100644 index f4e3307..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/CopsbootApplication.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.copsboot; - -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; - -@SpringBootApplication -public class CopsbootApplication { - - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java deleted file mode 100644 index 1361ab0..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.copsboot.infrastructure; - -public final class SpringProfiles { - public static final String DEV = "dev"; - public static final String LOCAL = "local"; - public static final String TEST = "test"; - public static final String STAGING = "staging"; - public static final String PROD = "prod"; - - private SpringProfiles() { - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/User.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/User.java deleted file mode 100644 index 236cd6d..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/User.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.copsboot.user; - -import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - - -@Entity -@Table(name = "copsboot_user") -public class User extends AbstractEntity { - - private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; - - protected User() { - - } - - public User(UserId id, String email, String password, Set roles) { - super(id); - this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); - } - - public String getEmail() { - return email; - } - - public String getPassword() { - return password; - } - - public Set getRoles() { - return roles; - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserId.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserId.java deleted file mode 100644 index a112f47..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserId.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.copsboot.user; - -import com.example.orm.jpa.AbstractEntityId; - -import java.util.UUID; - -public class UserId extends AbstractEntityId { - - protected UserId() { //<1> - - } - - public UserId(UUID id) { //<2> - super(id); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserNotFoundException.java deleted file mode 100644 index 1f65f04..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> -public class UserNotFoundException extends RuntimeException { - public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepositoryCustom.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepositoryCustom.java deleted file mode 100644 index b848e3c..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepositoryCustom.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.copsboot.user; - -public interface UserRepositoryCustom { - UserId nextId(); -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepositoryImpl.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepositoryImpl.java deleted file mode 100644 index 6ddfbe1..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepositoryImpl.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user; - -import com.example.orm.jpa.UniqueIdGenerator; - -import java.util.UUID; - -public class UserRepositoryImpl implements UserRepositoryCustom { - private final UniqueIdGenerator generator; - - public UserRepositoryImpl(UniqueIdGenerator generator) { - this.generator = generator; - } - - @Override - public UserId nextId() { - return new UserId(generator.getNextUniqueId()); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRole.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRole.java deleted file mode 100644 index d750719..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRole.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.copsboot.user; - -public enum UserRole { - OFFICER, - CAPTAIN, - ADMIN -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserService.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserService.java deleted file mode 100644 index 9e155a3..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserService.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.copsboot.user; - -import java.util.Optional; - -public interface UserService { - User createOfficer(String email, String password); - - Optional getUser(UserId userId); -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/UserDto.java deleted file mode 100644 index 3769d1a..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/UserDto.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; - -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; - - public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/UserRestController.java deleted file mode 100644 index c74ccd8..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; - -import javax.validation.Valid; - -@RestController -@RequestMapping("/api/users") -public class UserRestController { - - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; - } - - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); - } - - //tag::post[] - @PostMapping //<1> - @ResponseStatus(HttpStatus.CREATED) //<2> - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { //<3> - User officer = service.createOfficer(parameters.getEmail(), //<4> - parameters.getPassword()); - return UserDto.fromUser(officer); //<5> - } - //end::post[] -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/AbstractEntity.java deleted file mode 100644 index dfa9f1e..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.orm.jpa; - -import com.example.util.ArtifactForFramework; - -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; -import java.util.Objects; - -import static com.google.common.base.MoreObjects.toStringHelper; -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * Abstract super class for entities. We are assuming that early primary key - * generation will be used. - * - * @param the type of {@link EntityId} that will be used for this entity - */ -@MappedSuperclass -public abstract class AbstractEntity implements Entity { - - @EmbeddedId - private T id; - - - @ArtifactForFramework - protected AbstractEntity() { - } - - public AbstractEntity(T id) { - this.id = checkNotNull(id); - } - - - @Override - public T getId() { - return id; - } - - @Override - public boolean equals(Object obj) { - boolean result = false; - - if (this == obj) { - result = true; - } else if (obj instanceof AbstractEntity) { - AbstractEntity other = (AbstractEntity) obj; - result = Objects.equals(id, other.id); - } - - return result; - } - - @Override - public int hashCode() { - return Objects.hash(id); - } - - @Override - public String toString() { - return toStringHelper(this) - .add("id", id) - .toString(); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/AbstractEntityId.java deleted file mode 100755 index b9ddc5b..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.example.orm.jpa; - -import com.example.util.ArtifactForFramework; - -import javax.persistence.MappedSuperclass; -import java.io.Serializable; -import java.util.Objects; - -import static com.google.common.base.MoreObjects.toStringHelper; -import static com.google.common.base.Preconditions.checkNotNull; - -@MappedSuperclass -public abstract class AbstractEntityId implements Serializable, EntityId { - private T id; - - @ArtifactForFramework - protected AbstractEntityId() { - } - - protected AbstractEntityId(T id) { - this.id = checkNotNull(id); - } - - @Override - public T getId() { - return id; - } - - @Override - public String asString() { - return id.toString(); - } - - @Override - public boolean equals(Object o) { - boolean result = false; - - if (this == o) { - result = true; - } else if (o instanceof AbstractEntityId) { - AbstractEntityId other = (AbstractEntityId) o; - result = Objects.equals(id, other.id); - } - - return result; - } - - @Override - public int hashCode() { - return Objects.hash(id); - } - - @Override - public String toString() { - return toStringHelper(this) - .add("id", id) - .toString(); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/EntityId.java b/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/EntityId.java deleted file mode 100644 index 53da1e7..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/EntityId.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.orm.jpa; - -import java.io.Serializable; - -/** - * Interface for primary keys of entities. - * - * @param the underlying type of the entity id - */ -public interface EntityId extends Serializable { - - T getId(); - - String asString(); //<1> -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/InMemoryUniqueIdGenerator.java b/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/InMemoryUniqueIdGenerator.java deleted file mode 100755 index 06e9521..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/InMemoryUniqueIdGenerator.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.example.orm.jpa; - -import java.util.UUID; - -public class InMemoryUniqueIdGenerator implements UniqueIdGenerator { - @Override - public UUID getNextUniqueId() { - return UUID.randomUUID(); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/UniqueIdGenerator.java b/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/UniqueIdGenerator.java deleted file mode 100755 index 1264905..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/UniqueIdGenerator.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.orm.jpa; - -public interface UniqueIdGenerator { - T getNextUniqueId(); -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/util/ArtifactForFramework.java b/chapter06/02 - Post mapping/src/main/java/com/example/util/ArtifactForFramework.java deleted file mode 100644 index 5d4ec38..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/util/ArtifactForFramework.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.util; - -public @interface ArtifactForFramework { -} diff --git a/chapter06/02 - Post mapping/src/main/resources/application-dev.properties b/chapter06/02 - Post mapping/src/main/resources/application-dev.properties deleted file mode 100644 index 819196a..0000000 --- a/chapter06/02 - Post mapping/src/main/resources/application-dev.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN \ No newline at end of file diff --git a/chapter06/02 - Post mapping/src/main/resources/application.properties b/chapter06/02 - Post mapping/src/main/resources/application.properties deleted file mode 100644 index e69de29..0000000 diff --git a/chapter06/02 - Post mapping/src/main/resources/logback-spring.xml b/chapter06/02 - Post mapping/src/main/resources/logback-spring.xml deleted file mode 100644 index 6aff652..0000000 --- a/chapter06/02 - Post mapping/src/main/resources/logback-spring.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/CopsbootApplicationTests.java deleted file mode 100644 index add5a9b..0000000 --- a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -@RunWith(SpringRunner.class) -@SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { - - @Test - public void contextLoads() { - } - -} diff --git a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index b01a4ed..0000000 --- a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/UserRepositoryTest.java deleted file mode 100644 index ad7aa55..0000000 --- a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.example.copsboot.user; - -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.test.context.junit4.SpringRunner; - -import java.util.HashSet; -import java.util.Locale; -import java.util.Optional; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -public class UserRepositoryTest { - - @Autowired - private UserRepository repository; - - //tag::testStoreUser[] - @Test - public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] - - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); - - assertThat(optional).isEmpty(); - } - //end::find-by-email-tests[] - - //tag::testconfig[] - @TestConfiguration - static class TestConfig { - @Bean - public UniqueIdGenerator generator() { - return new InMemoryUniqueIdGenerator(); - } - } - //end::testconfig[] -} \ No newline at end of file diff --git a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/Users.java b/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java deleted file mode 100644 index 2a0e431..0000000 --- a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.AdditionalAnswers; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.http.MediaType; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; - -import java.util.Optional; - -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@RunWith(SpringRunner.class) -@WebMvcTest(UserRestController.class) -@ActiveProfiles(SpringProfiles.TEST) -public class UserRestControllerTest { - - @Autowired - private MockMvc mvc; - - //tag::extra-fields[] - @Autowired - private ObjectMapper objectMapper; //<1> - @MockBean - private UserService service; //<2> - //end::extra-fields[] - - @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); - } - - @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; - } - - @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); - } - - //tag::test-create-officer[] - @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<1> - - mvc.perform(post("/api/users") //<2> - .contentType(MediaType.APPLICATION_JSON_UTF8) //<3> - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andExpect(jsonPath("id").exists()) //<6> - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); //<7> - } - //end::test-create-officer[] - - @TestConfiguration - @Import(OAuth2ServerConfiguration.class) - static class TestConfig { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - - } -} diff --git a/chapter06/02 - Post mapping/src/test/resources/application-test.properties b/chapter06/02 - Post mapping/src/test/resources/application-test.properties deleted file mode 100644 index 78c3fdb..0000000 --- a/chapter06/02 - Post mapping/src/test/resources/application-test.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret \ No newline at end of file diff --git a/chapter06/02 - Post mapping/src/test/resources/logback-test.xml b/chapter06/02 - Post mapping/src/test/resources/logback-test.xml deleted file mode 100644 index f81fa4a..0000000 --- a/chapter06/02 - Post mapping/src/test/resources/logback-test.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - %date{YYYY-MM-dd HH:mm:ss} %level [%thread] %logger{0} - %msg%n%ex - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/chapter06/02 - user role/.gitignore b/chapter06/02 - user role/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter06/02 - user role/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter06/02 - user role/.mvn/wrapper/maven-wrapper.jar b/chapter06/02 - user role/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter06/02 - user role/.mvn/wrapper/maven-wrapper.properties b/chapter06/02 - user role/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter06/02 - user role/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter06/02 - user role/docker-compose.yaml b/chapter06/02 - user role/docker-compose.yaml new file mode 100644 index 0000000..7f8428d --- /dev/null +++ b/chapter06/02 - user role/docker-compose.yaml @@ -0,0 +1,14 @@ +version: '3' +services: + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter06/02 - user role/mvnw b/chapter06/02 - user role/mvnw new file mode 100755 index 0000000..66df285 --- /dev/null +++ b/chapter06/02 - user role/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter05/02 - Oauth configurable/mvnw.cmd b/chapter06/02 - user role/mvnw.cmd similarity index 54% rename from chapter05/02 - Oauth configurable/mvnw.cmd rename to chapter06/02 - user role/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter05/02 - Oauth configurable/mvnw.cmd +++ b/chapter06/02 - user role/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter06/02 - user role/pom.xml b/chapter06/02 - user role/pom.xml new file mode 100644 index 0000000..2b02dae --- /dev/null +++ b/chapter06/02 - user role/pom.xml @@ -0,0 +1,96 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + + com.google.guava + guava + ${guava.version} + + + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/CopsbootApplication.java new file mode 100644 index 0000000..7b031d7 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -0,0 +1,13 @@ +package com.example.copsboot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CopsbootApplication { + + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } + +} diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java rename to chapter06/02 - user role/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/user/User.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/User.java new file mode 100644 index 0000000..32d02a4 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/User.java @@ -0,0 +1,37 @@ +package com.example.copsboot.user; + +import com.example.orm.jpa.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(name = "copsboot_user") +public class User extends AbstractEntity { + + private String email; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> + + protected User() { + + } + + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> + super(id); + this.email = email; + this.authServerId = authServerId; + this.mobileToken = mobileToken; + } + + public String getEmail() { + return email; + } + + public AuthServerId getAuthServerId() { //<.> + return authServerId; + } + + public String getMobileToken() { + return mobileToken; + } +} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserId.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserId.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserId.java rename to chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserId.java diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRepository.java similarity index 75% rename from chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepository.java rename to chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRepositoryCustom.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRepositoryCustom.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRepositoryCustom.java rename to chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRepositoryCustom.java diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRepositoryImpl.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRepositoryImpl.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRepositoryImpl.java rename to chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRepositoryImpl.java diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRole.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRole.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRole.java rename to chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRole.java diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserService.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserService.java new file mode 100644 index 0000000..61846a5 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserService.java @@ -0,0 +1,28 @@ +package com.example.copsboot.user; + +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } + + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } + // end::createUser[] +} diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..0d8f0ab --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,16 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); //<.> + String email = jwt.getClaimAsString("email"); //<.> + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/UserDto.java new file mode 100644 index 0000000..2fac96c --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -0,0 +1,14 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.User; + +import java.util.UUID; + +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { + public static UserDto fromUser(User user) { + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); + } +} diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/UserRestController.java new file mode 100644 index 0000000..cd2fe43 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -0,0 +1,52 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserService; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +@RestController +@RequestMapping("/api/users") +public class UserRestController { + private final UserService userService; + + public UserRestController(UserService userService) { + this.userService = userService; + } + + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; + } + // end::myself[] + + // tag::createUser[] + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @PreAuthorize("hasRole('OFFICER')") //<.> + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); + } + // end::createUser[] +} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/AbstractEntity.java similarity index 94% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/AbstractEntity.java rename to chapter06/02 - user role/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/AbstractEntityId.java similarity index 96% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/AbstractEntityId.java rename to chapter06/02 - user role/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/Entity.java b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/Entity.java similarity index 99% rename from chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/Entity.java rename to chapter06/02 - user role/src/main/java/com/example/orm/jpa/Entity.java index 96cadf0..3a45231 100644 --- a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/Entity.java @@ -1,5 +1,6 @@ package com.example.orm.jpa; + /** * Interface for entity objects. * diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/EntityId.java b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/EntityId.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/EntityId.java rename to chapter06/02 - user role/src/main/java/com/example/orm/jpa/EntityId.java diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/InMemoryUniqueIdGenerator.java b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/InMemoryUniqueIdGenerator.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/InMemoryUniqueIdGenerator.java rename to chapter06/02 - user role/src/main/java/com/example/orm/jpa/InMemoryUniqueIdGenerator.java diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/UniqueIdGenerator.java b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/UniqueIdGenerator.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/UniqueIdGenerator.java rename to chapter06/02 - user role/src/main/java/com/example/orm/jpa/UniqueIdGenerator.java diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/util/ArtifactForFramework.java b/chapter06/02 - user role/src/main/java/com/example/util/ArtifactForFramework.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/util/ArtifactForFramework.java rename to chapter06/02 - user role/src/main/java/com/example/util/ArtifactForFramework.java diff --git a/chapter06/02 - user role/src/main/resources/application.properties b/chapter06/02 - user role/src/main/resources/application.properties new file mode 100644 index 0000000..22c3363 --- /dev/null +++ b/chapter06/02 - user role/src/main/resources/application.properties @@ -0,0 +1,3 @@ +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter05/02 - Oauth configurable/src/main/resources/logback-spring.xml b/chapter06/02 - user role/src/main/resources/logback-spring.xml similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/resources/logback-spring.xml rename to chapter06/02 - user role/src/main/resources/logback-spring.xml diff --git a/chapter06/02 - user role/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter06/02 - user role/src/test/java/com/example/copsboot/CopsbootApplicationTests.java new file mode 100644 index 0000000..73e7b68 --- /dev/null +++ b/chapter06/02 - user role/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.copsboot; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CopsbootApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/chapter06/02 - user role/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter06/02 - user role/src/test/java/com/example/copsboot/user/UserRepositoryTest.java new file mode 100644 index 0000000..b37e583 --- /dev/null +++ b/chapter06/02 - user role/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -0,0 +1,46 @@ +package com.example.copsboot.user; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +public class UserRepositoryTest { + + @Autowired + private UserRepository repository; + + //tag::testStoreUser[] + @Test + public void testStoreUser() { + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); + + assertThat(repository.count()).isEqualTo(1L); + } + //end::testStoreUser[] + + //tag::testconfig[] + @TestConfiguration + static class TestConfig { + @Bean + public UniqueIdGenerator generator() { + return new InMemoryUniqueIdGenerator(); + } + } + //end::testconfig[] +} diff --git a/chapter06/02 - user role/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter06/02 - user role/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java new file mode 100644 index 0000000..2654f2c --- /dev/null +++ b/chapter06/02 - user role/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -0,0 +1,87 @@ +package com.example.copsboot.user.web; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(UserRestController.class) +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> +class UserRestControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserService userService; //<.> + + @Test + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); + } + + @Test + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); + } + + @Test + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); + } + + @Test + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> + } +} diff --git a/chapter05/02 - Oauth configurable/src/test/resources/logback-test.xml b/chapter06/02 - user role/src/test/resources/logback-test.xml similarity index 100% rename from chapter05/02 - Oauth configurable/src/test/resources/logback-test.xml rename to chapter06/02 - user role/src/test/resources/logback-test.xml diff --git a/chapter06/03 - Writing API Documentation/.gitignore b/chapter06/03 - Writing API Documentation/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter06/03 - Writing API Documentation/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter06/03 - Writing API Documentation/.mvn/wrapper/maven-wrapper.jar b/chapter06/03 - Writing API Documentation/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter06/03 - Writing API Documentation/.mvn/wrapper/maven-wrapper.properties b/chapter06/03 - Writing API Documentation/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter06/03 - Writing API Documentation/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter06/03 - Writing API Documentation/docker-compose.yaml b/chapter06/03 - Writing API Documentation/docker-compose.yaml new file mode 100644 index 0000000..7f8428d --- /dev/null +++ b/chapter06/03 - Writing API Documentation/docker-compose.yaml @@ -0,0 +1,14 @@ +version: '3' +services: + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter06/03 - Writing API Documentation/mvnw b/chapter06/03 - Writing API Documentation/mvnw index 5bf251c..66df285 100755 --- a/chapter06/03 - Writing API Documentation/mvnw +++ b/chapter06/03 - Writing API Documentation/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter06/03 - Writing API Documentation/mvnw.cmd b/chapter06/03 - Writing API Documentation/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter06/03 - Writing API Documentation/mvnw.cmd +++ b/chapter06/03 - Writing API Documentation/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter06/03 - Writing API Documentation/pom.xml b/chapter06/03 - Writing API Documentation/pom.xml index 5d57aab..960db1b 100644 --- a/chapter06/03 - Writing API Documentation/pom.xml +++ b/chapter06/03 - Writing API Documentation/pom.xml @@ -1,214 +1,169 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 27.1-jre - - - 2.0.3.RELEASE - - + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - com.google.guava - guava - ${guava.version} - - - - org.projectlombok - lombok - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter06/03 - Writing API Documentation/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter06/03 - Writing API Documentation/src/docs/asciidoc/Copsboot REST API Guide.adoc similarity index 100% rename from chapter06/03 - Writing API Documentation/src/main/asciidoc/Copsboot REST API Guide.adoc rename to chapter06/03 - Writing API Documentation/src/docs/asciidoc/Copsboot REST API Guide.adoc diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/User.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/User.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserNotFoundException.java deleted file mode 100644 index 1f65f04..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> -public class UserNotFoundException extends RuntimeException { - public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); - } -} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserService.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..61846a5 100644 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,28 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } - Optional getUser(UserId userId); + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } + // end::createUser[] } diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..0d8f0ab --- /dev/null +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,16 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); //<.> + String email = jwt.getClaimAsString("email"); //<.> + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/UserRestController.java index c74ccd8..cd2fe43 100644 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,41 +1,52 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] - @PostMapping //<1> - @ResponseStatus(HttpStatus.CREATED) //<2> - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { //<3> - User officer = service.createOfficer(parameters.getEmail(), //<4> - parameters.getPassword()); - return UserDto.fromUser(officer); //<5> + // tag::createUser[] + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @PreAuthorize("hasRole('OFFICER')") //<.> + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/Entity.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/Entity.java index 96cadf0..3a45231 100644 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/Entity.java @@ -1,5 +1,6 @@ package com.example.orm.jpa; + /** * Interface for entity objects. * diff --git a/chapter06/03 - Writing API Documentation/src/main/resources/application-dev.properties b/chapter06/03 - Writing API Documentation/src/main/resources/application-dev.properties deleted file mode 100644 index 819196a..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/resources/application-dev.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN \ No newline at end of file diff --git a/chapter06/03 - Writing API Documentation/src/main/resources/application.properties b/chapter06/03 - Writing API Documentation/src/main/resources/application.properties index e69de29..22c3363 100644 --- a/chapter06/03 - Writing API Documentation/src/main/resources/application.properties +++ b/chapter06/03 - Writing API Documentation/src/main/resources/application.properties @@ -0,0 +1,3 @@ +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..73e7b68 100644 --- a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,13 @@ package com.example.copsboot; -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index b01a4ed..0000000 --- a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index ad7aa55..b37e583 100644 --- a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -2,13 +2,11 @@ import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -17,7 +15,6 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest public class UserRepositoryTest { @@ -27,50 +24,16 @@ public class UserRepositoryTest { //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - assertThat(repository.count()).isEqualTo(1L); //<7> + assertThat(repository.count()).isEqualTo(1L); } //end::testStoreUser[] - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); - - assertThat(optional).isEmpty(); - } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @@ -80,4 +43,4 @@ public UniqueIdGenerator generator() { } } //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/Users.java b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index 2a0e431..2654f2c 100644 --- a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,128 +1,87 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.AdditionalAnswers; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.verify; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@RunWith(SpringRunner.class) @WebMvcTest(UserRestController.class) -@ActiveProfiles(SpringProfiles.TEST) -public class UserRestControllerTest { +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> +class UserRestControllerTest { @Autowired - private MockMvc mvc; + private MockMvc mockMvc; - //tag::extra-fields[] - @Autowired - private ObjectMapper objectMapper; //<1> @MockBean - private UserService service; //<2> - //end::extra-fields[] + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } - //tag::test-create-officer[] @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<1> - - mvc.perform(post("/api/users") //<2> - .contentType(MediaType.APPLICATION_JSON_UTF8) //<3> - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andExpect(jsonPath("id").exists()) //<6> - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); //<7> - } - //end::test-create-officer[] - - @TestConfiguration - @Import(OAuth2ServerConfiguration.class) - static class TestConfig { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } } diff --git a/chapter06/03 - Writing API Documentation/src/test/resources/application-test.properties b/chapter06/03 - Writing API Documentation/src/test/resources/application-test.properties deleted file mode 100644 index 78c3fdb..0000000 --- a/chapter06/03 - Writing API Documentation/src/test/resources/application-test.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret \ No newline at end of file diff --git a/chapter06/04 - Generating snippets/.gitignore b/chapter06/04 - Generating snippets/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter06/04 - Generating snippets/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter06/04 - Generating snippets/.mvn/wrapper/maven-wrapper.jar b/chapter06/04 - Generating snippets/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter06/04 - Generating snippets/.mvn/wrapper/maven-wrapper.properties b/chapter06/04 - Generating snippets/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter06/04 - Generating snippets/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter06/04 - Generating snippets/docker-compose.yaml b/chapter06/04 - Generating snippets/docker-compose.yaml new file mode 100644 index 0000000..7f8428d --- /dev/null +++ b/chapter06/04 - Generating snippets/docker-compose.yaml @@ -0,0 +1,14 @@ +version: '3' +services: + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter06/04 - Generating snippets/mvnw b/chapter06/04 - Generating snippets/mvnw index 5bf251c..66df285 100755 --- a/chapter06/04 - Generating snippets/mvnw +++ b/chapter06/04 - Generating snippets/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter06/04 - Generating snippets/mvnw.cmd b/chapter06/04 - Generating snippets/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter06/04 - Generating snippets/mvnw.cmd +++ b/chapter06/04 - Generating snippets/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter06/04 - Generating snippets/pom.xml b/chapter06/04 - Generating snippets/pom.xml index 5d57aab..f48de47 100644 --- a/chapter06/04 - Generating snippets/pom.xml +++ b/chapter06/04 - Generating snippets/pom.xml @@ -1,214 +1,183 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 27.1-jre - - - 2.0.3.RELEASE - - + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - com.google.guava - guava - ${guava.version} - - - - org.projectlombok - lombok - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter06/04 - Generating snippets/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter06/04 - Generating snippets/src/docs/asciidoc/Copsboot REST API Guide.adoc similarity index 91% rename from chapter06/04 - Generating snippets/src/main/asciidoc/Copsboot REST API Guide.adoc rename to chapter06/04 - Generating snippets/src/docs/asciidoc/Copsboot REST API Guide.adoc index 255bc8e..b0b91ae 100644 --- a/chapter06/04 - Generating snippets/src/main/asciidoc/Copsboot REST API Guide.adoc +++ b/chapter06/04 - Generating snippets/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -11,4 +11,4 @@ The Copsboot project uses a REST API for interfacing with the server. This documentation covers version {project-version} of the application. -include::_users.adoc[] \ No newline at end of file +include::_users.adoc[] diff --git a/chapter06/04 - Generating snippets/src/main/asciidoc/_users.adoc b/chapter06/04 - Generating snippets/src/docs/asciidoc/_users.adoc similarity index 56% rename from chapter06/04 - Generating snippets/src/main/asciidoc/_users.adoc rename to chapter06/04 - Generating snippets/src/docs/asciidoc/_users.adoc index a033db8..2becf75 100644 --- a/chapter06/04 - Generating snippets/src/main/asciidoc/_users.adoc +++ b/chapter06/04 - Generating snippets/src/docs/asciidoc/_users.adoc @@ -7,12 +7,12 @@ The API allows to get information on the currently logged on user via a `GET` on `/api/users/me`. If you are not a logged on user, the following response will be returned: -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] +operation::own-details-unauthorized[snippets='http-request,http-response'] //end::initial-doc[] If you do log on as a user, you get more information on that user: -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] +operation::own-details[snippets='http-request,http-response,response-fields'] //tag::create-user[] @@ -20,5 +20,5 @@ operation::authenticated-officer-details-example[snippets='http-request,http-res To create an new user, do a `POST` on `/api/users`: -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/User.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/User.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserNotFoundException.java deleted file mode 100644 index 1f65f04..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> -public class UserNotFoundException extends RuntimeException { - public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); - } -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserService.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..61846a5 100644 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,28 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } - Optional getUser(UserId userId); + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } + // end::createUser[] } diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..0d8f0ab --- /dev/null +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,16 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); //<.> + String email = jwt.getClaimAsString("email"); //<.> + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/UserRestController.java index c74ccd8..796adc1 100644 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,41 +1,52 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] - @PostMapping //<1> - @ResponseStatus(HttpStatus.CREATED) //<2> - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { //<3> - User officer = service.createOfficer(parameters.getEmail(), //<4> - parameters.getPassword()); - return UserDto.fromUser(officer); //<5> + // tag::createUser[] + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/Entity.java b/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/Entity.java index 96cadf0..3a45231 100644 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/Entity.java @@ -1,5 +1,6 @@ package com.example.orm.jpa; + /** * Interface for entity objects. * diff --git a/chapter06/04 - Generating snippets/src/main/resources/application-dev.properties b/chapter06/04 - Generating snippets/src/main/resources/application-dev.properties deleted file mode 100644 index 819196a..0000000 --- a/chapter06/04 - Generating snippets/src/main/resources/application-dev.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN \ No newline at end of file diff --git a/chapter06/04 - Generating snippets/src/main/resources/application.properties b/chapter06/04 - Generating snippets/src/main/resources/application.properties index e69de29..22c3363 100644 --- a/chapter06/04 - Generating snippets/src/main/resources/application.properties +++ b/chapter06/04 - Generating snippets/src/main/resources/application.properties @@ -0,0 +1,3 @@ +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..73e7b68 100644 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,13 @@ package com.example.copsboot; -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index ad7aa55..b37e583 100644 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -2,13 +2,11 @@ import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -17,7 +15,6 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest public class UserRepositoryTest { @@ -27,50 +24,16 @@ public class UserRepositoryTest { //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - assertThat(repository.count()).isEqualTo(1L); //<7> + assertThat(repository.count()).isEqualTo(1L); } //end::testStoreUser[] - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); - - assertThat(optional).isEmpty(); - } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @@ -80,4 +43,4 @@ public UniqueIdGenerator generator() { } } //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/Users.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index f7b7e09..aae4b66 100644 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,162 +1,126 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import com.example.copsboot.user.*; +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.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; //tag::class-setup[] -@RunWith(SpringRunner.class) @WebMvcTest(UserRestController.class) -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureAddonsWebmvcResourceServerSecurity +@Import(WebSecurityConfiguration.class) +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs public class UserRestControllerDocumentation { - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; //end::class-setup[] - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] - //tag::test-config[] + //tag::testconfig[] @TestConfiguration - @Import(OAuth2ServerConfiguration.class) static class TestConfig { @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - } - //end::test-config[] + //end::testconfig[] } diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index 23d72ad..bcc571f 100644 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,125 +1,89 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.verify; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @WebMvcTest(UserRestController.class) -@ActiveProfiles(SpringProfiles.TEST) -public class UserRestControllerTest { -//end::class-annotations[] +@AutoConfigureAddonsWebmvcResourceServerSecurity +@Import(WebSecurityConfiguration.class) +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private MockMvc mvc; + private MockMvc mockMvc; - @Autowired - private ObjectMapper objectMapper; @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); - } - - @TestConfiguration - @Import(OAuth2ServerConfiguration.class) - static class TestConfig { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } } diff --git a/chapter06/04 - Generating snippets/src/test/resources/application-test.properties b/chapter06/04 - Generating snippets/src/test/resources/application-test.properties deleted file mode 100644 index 78c3fdb..0000000 --- a/chapter06/04 - Generating snippets/src/test/resources/application-test.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret \ No newline at end of file diff --git a/chapter06/05 - Refactoring/.mvn/wrapper/maven-wrapper.jar b/chapter06/05 - Refactoring/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter06/05 - Refactoring/.mvn/wrapper/maven-wrapper.properties b/chapter06/05 - Refactoring/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter06/05 - Refactoring/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter06/05 - Refactoring/docker-compose.yaml b/chapter06/05 - Refactoring/docker-compose.yaml new file mode 100644 index 0000000..7f8428d --- /dev/null +++ b/chapter06/05 - Refactoring/docker-compose.yaml @@ -0,0 +1,14 @@ +version: '3' +services: + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter06/05 - Refactoring/mvnw b/chapter06/05 - Refactoring/mvnw index 5bf251c..66df285 100755 --- a/chapter06/05 - Refactoring/mvnw +++ b/chapter06/05 - Refactoring/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter06/05 - Refactoring/mvnw.cmd b/chapter06/05 - Refactoring/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter06/05 - Refactoring/mvnw.cmd +++ b/chapter06/05 - Refactoring/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter06/05 - Refactoring/pom.xml b/chapter06/05 - Refactoring/pom.xml index 5d57aab..f48de47 100644 --- a/chapter06/05 - Refactoring/pom.xml +++ b/chapter06/05 - Refactoring/pom.xml @@ -1,214 +1,183 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 27.1-jre - - - 2.0.3.RELEASE - - + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - com.google.guava - guava - ${guava.version} - - - - org.projectlombok - lombok - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter06/05 - Refactoring/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter06/05 - Refactoring/src/docs/asciidoc/Copsboot REST API Guide.adoc similarity index 91% rename from chapter06/05 - Refactoring/src/main/asciidoc/Copsboot REST API Guide.adoc rename to chapter06/05 - Refactoring/src/docs/asciidoc/Copsboot REST API Guide.adoc index 255bc8e..b0b91ae 100644 --- a/chapter06/05 - Refactoring/src/main/asciidoc/Copsboot REST API Guide.adoc +++ b/chapter06/05 - Refactoring/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -11,4 +11,4 @@ The Copsboot project uses a REST API for interfacing with the server. This documentation covers version {project-version} of the application. -include::_users.adoc[] \ No newline at end of file +include::_users.adoc[] diff --git a/chapter06/05 - Refactoring/src/main/asciidoc/_users.adoc b/chapter06/05 - Refactoring/src/docs/asciidoc/_users.adoc similarity index 56% rename from chapter06/05 - Refactoring/src/main/asciidoc/_users.adoc rename to chapter06/05 - Refactoring/src/docs/asciidoc/_users.adoc index a033db8..2becf75 100644 --- a/chapter06/05 - Refactoring/src/main/asciidoc/_users.adoc +++ b/chapter06/05 - Refactoring/src/docs/asciidoc/_users.adoc @@ -7,12 +7,12 @@ The API allows to get information on the currently logged on user via a `GET` on `/api/users/me`. If you are not a logged on user, the following response will be returned: -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] +operation::own-details-unauthorized[snippets='http-request,http-response'] //end::initial-doc[] If you do log on as a user, you get more information on that user: -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] +operation::own-details[snippets='http-request,http-response,response-fields'] //tag::create-user[] @@ -20,5 +20,5 @@ operation::authenticated-officer-details-example[snippets='http-request,http-res To create an new user, do a `POST` on `/api/users`: -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/User.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/User.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserNotFoundException.java deleted file mode 100644 index 1f65f04..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> -public class UserNotFoundException extends RuntimeException { - public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); - } -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserService.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..61846a5 100644 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,28 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } - Optional getUser(UserId userId); + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } + // end::createUser[] } diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..0d8f0ab --- /dev/null +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,16 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); //<.> + String email = jwt.getClaimAsString("email"); //<.> + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/UserRestController.java index c74ccd8..796adc1 100644 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,41 +1,52 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] - @PostMapping //<1> - @ResponseStatus(HttpStatus.CREATED) //<2> - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { //<3> - User officer = service.createOfficer(parameters.getEmail(), //<4> - parameters.getPassword()); - return UserDto.fromUser(officer); //<5> + // tag::createUser[] + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/Entity.java b/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/Entity.java index a573e0e..3a45231 100644 --- a/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/Entity.java @@ -1,6 +1,5 @@ package com.example.orm.jpa; -import java.io.Serializable; /** * Interface for entity objects. diff --git a/chapter06/05 - Refactoring/src/main/resources/application-dev.properties b/chapter06/05 - Refactoring/src/main/resources/application-dev.properties deleted file mode 100644 index 819196a..0000000 --- a/chapter06/05 - Refactoring/src/main/resources/application-dev.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN \ No newline at end of file diff --git a/chapter06/05 - Refactoring/src/main/resources/application.properties b/chapter06/05 - Refactoring/src/main/resources/application.properties index e69de29..22c3363 100644 --- a/chapter06/05 - Refactoring/src/main/resources/application.properties +++ b/chapter06/05 - Refactoring/src/main/resources/application.properties @@ -0,0 +1,3 @@ +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..73e7b68 100644 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,13 @@ package com.example.copsboot; -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index ad7aa55..b37e583 100644 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -2,13 +2,11 @@ import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -17,7 +15,6 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest public class UserRepositoryTest { @@ -27,50 +24,16 @@ public class UserRepositoryTest { //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - assertThat(repository.count()).isEqualTo(1L); //<7> + assertThat(repository.count()).isEqualTo(1L); } //end::testStoreUser[] - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); - - assertThat(optional).isEmpty(); - } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @@ -80,4 +43,4 @@ public UniqueIdGenerator generator() { } } //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/Users.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index e0d24b0..c142293 100644 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,133 +1,124 @@ package com.example.copsboot.user.web; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +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.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; - + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] + + //tag::testconfig[] + @TestConfiguration + static class TestConfig { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } + //end::testconfig[] } diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index 9014594..2acf875 100644 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,97 +1,84 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.verify; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } } diff --git a/chapter06/05 - Refactoring/src/test/resources/application-test.properties b/chapter06/05 - Refactoring/src/test/resources/application-test.properties deleted file mode 100644 index 78c3fdb..0000000 --- a/chapter06/05 - Refactoring/src/test/resources/application-test.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret \ No newline at end of file diff --git a/chapter07/01 - postgresql/.mvn/wrapper/maven-wrapper.jar b/chapter07/01 - postgresql/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter07/01 - postgresql/.mvn/wrapper/maven-wrapper.properties b/chapter07/01 - postgresql/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter07/01 - postgresql/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter07/01 - postgresql/docker-compose.yaml b/chapter07/01 - postgresql/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter07/01 - postgresql/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter07/01 - postgresql/mvnw b/chapter07/01 - postgresql/mvnw index 5bf251c..66df285 100755 --- a/chapter07/01 - postgresql/mvnw +++ b/chapter07/01 - postgresql/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter07/01 - postgresql/mvnw.cmd b/chapter07/01 - postgresql/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter07/01 - postgresql/mvnw.cmd +++ b/chapter07/01 - postgresql/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter07/01 - postgresql/pom.xml b/chapter07/01 - postgresql/pom.xml index 4d22b68..7eb0063 100644 --- a/chapter07/01 - postgresql/pom.xml +++ b/chapter07/01 - postgresql/pom.xml @@ -1,220 +1,190 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 27.1-jre + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 2.0.3.RELEASE - - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - - org.postgresql - postgresql - - - - - org.flywaydb - flyway-core - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter07/01 - postgresql/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter07/01 - postgresql/src/docs/asciidoc/Copsboot REST API Guide.adoc similarity index 91% rename from chapter07/01 - postgresql/src/main/asciidoc/Copsboot REST API Guide.adoc rename to chapter07/01 - postgresql/src/docs/asciidoc/Copsboot REST API Guide.adoc index 255bc8e..b0b91ae 100644 --- a/chapter07/01 - postgresql/src/main/asciidoc/Copsboot REST API Guide.adoc +++ b/chapter07/01 - postgresql/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -11,4 +11,4 @@ The Copsboot project uses a REST API for interfacing with the server. This documentation covers version {project-version} of the application. -include::_users.adoc[] \ No newline at end of file +include::_users.adoc[] diff --git a/chapter07/01 - postgresql/src/main/asciidoc/_users.adoc b/chapter07/01 - postgresql/src/docs/asciidoc/_users.adoc similarity index 56% rename from chapter07/01 - postgresql/src/main/asciidoc/_users.adoc rename to chapter07/01 - postgresql/src/docs/asciidoc/_users.adoc index a033db8..2becf75 100644 --- a/chapter07/01 - postgresql/src/main/asciidoc/_users.adoc +++ b/chapter07/01 - postgresql/src/docs/asciidoc/_users.adoc @@ -7,12 +7,12 @@ The API allows to get information on the currently logged on user via a `GET` on `/api/users/me`. If you are not a logged on user, the following response will be returned: -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] +operation::own-details-unauthorized[snippets='http-request,http-response'] //end::initial-doc[] If you do log on as a user, you get more information on that user: -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] +operation::own-details[snippets='http-request,http-response,response-fields'] //tag::create-user[] @@ -20,5 +20,5 @@ operation::authenticated-officer-details-example[snippets='http-request,http-res To create an new user, do a `POST` on `/api/users`: -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/User.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/User.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserNotFoundException.java deleted file mode 100644 index 1f65f04..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> -public class UserNotFoundException extends RuntimeException { - public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); - } -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserService.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..61846a5 100644 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,28 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } - Optional getUser(UserId userId); + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } + // end::createUser[] } diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..0d8f0ab --- /dev/null +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,16 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); //<.> + String email = jwt.getClaimAsString("email"); //<.> + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/UserRestController.java index c74ccd8..796adc1 100644 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,41 +1,52 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] - @PostMapping //<1> - @ResponseStatus(HttpStatus.CREATED) //<2> - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { //<3> - User officer = service.createOfficer(parameters.getEmail(), //<4> - parameters.getPassword()); - return UserDto.fromUser(officer); //<5> + // tag::createUser[] + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/Entity.java b/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/Entity.java index a573e0e..3a45231 100644 --- a/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/Entity.java @@ -1,6 +1,5 @@ package com.example.orm.jpa; -import java.io.Serializable; /** * Interface for entity objects. diff --git a/chapter07/01 - postgresql/src/main/resources/application-dev.properties b/chapter07/01 - postgresql/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter07/01 - postgresql/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter07/01 - postgresql/src/main/resources/application-local.properties b/chapter07/01 - postgresql/src/main/resources/application-local.properties index c14a8c4..8fbe161 100644 --- a/chapter07/01 - postgresql/src/main/resources/application-local.properties +++ b/chapter07/01 - postgresql/src/main/resources/application-local.properties @@ -3,14 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter07/01 - postgresql/src/main/resources/application.properties b/chapter07/01 - postgresql/src/main/resources/application.properties index e69de29..22c3363 100644 --- a/chapter07/01 - postgresql/src/main/resources/application.properties +++ b/chapter07/01 - postgresql/src/main/resources/application.properties @@ -0,0 +1,3 @@ +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter07/01 - postgresql/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter07/01 - postgresql/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter07/01 - postgresql/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter07/01 - postgresql/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter07/01 - postgresql/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter07/01 - postgresql/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter07/01 - postgresql/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter07/01 - postgresql/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter07/01 - postgresql/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter07/01 - postgresql/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter07/01 - postgresql/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter07/01 - postgresql/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..73e7b68 100644 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,13 @@ package com.example.copsboot; -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..b37e583 100644 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -1,16 +1,12 @@ package com.example.copsboot.user; -import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,9 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) public class UserRepositoryTest { @Autowired @@ -30,50 +24,16 @@ public class UserRepositoryTest { //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - assertThat(repository.count()).isEqualTo(1L); //<7> + assertThat(repository.count()).isEqualTo(1L); } //end::testStoreUser[] - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); - - assertThat(optional).isEmpty(); - } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @@ -83,4 +43,4 @@ public UniqueIdGenerator generator() { } } //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/Users.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index e0d24b0..805c501 100644 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,133 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index 9014594..2acf875 100644 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,97 +1,84 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.verify; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } } diff --git a/chapter07/01 - postgresql/src/test/resources/application-test.properties b/chapter07/01 - postgresql/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter07/01 - postgresql/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter07/02 - testcontainers/.mvn/wrapper/maven-wrapper.jar b/chapter07/02 - testcontainers/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter07/02 - testcontainers/.mvn/wrapper/maven-wrapper.properties b/chapter07/02 - testcontainers/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter07/02 - testcontainers/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter07/02 - testcontainers/docker-compose.yaml b/chapter07/02 - testcontainers/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter07/02 - testcontainers/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter07/02 - testcontainers/mvnw b/chapter07/02 - testcontainers/mvnw index 5bf251c..66df285 100755 --- a/chapter07/02 - testcontainers/mvnw +++ b/chapter07/02 - testcontainers/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter07/02 - testcontainers/mvnw.cmd b/chapter07/02 - testcontainers/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter07/02 - testcontainers/mvnw.cmd +++ b/chapter07/02 - testcontainers/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter07/02 - testcontainers/pom.xml b/chapter07/02 - testcontainers/pom.xml index 468a4d0..43db322 100644 --- a/chapter07/02 - testcontainers/pom.xml +++ b/chapter07/02 - testcontainers/pom.xml @@ -1,231 +1,208 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 27.1-jre + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 2.0.3.RELEASE - 1.11.2 - - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - org.postgresql - postgresql - - - org.flywaydb - flyway-core - + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter07/02 - testcontainers/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter07/02 - testcontainers/src/docs/asciidoc/Copsboot REST API Guide.adoc similarity index 91% rename from chapter07/02 - testcontainers/src/main/asciidoc/Copsboot REST API Guide.adoc rename to chapter07/02 - testcontainers/src/docs/asciidoc/Copsboot REST API Guide.adoc index 255bc8e..b0b91ae 100644 --- a/chapter07/02 - testcontainers/src/main/asciidoc/Copsboot REST API Guide.adoc +++ b/chapter07/02 - testcontainers/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -11,4 +11,4 @@ The Copsboot project uses a REST API for interfacing with the server. This documentation covers version {project-version} of the application. -include::_users.adoc[] \ No newline at end of file +include::_users.adoc[] diff --git a/chapter07/02 - testcontainers/src/main/asciidoc/_users.adoc b/chapter07/02 - testcontainers/src/docs/asciidoc/_users.adoc similarity index 56% rename from chapter07/02 - testcontainers/src/main/asciidoc/_users.adoc rename to chapter07/02 - testcontainers/src/docs/asciidoc/_users.adoc index a033db8..2becf75 100644 --- a/chapter07/02 - testcontainers/src/main/asciidoc/_users.adoc +++ b/chapter07/02 - testcontainers/src/docs/asciidoc/_users.adoc @@ -7,12 +7,12 @@ The API allows to get information on the currently logged on user via a `GET` on `/api/users/me`. If you are not a logged on user, the following response will be returned: -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] +operation::own-details-unauthorized[snippets='http-request,http-response'] //end::initial-doc[] If you do log on as a user, you get more information on that user: -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] +operation::own-details[snippets='http-request,http-response,response-fields'] //tag::create-user[] @@ -20,5 +20,5 @@ operation::authenticated-officer-details-example[snippets='http-request,http-res To create an new user, do a `POST` on `/api/users`: -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java index 344a5fe..fb1cc59 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java @@ -6,6 +6,7 @@ public final class SpringProfiles { public static final String TEST = "test"; public static final String STAGING = "staging"; public static final String PROD = "prod"; + public static final String REPOSITORY_TEST = "repository-test"; public static final String INTEGRATION_TEST = "integration-test"; private SpringProfiles() { diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/User.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/User.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserNotFoundException.java deleted file mode 100644 index 1f65f04..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> -public class UserNotFoundException extends RuntimeException { - public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); - } -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserService.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..61846a5 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,28 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } - Optional getUser(UserId userId); + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } + // end::createUser[] } diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..0d8f0ab --- /dev/null +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,16 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); //<.> + String email = jwt.getClaimAsString("email"); //<.> + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/UserRestController.java index c74ccd8..796adc1 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,41 +1,52 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] - @PostMapping //<1> - @ResponseStatus(HttpStatus.CREATED) //<2> - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { //<3> - User officer = service.createOfficer(parameters.getEmail(), //<4> - parameters.getPassword()); - return UserDto.fromUser(officer); //<5> + // tag::createUser[] + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/Entity.java b/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/Entity.java index a573e0e..3a45231 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/Entity.java @@ -1,6 +1,5 @@ package com.example.orm.jpa; -import java.io.Serializable; /** * Interface for entity objects. diff --git a/chapter07/02 - testcontainers/src/main/resources/application-dev.properties b/chapter07/02 - testcontainers/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter07/02 - testcontainers/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter07/02 - testcontainers/src/main/resources/application-local.properties b/chapter07/02 - testcontainers/src/main/resources/application-local.properties index 8f13f3f..7e354d5 100644 --- a/chapter07/02 - testcontainers/src/main/resources/application-local.properties +++ b/chapter07/02 - testcontainers/src/main/resources/application-local.properties @@ -3,13 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +#spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter07/02 - testcontainers/src/main/resources/application.properties b/chapter07/02 - testcontainers/src/main/resources/application.properties index e69de29..22c3363 100644 --- a/chapter07/02 - testcontainers/src/main/resources/application.properties +++ b/chapter07/02 - testcontainers/src/main/resources/application.properties @@ -0,0 +1,3 @@ +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter07/02 - testcontainers/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter07/02 - testcontainers/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter07/02 - testcontainers/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter07/02 - testcontainers/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter07/02 - testcontainers/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter07/02 - testcontainers/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter07/02 - testcontainers/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter07/02 - testcontainers/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter07/02 - testcontainers/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter07/02 - testcontainers/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter07/02 - testcontainers/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter07/02 - testcontainers/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..5feb390 100644 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,16 @@ package com.example.copsboot; import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java deleted file mode 100644 index 720f959..0000000 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.copsboot.user; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.HashSet; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> -@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) //<2> -public class UserRepositoryIntegrationTest { - @Autowired - private UserRepository repository; - @PersistenceContext - private EntityManager entityManager; - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void testSaveUser() { - Set roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); - - assertThat(repository.count()).isEqualTo(1L); - - entityManager.flush(); //<3> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM user_roles", Long.class)).isEqualTo(1L); - assertThat(jdbcTemplate.queryForObject("SELECT roles FROM user_roles", String.class)).isEqualTo("OFFICER"); - } -} diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..19c23fe 100644 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -3,14 +3,16 @@ import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,62 +21,34 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) //<2> public class UserRepositoryTest { @Autowired private UserRepository repository; + @PersistenceContext + private EntityManager entityManager; + @Autowired + private JdbcTemplate jdbcTemplate; - //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } + assertThat(repository.count()).isEqualTo(1L); - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); + entityManager.flush(); //<3> - assertThat(optional).isEmpty(); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> + assertThat(jdbcTemplate.queryForObject("SELECT email FROM copsboot_user", String.class)).isEqualTo("alex.foley@beverly-hills.com"); } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @Bean @@ -82,5 +56,4 @@ public UniqueIdGenerator generator() { return new InMemoryUniqueIdGenerator(); } } - //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/Users.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index e0d24b0..805c501 100644 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,133 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index 9014594..2acf875 100644 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,97 +1,84 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.verify; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } } diff --git a/chapter07/02 - testcontainers/src/test/resources/application-integration-test.properties b/chapter07/02 - testcontainers/src/test/resources/application-integration-test.properties index 159536c..c61e563 100644 --- a/chapter07/02 - testcontainers/src/test/resources/application-integration-test.properties +++ b/chapter07/02 - testcontainers/src/test/resources/application-integration-test.properties @@ -1,11 +1,6 @@ -spring.datasource.url=jdbc:tc:postgresql://localhost/copsbootdb +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.username=user spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none - -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter07/02 - testcontainers/src/test/resources/application-repository-test.properties b/chapter07/02 - testcontainers/src/test/resources/application-repository-test.properties new file mode 100644 index 0000000..c61e563 --- /dev/null +++ b/chapter07/02 - testcontainers/src/test/resources/application-repository-test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=user +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter07/02 - testcontainers/src/test/resources/application-test.properties b/chapter07/02 - testcontainers/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter07/02 - testcontainers/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter07/02 - testcontainers/src/test/resources/logback-test.xml b/chapter07/02 - testcontainers/src/test/resources/logback-test.xml index bf47fec..164429c 100644 --- a/chapter07/02 - testcontainers/src/test/resources/logback-test.xml +++ b/chapter07/02 - testcontainers/src/test/resources/logback-test.xml @@ -5,7 +5,7 @@ - + @@ -17,14 +17,8 @@ - - - - - - - \ No newline at end of file + diff --git a/chapter08/01 - builtin/.mvn/wrapper/maven-wrapper.jar b/chapter08/01 - builtin/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter08/01 - builtin/.mvn/wrapper/maven-wrapper.properties b/chapter08/01 - builtin/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter08/01 - builtin/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter08/01 - builtin/docker-compose.yaml b/chapter08/01 - builtin/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter08/01 - builtin/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter08/01 - builtin/mvnw b/chapter08/01 - builtin/mvnw index 5bf251c..66df285 100755 --- a/chapter08/01 - builtin/mvnw +++ b/chapter08/01 - builtin/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter08/01 - builtin/mvnw.cmd b/chapter08/01 - builtin/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter08/01 - builtin/mvnw.cmd +++ b/chapter08/01 - builtin/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter08/01 - builtin/pom.xml b/chapter08/01 - builtin/pom.xml index 468a4d0..43db322 100644 --- a/chapter08/01 - builtin/pom.xml +++ b/chapter08/01 - builtin/pom.xml @@ -1,231 +1,208 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 27.1-jre + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 2.0.3.RELEASE - 1.11.2 - - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - org.postgresql - postgresql - - - org.flywaydb - flyway-core - + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter08/01 - builtin/src/docs/asciidoc/Copsboot REST API Guide.adoc b/chapter08/01 - builtin/src/docs/asciidoc/Copsboot REST API Guide.adoc new file mode 100644 index 0000000..b0b91ae --- /dev/null +++ b/chapter08/01 - builtin/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -0,0 +1,14 @@ += Copsboot REST API Guide +:icons: font +:toc: +:toclevels: 2 + +:numbered: + +== Introduction + +The Copsboot project uses a REST API for interfacing with the server. + +This documentation covers version {project-version} of the application. + +include::_users.adoc[] diff --git a/chapter08/01 - builtin/src/docs/asciidoc/_users.adoc b/chapter08/01 - builtin/src/docs/asciidoc/_users.adoc new file mode 100644 index 0000000..2becf75 --- /dev/null +++ b/chapter08/01 - builtin/src/docs/asciidoc/_users.adoc @@ -0,0 +1,24 @@ +//tag::initial-doc[] +== User Management + +=== User information + +The API allows to get information on the currently logged on user +via a `GET` on `/api/users/me`. If you are not a logged on user, the +following response will be returned: + +operation::own-details-unauthorized[snippets='http-request,http-response'] +//end::initial-doc[] + +If you do log on as a user, you get more information on that user: + +operation::own-details[snippets='http-request,http-response,response-fields'] + + +//tag::create-user[] +=== Create a user + +To create an new user, do a `POST` on `/api/users`: + +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter08/01 - builtin/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter08/01 - builtin/src/main/asciidoc/Copsboot REST API Guide.adoc deleted file mode 100644 index 255bc8e..0000000 --- a/chapter08/01 - builtin/src/main/asciidoc/Copsboot REST API Guide.adoc +++ /dev/null @@ -1,14 +0,0 @@ -= Copsboot REST API Guide -:icons: font -:toc: -:toclevels: 2 - -:numbered: - -== Introduction - -The Copsboot project uses a REST API for interfacing with the server. - -This documentation covers version {project-version} of the application. - -include::_users.adoc[] \ No newline at end of file diff --git a/chapter08/01 - builtin/src/main/asciidoc/_users.adoc b/chapter08/01 - builtin/src/main/asciidoc/_users.adoc deleted file mode 100644 index a033db8..0000000 --- a/chapter08/01 - builtin/src/main/asciidoc/_users.adoc +++ /dev/null @@ -1,24 +0,0 @@ -//tag::initial-doc[] -== User Management - -=== User information - -The API allows to get information on the currently logged on user -via a `GET` on `/api/users/me`. If you are not a logged on user, the -following response will be returned: - -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] -//end::initial-doc[] - -If you do log on as a user, you get more information on that user: - -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] - - -//tag::create-user[] -=== Create a user - -To create an new user, do a `POST` on `/api/users`: - -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java index 344a5fe..fb1cc59 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java @@ -6,6 +6,7 @@ public final class SpringProfiles { public static final String TEST = "test"; public static final String STAGING = "staging"; public static final String PROD = "prod"; + public static final String REPOSITORY_TEST = "repository-test"; public static final String INTEGRATION_TEST = "integration-test"; private SpringProfiles() { diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java index 9c92c49..8d26775 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java @@ -1,11 +1,4 @@ package com.example.copsboot.infrastructure.mvc; -import lombok.Value; - -//tag::class[] -@Value -public class FieldErrorResponse { - private String fieldName; - private String errorMessage; +public record FieldErrorResponse(String fieldName, String errorMesesage) { } -//end::class[] \ No newline at end of file diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/User.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/User.java +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserNotFoundException.java deleted file mode 100644 index 1f65f04..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> -public class UserNotFoundException extends RuntimeException { - public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); - } -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserService.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..61846a5 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,28 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } - Optional getUser(UserId userId); + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } + // end::createUser[] } diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..b87302d --- /dev/null +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,17 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(@NotEmpty String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + String email = jwt.getClaimAsString("email"); + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/UserRestController.java index b5aa1a8..e0a6545 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,49 +1,53 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import lombok.Value; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Valid; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] + // tag::createUser[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { - User officer = service.createOfficer(parameters.getEmail(), - parameters.getPassword()); - return UserDto.fromUser(officer); + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/Entity.java b/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/Entity.java index a573e0e..3a45231 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/Entity.java @@ -1,6 +1,5 @@ package com.example.orm.jpa; -import java.io.Serializable; /** * Interface for entity objects. diff --git a/chapter08/01 - builtin/src/main/resources/application-dev.properties b/chapter08/01 - builtin/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter08/01 - builtin/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/01 - builtin/src/main/resources/application-local.properties b/chapter08/01 - builtin/src/main/resources/application-local.properties index 8f13f3f..7e354d5 100644 --- a/chapter08/01 - builtin/src/main/resources/application-local.properties +++ b/chapter08/01 - builtin/src/main/resources/application-local.properties @@ -3,13 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +#spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter08/01 - builtin/src/main/resources/application.properties b/chapter08/01 - builtin/src/main/resources/application.properties index e69de29..3e80adf 100644 --- a/chapter08/01 - builtin/src/main/resources/application.properties +++ b/chapter08/01 - builtin/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/copsboot + +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter08/01 - builtin/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter08/01 - builtin/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter08/01 - builtin/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter08/01 - builtin/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter08/01 - builtin/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter08/01 - builtin/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter08/01 - builtin/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter08/01 - builtin/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter08/01 - builtin/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter08/01 - builtin/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter08/01 - builtin/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter08/01 - builtin/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..5feb390 100644 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter08/01 - builtin/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,16 @@ package com.example.copsboot; import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java deleted file mode 100644 index 720f959..0000000 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.copsboot.user; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.HashSet; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> -@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) //<2> -public class UserRepositoryIntegrationTest { - @Autowired - private UserRepository repository; - @PersistenceContext - private EntityManager entityManager; - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void testSaveUser() { - Set roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); - - assertThat(repository.count()).isEqualTo(1L); - - entityManager.flush(); //<3> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM user_roles", Long.class)).isEqualTo(1L); - assertThat(jdbcTemplate.queryForObject("SELECT roles FROM user_roles", String.class)).isEqualTo("OFFICER"); - } -} diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..19c23fe 100644 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -3,14 +3,16 @@ import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,62 +21,34 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) //<2> public class UserRepositoryTest { @Autowired private UserRepository repository; + @PersistenceContext + private EntityManager entityManager; + @Autowired + private JdbcTemplate jdbcTemplate; - //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } + assertThat(repository.count()).isEqualTo(1L); - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); + entityManager.flush(); //<3> - assertThat(optional).isEmpty(); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> + assertThat(jdbcTemplate.queryForObject("SELECT email FROM copsboot_user", String.class)).isEqualTo("alex.foley@beverly-hills.com"); } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @Bean @@ -82,5 +56,4 @@ public UniqueIdGenerator generator() { return new InMemoryUniqueIdGenerator(); } } - //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/Users.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index e0d24b0..805c501 100644 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,133 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index 519fa70..a20d744 100644 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,120 +1,104 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } - //tag::pwdshort[] + // tag::emptyToken[] @Test - public void testCreateOfficerIfPasswordIsTooShort() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "pwd"; // <1> - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isBadRequest()) //<2> - .andDo(print()); //<3> - - verify(service, never()).createOfficer(email, password); //<4> + void givenEmptyMobileToken_badRequestIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "" + } + """)) //<.> + .andExpect(status().isBadRequest()) //<.> + .andDo(print()); //<.> + + verify(userService, never()).createUser(any(CreateUserParameters.class)); //<.> } - //end::pwdshort[] + // end::emptyToken[] } diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerWithResponseBodyValidationTest.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerWithResponseBodyValidationTest.java deleted file mode 100644 index 4ee1390..0000000 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerWithResponseBodyValidationTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; - -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) -public class UserRestControllerWithResponseBodyValidationTest { - //end::class-annotations[] - @Autowired - private MockMvc mvc; - - @Autowired - private ObjectMapper objectMapper; - @MockBean - private UserService service; - - //tag::pwdshort[] - @Test - public void testCreateOfficerIfPasswordIsTooShort() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("errors[0].fieldName").value("password")); //<1> - - verify(service, never()).createOfficer(email, password); - } - //end::pwdshort[] -} diff --git a/chapter08/01 - builtin/src/test/resources/application-integration-test.properties b/chapter08/01 - builtin/src/test/resources/application-integration-test.properties index 159536c..c61e563 100644 --- a/chapter08/01 - builtin/src/test/resources/application-integration-test.properties +++ b/chapter08/01 - builtin/src/test/resources/application-integration-test.properties @@ -1,11 +1,6 @@ -spring.datasource.url=jdbc:tc:postgresql://localhost/copsbootdb +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.username=user spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none - -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/01 - builtin/src/test/resources/application-repository-test.properties b/chapter08/01 - builtin/src/test/resources/application-repository-test.properties new file mode 100644 index 0000000..c61e563 --- /dev/null +++ b/chapter08/01 - builtin/src/test/resources/application-repository-test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=user +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/01 - builtin/src/test/resources/application-test.properties b/chapter08/01 - builtin/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter08/01 - builtin/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/01 - builtin/src/test/resources/logback-test.xml b/chapter08/01 - builtin/src/test/resources/logback-test.xml index bf47fec..164429c 100644 --- a/chapter08/01 - builtin/src/test/resources/logback-test.xml +++ b/chapter08/01 - builtin/src/test/resources/logback-test.xml @@ -5,7 +5,7 @@ - + @@ -17,14 +17,8 @@ - - - - - - - \ No newline at end of file + diff --git a/chapter08/02 - customfield/.mvn/wrapper/maven-wrapper.jar b/chapter08/02 - customfield/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter08/02 - customfield/.mvn/wrapper/maven-wrapper.properties b/chapter08/02 - customfield/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter08/02 - customfield/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter08/02 - customfield/docker-compose.yaml b/chapter08/02 - customfield/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter08/02 - customfield/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter08/02 - customfield/mvnw b/chapter08/02 - customfield/mvnw index 5bf251c..66df285 100755 --- a/chapter08/02 - customfield/mvnw +++ b/chapter08/02 - customfield/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter08/02 - customfield/mvnw.cmd b/chapter08/02 - customfield/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter08/02 - customfield/mvnw.cmd +++ b/chapter08/02 - customfield/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter08/02 - customfield/pom.xml b/chapter08/02 - customfield/pom.xml index 468a4d0..43db322 100644 --- a/chapter08/02 - customfield/pom.xml +++ b/chapter08/02 - customfield/pom.xml @@ -1,231 +1,208 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 27.1-jre + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 2.0.3.RELEASE - 1.11.2 - - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - org.postgresql - postgresql - - - org.flywaydb - flyway-core - + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter08/02 - customfield/src/docs/asciidoc/Copsboot REST API Guide.adoc b/chapter08/02 - customfield/src/docs/asciidoc/Copsboot REST API Guide.adoc new file mode 100644 index 0000000..b0b91ae --- /dev/null +++ b/chapter08/02 - customfield/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -0,0 +1,14 @@ += Copsboot REST API Guide +:icons: font +:toc: +:toclevels: 2 + +:numbered: + +== Introduction + +The Copsboot project uses a REST API for interfacing with the server. + +This documentation covers version {project-version} of the application. + +include::_users.adoc[] diff --git a/chapter08/02 - customfield/src/docs/asciidoc/_users.adoc b/chapter08/02 - customfield/src/docs/asciidoc/_users.adoc new file mode 100644 index 0000000..2becf75 --- /dev/null +++ b/chapter08/02 - customfield/src/docs/asciidoc/_users.adoc @@ -0,0 +1,24 @@ +//tag::initial-doc[] +== User Management + +=== User information + +The API allows to get information on the currently logged on user +via a `GET` on `/api/users/me`. If you are not a logged on user, the +following response will be returned: + +operation::own-details-unauthorized[snippets='http-request,http-response'] +//end::initial-doc[] + +If you do log on as a user, you get more information on that user: + +operation::own-details[snippets='http-request,http-response,response-fields'] + + +//tag::create-user[] +=== Create a user + +To create an new user, do a `POST` on `/api/users`: + +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter08/02 - customfield/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter08/02 - customfield/src/main/asciidoc/Copsboot REST API Guide.adoc deleted file mode 100644 index 255bc8e..0000000 --- a/chapter08/02 - customfield/src/main/asciidoc/Copsboot REST API Guide.adoc +++ /dev/null @@ -1,14 +0,0 @@ -= Copsboot REST API Guide -:icons: font -:toc: -:toclevels: 2 - -:numbered: - -== Introduction - -The Copsboot project uses a REST API for interfacing with the server. - -This documentation covers version {project-version} of the application. - -include::_users.adoc[] \ No newline at end of file diff --git a/chapter08/02 - customfield/src/main/asciidoc/_users.adoc b/chapter08/02 - customfield/src/main/asciidoc/_users.adoc deleted file mode 100644 index a033db8..0000000 --- a/chapter08/02 - customfield/src/main/asciidoc/_users.adoc +++ /dev/null @@ -1,24 +0,0 @@ -//tag::initial-doc[] -== User Management - -=== User information - -The API allows to get information on the currently logged on user -via a `GET` on `/api/users/me`. If you are not a logged on user, the -following response will be returned: - -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] -//end::initial-doc[] - -If you do log on as a user, you get more information on that user: - -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] - - -//tag::create-user[] -=== Create a user - -To create an new user, do a `POST` on `/api/users`: - -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java index 344a5fe..fb1cc59 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java @@ -6,6 +6,7 @@ public final class SpringProfiles { public static final String TEST = "test"; public static final String STAGING = "staging"; public static final String PROD = "prod"; + public static final String REPOSITORY_TEST = "repository-test"; public static final String INTEGRATION_TEST = "integration-test"; private SpringProfiles() { diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java index 9c92c49..8d26775 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java @@ -1,11 +1,4 @@ package com.example.copsboot.infrastructure.mvc; -import lombok.Value; - -//tag::class[] -@Value -public class FieldErrorResponse { - private String fieldName; - private String errorMessage; +public record FieldErrorResponse(String fieldName, String errorMesesage) { } -//end::class[] \ No newline at end of file diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/CreateReportParameters.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/CreateReportParameters.java new file mode 100644 index 0000000..64aeea6 --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/CreateReportParameters.java @@ -0,0 +1,8 @@ +package com.example.copsboot.report; + +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +public record CreateReportParameters(UserId userId, Instant dateTime, String description) { +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/Report.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/Report.java index af25bfd..b10756f 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/Report.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/Report.java @@ -1,37 +1,36 @@ package com.example.copsboot.report; -import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; import com.example.orm.jpa.AbstractEntity; import com.example.util.ArtifactForFramework; +import jakarta.persistence.Entity; + +import java.time.Instant; -import javax.persistence.Entity; -import javax.persistence.ManyToOne; -import java.time.ZonedDateTime; //tag::class[] @Entity public class Report extends AbstractEntity { - @ManyToOne - private User reporter; - private ZonedDateTime dateTime; + + private UserId reporterId; + private Instant dateTime; private String description; @ArtifactForFramework protected Report() { } - - public Report(ReportId id, User reporter, ZonedDateTime dateTime, String description) { + public Report(ReportId id, UserId reporterId, Instant dateTime, String description) { super(id); - this.reporter = reporter; + this.reporterId = reporterId; this.dateTime = dateTime; this.description = description; } - public User getReporter() { - return reporter; + public UserId getReporterId() { + return reporterId; } - public ZonedDateTime getDateTime() { + public Instant getDateTime() { return dateTime; } diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/ReportService.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/ReportService.java index f6ed620..613248b 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/ReportService.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/ReportService.java @@ -1,9 +1,23 @@ package com.example.copsboot.report; -import com.example.copsboot.user.UserId; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.ZonedDateTime; -public interface ReportService { - Report createReport(UserId reporterId, ZonedDateTime dateTime, String description); +@Service +@Transactional +public class ReportService { + private final ReportRepository repository; + + public ReportService(ReportRepository repository) { + this.repository = repository; + } + + public Report createReport(CreateReportParameters parameters) { + return repository.save(new Report(repository.nextId(), + parameters.userId(), + parameters.dateTime(), + parameters.description())); + } } diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/ReportServiceImpl.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/ReportServiceImpl.java deleted file mode 100644 index 67c21e1..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/ReportServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserNotFoundException; -import com.example.copsboot.user.UserService; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.ZonedDateTime; - -@Service -@Transactional -public class ReportServiceImpl implements ReportService { - private final ReportRepository repository; - private final UserService userService; - - public ReportServiceImpl(ReportRepository repository, UserService userService) { - this.repository = repository; - this.userService = userService; - } - - @Override - public Report createReport(UserId reporterId, ZonedDateTime dateTime, String description) { - return repository.save(new Report(repository.nextId(), - userService.getUser(reporterId) - .orElseThrow(() -> new UserNotFoundException(reporterId)), - dateTime, - description)); - } -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java deleted file mode 100644 index e506d1b..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.copsboot.report.web; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.ZonedDateTime; - -//tag::class[] -@Data -@AllArgsConstructor -@NoArgsConstructor -public class CreateReportParameters { - private ZonedDateTime dateTime; - private String description; -} -//end::class[] \ No newline at end of file diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java new file mode 100644 index 0000000..bd8d8ef --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java @@ -0,0 +1,12 @@ +package com.example.copsboot.report.web; + +import com.example.copsboot.report.CreateReportParameters; +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +public record CreateReportRequest(Instant dateTime, String description) { + public CreateReportParameters toParameters(UserId userId) { + return new CreateReportParameters(userId, dateTime, description); + } +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/ReportDto.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/ReportDto.java index 0adc7f8..28e606e 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/ReportDto.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/ReportDto.java @@ -2,23 +2,21 @@ import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; -import lombok.Value; +import com.example.copsboot.user.UserService; -import java.time.ZonedDateTime; +import java.time.Instant; //tag::class[] -@Value -public class ReportDto { - private ReportId id; - private String reporter; - private ZonedDateTime dateTime; - private String description; +public record ReportDto(ReportId id, + String reporter, + Instant dateTime, + String description) { - public static ReportDto fromReport(Report report) { + public static ReportDto fromReport(Report report, UserService userService) { return new ReportDto(report.getId(), - report.getReporter().getEmail(), - report.getDateTime(), - report.getDescription()); + userService.getUserById(report.getReporterId()).getEmail(), + report.getDateTime(), + report.getDescription()); } } //end::class[] diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/ReportRestController.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/ReportRestController.java index 28b79ae..83f9d54 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/ReportRestController.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/ReportRestController.java @@ -1,31 +1,42 @@ package com.example.copsboot.report.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportService; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserNotFoundException; +import com.example.copsboot.user.UserService; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.UUID; //tag::class[] @RestController @RequestMapping("/api/reports") public class ReportRestController { private final ReportService service; + private final UserService userService; - public ReportRestController(ReportService service) { + public ReportRestController(ReportService service, UserService userService) { this.service = service; + this.userService = userService; } @PostMapping @ResponseStatus(HttpStatus.CREATED) - public ReportDto createReport(@AuthenticationPrincipal ApplicationUserDetails userDetails, - @Valid @RequestBody CreateReportParameters parameters) { - return ReportDto.fromReport(service.createReport(userDetails.getUserId(), - parameters.getDateTime(), - parameters.getDescription())); + public ReportDto createReport(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateReportRequest request) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + User user = userService.findUserByAuthServerId(authServerId) + .orElseThrow(() -> new UserNotFoundException(authServerId)); + CreateReportParameters parameters = request.toParameters(user.getId()); + Report report = service.createReport(parameters); + return ReportDto.fromReport(report, userService); } } //end::class[] diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/User.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/User.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java new file mode 100644 index 0000000..2a434e3 --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class UserIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(UserId attribute) { + return attribute.getId(); + } + + @Override + public UserId convertToEntityAttribute(UUID dbData) { + return new UserId(dbData); + } +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserNotFoundException.java index 1f65f04..97d0813 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserNotFoundException.java @@ -3,9 +3,13 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> +@ResponseStatus(HttpStatus.NOT_FOUND) public class UserNotFoundException extends RuntimeException { public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); + super(String.format("Unable to find user with id %s", userId)); + } + + public UserNotFoundException(AuthServerId authServerId) { + super(String.format("Unable to find user with auth server id %s", authServerId)); } } diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserService.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..ec5aa13 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,33 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } + + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } - Optional getUser(UserId userId); + public User getUserById(UserId userId) { + return repository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + } + // end::createUser[] } diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..b87302d --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,17 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(@NotEmpty String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + String email = jwt.getClaimAsString("email"); + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/UserRestController.java index b5aa1a8..e0a6545 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,49 +1,53 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import lombok.Value; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Valid; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] + // tag::createUser[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { - User officer = service.createOfficer(parameters.getEmail(), - parameters.getPassword()); - return UserDto.fromUser(officer); + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/Entity.java b/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/Entity.java index a573e0e..3a45231 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/Entity.java @@ -1,6 +1,5 @@ package com.example.orm.jpa; -import java.io.Serializable; /** * Interface for entity objects. diff --git a/chapter08/02 - customfield/src/main/resources/application-dev.properties b/chapter08/02 - customfield/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter08/02 - customfield/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/02 - customfield/src/main/resources/application-local.properties b/chapter08/02 - customfield/src/main/resources/application-local.properties index 8f13f3f..7e354d5 100644 --- a/chapter08/02 - customfield/src/main/resources/application-local.properties +++ b/chapter08/02 - customfield/src/main/resources/application-local.properties @@ -3,13 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +#spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter08/02 - customfield/src/main/resources/application.properties b/chapter08/02 - customfield/src/main/resources/application.properties index e69de29..3e80adf 100644 --- a/chapter08/02 - customfield/src/main/resources/application.properties +++ b/chapter08/02 - customfield/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/copsboot + +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter08/02 - customfield/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter08/02 - customfield/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter08/02 - customfield/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter08/02 - customfield/src/main/resources/db/migration/V1.0.0.2__reports.sql b/chapter08/02 - customfield/src/main/resources/db/migration/V1.0.0.2__reports.sql new file mode 100644 index 0000000..cc2e26c --- /dev/null +++ b/chapter08/02 - customfield/src/main/resources/db/migration/V1.0.0.2__reports.sql @@ -0,0 +1,8 @@ +CREATE TABLE report +( + date_time TIMESTAMP(6) WITH TIME ZONE, + id uuid NOT NULL, + description VARCHAR(255), + reporter_id uuid, + PRIMARY KEY (id) +); diff --git a/chapter08/02 - customfield/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter08/02 - customfield/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter08/02 - customfield/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter08/02 - customfield/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter08/02 - customfield/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter08/02 - customfield/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter08/02 - customfield/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter08/02 - customfield/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter08/02 - customfield/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..5feb390 100644 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter08/02 - customfield/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,16 @@ package com.example.copsboot; import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java index 51e8f79..dcf5eee 100644 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java +++ b/chapter08/02 - customfield/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java @@ -1,61 +1,76 @@ package com.example.copsboot.report.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; import com.example.copsboot.report.ReportService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.time.ZonedDateTime; +import java.time.Instant; +import java.util.Optional; import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; //tag::class[] -@RunWith(SpringRunner.class) @CopsbootControllerTest(ReportRestController.class) public class ReportRestControllerTest { @Autowired - private MockMvc mvc; - - @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; @MockBean private ReportService service; + @MockBean + private UserService userService; @Test public void officerIsAbleToPostAReport() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - ZonedDateTime dateTime = ZonedDateTime.parse("2018-04-11T22:59:03.189+02:00"); - String description = "This is a test report description."; - CreateReportParameters parameters = new CreateReportParameters(dateTime, - description); - when(service.createReport(eq(Users.officer().getId()), any(ZonedDateTime.class), eq(description))) - .thenReturn(new Report(new ReportId(UUID.randomUUID()), Users.officer(), dateTime, description)); - mvc.perform(post("/api/reports") - .header(HEADER_AUTHORIZATION, bearer(accessToken)) - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("reporter").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("dateTime").value("2018-04-11T22:59:03.189+02:00")) - .andExpect(jsonPath("description").value(description)); + UserId userId = new UserId(UUID.randomUUID()); + AuthServerId authServerId = new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); + User user = new User(userId, + "wim@example.com", + authServerId, + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"); + when(userService.findUserByAuthServerId(authServerId)) + .thenReturn(Optional.of(user)); + when(userService.getUserById(userId)) + .thenReturn(user); + when(service.createReport(any(CreateReportParameters.class))) + .thenReturn(new Report(new ReportId(UUID.randomUUID()), + userId, + Instant.parse("2023-04-11T22:59:03.189+02:00"), + "This is a test report description.")); + mockMvc.perform(post("/api/reports") + .with(jwt().jwt(builder -> builder.subject(authServerId.value().toString()) + .claim("email", "wim@example.com")) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "dateTime": "2023-04-11T22:59:03.189+02:00", + "description": "This is a test report description." + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("id").exists()) + .andExpect(jsonPath("reporter").value("wim@example.com")) + .andExpect(jsonPath("dateTime").value("2023-04-11T20:59:03.189Z")) + .andExpect(jsonPath("description").value("This is a test report description.")); } } -//end::class[] \ No newline at end of file +//end::class[] diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java deleted file mode 100644 index 720f959..0000000 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.copsboot.user; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.HashSet; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> -@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) //<2> -public class UserRepositoryIntegrationTest { - @Autowired - private UserRepository repository; - @PersistenceContext - private EntityManager entityManager; - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void testSaveUser() { - Set roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); - - assertThat(repository.count()).isEqualTo(1L); - - entityManager.flush(); //<3> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM user_roles", Long.class)).isEqualTo(1L); - assertThat(jdbcTemplate.queryForObject("SELECT roles FROM user_roles", String.class)).isEqualTo("OFFICER"); - } -} diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..19c23fe 100644 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -3,14 +3,16 @@ import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,62 +21,34 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) //<2> public class UserRepositoryTest { @Autowired private UserRepository repository; + @PersistenceContext + private EntityManager entityManager; + @Autowired + private JdbcTemplate jdbcTemplate; - //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } + assertThat(repository.count()).isEqualTo(1L); - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); + entityManager.flush(); //<3> - assertThat(optional).isEmpty(); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> + assertThat(jdbcTemplate.queryForObject("SELECT email FROM copsboot_user", String.class)).isEqualTo("alex.foley@beverly-hills.com"); } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @Bean @@ -82,5 +56,4 @@ public UniqueIdGenerator generator() { return new InMemoryUniqueIdGenerator(); } } - //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/Users.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index e0d24b0..805c501 100644 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,133 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index eb07c50..a20d744 100644 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,118 +1,104 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } + // tag::emptyToken[] @Test - public void testCreateOfficerIfPasswordIsTooShort() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("errors[0].fieldName").value("password")); - - verify(service, never()).createOfficer(email, password); + void givenEmptyMobileToken_badRequestIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "" + } + """)) //<.> + .andExpect(status().isBadRequest()) //<.> + .andDo(print()); //<.> + + verify(userService, never()).createUser(any(CreateUserParameters.class)); //<.> } + // end::emptyToken[] } diff --git a/chapter08/02 - customfield/src/test/resources/application-integration-test.properties b/chapter08/02 - customfield/src/test/resources/application-integration-test.properties index 159536c..c61e563 100644 --- a/chapter08/02 - customfield/src/test/resources/application-integration-test.properties +++ b/chapter08/02 - customfield/src/test/resources/application-integration-test.properties @@ -1,11 +1,6 @@ -spring.datasource.url=jdbc:tc:postgresql://localhost/copsbootdb +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.username=user spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none - -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/02 - customfield/src/test/resources/application-repository-test.properties b/chapter08/02 - customfield/src/test/resources/application-repository-test.properties new file mode 100644 index 0000000..c61e563 --- /dev/null +++ b/chapter08/02 - customfield/src/test/resources/application-repository-test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=user +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/02 - customfield/src/test/resources/application-test.properties b/chapter08/02 - customfield/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter08/02 - customfield/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/02 - customfield/src/test/resources/logback-test.xml b/chapter08/02 - customfield/src/test/resources/logback-test.xml index bf47fec..164429c 100644 --- a/chapter08/02 - customfield/src/test/resources/logback-test.xml +++ b/chapter08/02 - customfield/src/test/resources/logback-test.xml @@ -5,7 +5,7 @@ - + @@ -17,14 +17,8 @@ - - - - - - - \ No newline at end of file + diff --git a/chapter08/03 - customfieldfinal/.mvn/wrapper/maven-wrapper.jar b/chapter08/03 - customfieldfinal/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 zcmb5V1CS=sk~Z9!wr$(CZEL#U=Co~N+O}=mwr$(Cds^S@-Tij=#=rmlVk@E|Dyp8$ z$UKz?`Q$l@GN3=8fq)=^fVx`E)Pern1@-q?PE1vZPD);!LGdpP^)C$aAFx&{CzjH` zpQV9;fd0PyFPNN=yp*_@iYmRFcvOrKbU!1a*o)t$0ex(~3z5?bw11HQYW_uDngyer za60w&wz^`W&Z!0XSH^cLNR&k>%)Vr|$}(wfBzmSbuK^)dy#xr@_NZVszJASn12dw; z-KbI5yz=2awY0>OUF)&crfPu&tVl|!>g*#ur@K=$@8N05<_Mldg}X`N6O<~3|Dpk3 zRWb!e7z<{Mr96 z^C{%ROigEIapRGbFA5g4XoQAe_Y1ii3Ci!KV`?$ zZ2Hy1VP#hVp>OOqe~m|lo@^276Ik<~*6eRSOe;$wn_0@St#cJy}qI#RP= zHVMXyFYYX%T_k3MNbtOX{<*_6Htq*o|7~MkS|A|A|8AqKl!%zTirAJGz;R<3&F7_N z)uC9$9K1M-)g0#}tnM(lO2k~W&4xT7gshgZ1-y2Yo-q9Li7%zguh7W#kGfnjo7Cl6 z!^wTtP392HU0aVB!$cPHjdK}yi7xNMp+KVZy3_u}+lBCloJ&C?#NE@y$_{Uv83*iV zhDOcv`=|CiyQ5)C4fghUmxmwBP0fvuR>aV`bZ3{Q4&6-(M@5sHt0M(}WetqItGB1C zCU-)_n-VD;(6T1%0(@6%U`UgUwgJCCdXvI#f%79Elbg4^yucgfW1^ zNF!|C39SaXsqU9kIimX0vZ`U29)>O|Kfs*hXBXC;Cs9_Zos3%8lu)JGm~c19+j8Va z)~kFfHouwMbfRHJ``%9mLj_bCx!<)O9XNq&uH(>(Q0V7-gom7$kxSpjpPiYGG{IT8 zKdjoDkkMTL9-|vXDuUL=B-K)nVaSFd5TsX0v1C$ETE1Ajnhe9ept?d;xVCWMc$MbR zL{-oP*vjp_3%f0b8h!Qija6rzq~E!#7X~8^ZUb#@rnF~sG0hx^Ok?G9dwmit494OT z_WQzm_sR_#%|I`jx5(6aJYTLv;3U#e@*^jms9#~U`eHOZZEB~yn=4UA(=_U#pYn5e zeeaDmq-$-)&)5Y}h1zDbftv>|?GjQ=)qUw*^CkcAG#o%I8i186AbS@;qrezPCQYWHe=q-5zF>xO*Kk|VTZD;t={XqrKfR|{itr~k71VS?cBc=9zgeFbpeQf*Wad-tAW7(o ze6RbNeu31Uebi}b0>|=7ZjH*J+zSj8fy|+T)+X{N8Vv^d+USG3arWZ?pz)WD)VW}P z0!D>}01W#e@VWTL8w1m|h`D(EnHc*C5#1WK4G|C5ViXO$YzKfJkda# z2c2*qXI-StLW*7_c-%Dws+D#Kkv^gL!_=GMn?Y^0J7*3le!!fTzSux%=1T$O8oy8j z%)PQ9!O+>+y+Dw*r`*}y4SpUa21pWJ$gEDXCZg8L+B!pYWd8X;jRBQkN_b=#tb6Nx zVodM4k?gF&R&P=s`B3d@M5Qvr;1;i_w1AI=*rH(G1kVRMC`_nohm~Ie5^YWYqZMV2<`J* z`i)p799U_mcUjKYn!^T&hu7`Lw$PkddV&W(ni)y|9f}rGr|i-7nnfH6nyB$Q{(*Nv zZz@~rzWM#V@sjT3ewv9c`pP@xM6D!StnV@qCdO${loe(4Gy00NDF5&@Ku;h2P+Vh7 z(X6De$cX5@V}DHXG?K^6mV>XiT768Ee^ye&Cs=2yefVcFn|G zBz$~J(ld&1j@%`sBK^^0Gs$I$q9{R}!HhVu|B@Bhb29PF(%U6#P|T|{ughrfjB@s- zZ)nWbT=6f6aVyk86h(0{NqFg#_d-&q^A@E2l0Iu0(C1@^s6Y-G0r32qll>aW3cHP# zyH`KWu&2?XrIGVB6LOgb+$1zrsW>c2!a(2Y!TnGSAg(|akb#ROpk$~$h}jiY&nWEz zmMxk4&H$8yk(6GKOLQCx$Ji-5H%$Oo4l7~@gbHzNj;iC%_g-+`hCf=YA>Z&F)I1sI z%?Mm27>#i5b5x*U%#QE0wgsN|L73Qf%Mq)QW@O+)a;#mQN?b8e#X%wHbZyA_F+`P%-1SZVnTPPMermk1Rpm#(;z^tMJqwt zDMHw=^c9%?#BcjyPGZFlGOC12RN(i`QAez>VM4#BK&Tm~MZ_!#U8PR->|l+38rIqk zap{3_ei_txm=KL<4p_ukI`9GAEZ+--)Z%)I+9LYO!c|rF=Da5DE@8%g-Zb*O-z8Tv zzbvTzeUcYFgy{b)8Q6+BPl*C}p~DiX%RHMlZf;NmCH;xy=D6Ii;tGU~ zM?k;9X_E?)-wP|VRChb4LrAL*?XD6R2L(MxRFolr6GJ$C>Ihr*nv#lBU>Yklt`-bQ zr;5c(o}R!m4PRz=CnYcQv}m?O=CA(PWBW0?)UY)5d4Kf;8-HU@=xMnA#uw{g`hK{U zB-EQG%T-7FMuUQ;r2xgBi1w69b-Jk8Kujr>`C#&kw-kx_R_GLRC}oum#c{je^h&x9 zoEe)8uUX|SahpME4SEog-5X^wQE0^I!YEHlwawJ|l^^0kD)z{o4^I$Eha$5tzD*A8 zR<*lss4U5N*JCYl;sxBaQkB3M8VT|gXibxFR-NH4Hsmw|{={*Xk)%!$IeqpW&($DQ zuf$~fL+;QIaK?EUfKSX;Gpbm8{<=v#$SrH~P-it--v1kL>3SbJS@>hAE2x_k1-iK# zRN~My-v@dGN3E#c!V1(nOH>vJ{rcOVCx$5s7B?7EKe%B`bbx(8}km#t2a z1A~COG(S4C7~h~k+3;NkxdA4gbB7bRVbm%$DXK0TSBI=Ph6f+PA@$t){_NrRLb`jp zn1u=O0C8%&`rdQgO3kEi#QqiBQcBcbG3wqPrJ8+0r<`L0Co-n8y-NbWbx;}DTq@FD z1b)B$b>Nwx^2;+oIcgW(4I`5DeLE$mWYYc7#tishbd;Y!oQLxI>?6_zq7Ej)92xAZ z!D0mfl|v4EC<3(06V8m+BS)Vx90b=xBSTwTznptIbt5u5KD54$vwl|kp#RpZuJ*k) z>jw52JS&x)9&g3RDXGV zElux37>A=`#5(UuRx&d4qxrV<38_w?#plbw03l9>Nz$Y zZS;fNq6>cGvoASa2y(D&qR9_{@tVrnvduek+riBR#VCG|4Ne^w@mf2Y;-k90%V zpA6dVw|naH;pM~VAwLcQZ|pyTEr;_S2GpkB?7)+?cW{0yE$G43`viTn+^}IPNlDo3 zmE`*)*tFe^=p+a{a5xR;H0r=&!u9y)kYUv@;NUKZ)`u-KFTv0S&FTEQc;D3d|KEKSxirI9TtAWe#hvOXV z>807~TWI~^rL?)WMmi!T!j-vjsw@f11?#jNTu^cmjp!+A1f__Dw!7oqF>&r$V7gc< z?6D92h~Y?faUD+I8V!w~8Z%ws5S{20(AkaTZc>=z`ZK=>ik1td7Op#vAnD;8S zh<>2tmEZiSm-nEjuaWVE)aUXp$BumSS;qw#Xy7-yeq)(<{2G#ap8z)+lTi( ziMb-iig6!==yk zb6{;1hs`#qO5OJQlcJ|62g!?fbI^6v-(`tAQ%Drjcm!`-$%Q#@yw3pf`mXjN>=BSH z(Nftnf50zUUTK;htPt0ONKJq1_d0!a^g>DeNCNpoyZhsnch+s|jXg1!NnEv%li2yw zL}Y=P3u`S%Fj)lhWv0vF4}R;rh4&}2YB8B!|7^}a{#Oac|%oFdMToRrWxEIEN<0CG@_j#R4%R4i0$*6xzzr}^`rI!#y9Xkr{+Rt9G$*@ zQ}XJ+_dl^9@(QYdlXLIMI_Q2uSl>N9g*YXMjddFvVouadTFwyNOT0uG$p!rGF5*`1 z&xsKPj&;t10m&pdPv+LpZd$pyI_v1IJnMD%kWn{vY=O3k1sJRYwPoDV1S4OfVz4FB z$^ygjgHCW=ySKSsoSA&wSlq83JB+O-)s>>e@a{_FjB{@=AlrX7wq>JE=n@}@fba(;n4EG| zge1i)?NE@M@DC5eEv4; z#R~0aNssmFHANL@-eDq2_jFn=MXE9y>1FZH4&v<}vEdB6Kz^l)X%%X@E#4)ahB(KY zx8RH+1*6b|o1$_lRqi^)qoLs;eV5zkKSN;HDwJIx#ceKS!A$ZJ-BpJSc*zl+D~EM2 zm@Kpq2M*kX`;gES_Dd1Y#UH`i!#1HdehqP^{DA-AW^dV(UPu|O@Hvr>?X3^~=1iaRa~AVXbj z-yGL<(5}*)su2Tj#oIt+c6Gh}$0|sUYGGDzNMX+$Oi$e&UJt3&kwu)HX+XP{es(S3 z%9C9y({_fu>^BKjI7k;mZ4DKrdqxw`IM#8{Sh?X(6WE4S6-9M}U0&e32fV$2w{`19 zd=9JfCaYm@J$;nSG3(|byYDqh>c%`JW)W*Y0&K~g6)W?AvVP&DsF_6!fG3i%j^Q>R zR_j5@NguaZB{&XjXF+~6m|utO*pxq$8?0GjW0J-e6Lnf0c@}hvom8KOnirhjOM7!n zP#Iv^0_BqJI?hR5+Dl}p!7X}^NvFOCGvh9y*hgik<&X)3UcEBCdUr$Dt8?0f&LSur ze*n!(V(7umZ%UCS>Hf(g=}39OcvGbf2+D;OZ089m_nUbdCE0PXJfnyrIlLXGh2D!m zK=C#{JmoHY1ws47L0zeWkxxV=A%V8a&E^w%;fBp`PN_ndicD@oN?p?Bu~20>;h;W` ztV=hI*Ts$6JXOwOY?sOk_1xjzNYA#40dD}|js#3V{SLhPEkn5>Ma+cGQi*#`g-*g56Q&@!dg)|1YpLai3Bu8a;l2fnD6&)MZ~hS%&J}k z2p-wG=S|5YGy*Rcnm<9VIVq%~`Q{g(Vq4V)CP257v06=M2W|8AgZO0CC_}HVQ>`VU zy;2LDlG1iwIeMj?l40_`21Qsm?d=1~6f4@_&`lp~pIeXnR)wF0z7FH&wu~L~mfmMr zY4_w6tc{ZP&sa&Ui@UxZ*!UovRT})(p!GtQh~+AMZ6wcqMXM*4r@EaUdt>;Qs2Nt8 zDCJi#^Rwx|T|j_kZi6K!X>Ir%%UxaH>m6I9Yp;Sr;DKJ@{)dz4hpG>jX?>iiXzVQ0 zR$IzL8q11KPvIWIT{hU`TrFyI0YQh`#>J4XE*3;v^07C004~FC7TlRVVC}<}LC4h_ zZjZ)2*#)JyXPHcwte!}{y%i_!{^KwF9qzIRst@oUu~4m;1J_qR;Pz1KSI{rXY5_I_ z%gWC*%bNsb;v?>+TbM$qT`_U8{-g@egY=7+SN#(?RE<2nfrWrOn2OXK!ek7v`aDrH zxCoFHyA&@^@m+#Y(*cohQ4B76me;)(t}{#7?E$_u#1fv)vUE5K;jmlgYI0$Mo!*EA zf?dx$4L(?nyFbv|AF1kB!$P_q)wk1*@L0>mSC(A8f4Rgmv1HG;QDWFj<(1oz)JHr+cP|EPET zSD~QW&W(W?1PF-iZ()b|UrnB(#wG^NR!*X}t~OS-21dpXq)h)YcdA(1A`2nzVFax9rx~WuN=SVt`OIR=eE@$^9&Gx_HCfN= zI(V`)Jn+tJPF~mS?ED7#InwS&6OfH;qDzI_8@t>In6nl zo}q{Ds*cTG*w3CH{Mw9*Zs|iDH^KqmhlLp_+wfwIS24G z{c@fdgqy^Y)RNpI7va^nYr9;18t|j=AYDMpj)j1oNE;8+QQ)ap8O??lv%jbrb*a;} z?OvnGXbtE9zt;TOyWc|$9BeSGQbfNZR`o_C!kMr|mzFvN+5;g2TgFo8DzgS2kkuw@ z=`Gq?xbAPzyf3MQ^ZXp>Gx4GwPD))qv<1EreWT!S@H-IpO{TPP1se8Yv8f@Xw>B}Y z@#;egDL_+0WDA)AuP5@5Dyefuu&0g;P>ro9Qr>@2-VDrb(-whYxmWgkRGE(KC2LwS z;ya>ASBlDMtcZCCD8h+Awq1%A|Hbx)rpn`REck#(J^SbjiHXe-jBp!?>~DC7Wb?mC z_AN+^nOt;3tPnaRZBEpB6s|hCcFouWlA{3QJHP!EPBq1``CIsgMCYD#80(bsKpvwO)0#)1{ zos6v&9c=%W0G-T@9sfSLxeGZvnHk$SnHw57+5X4!u1dvH0YwOvuZ7M^2YOKra0dqR zD`K@MTs(k@h>VeI5UYI%n7#3L_WXVnpu$Vr-g}gEE>Y8ZQQsj_wbl&t6nj{;ga4q8SN#Z6cBZepMoyv7MF-tnnZp*(8jq848yZ zsG_fP$Y-rtCAPPI7QC^nzQjlk;p3tk88!1dJuEFZ!BoB;c!T>L>xSD<#+4X%*;_IB z0bZ%-SLOi5DV7uo{z}YLKHsOHfFIYlu8h(?gRs9@bbzk&dkvw*CWnV;GTAKOZfbY9 z(nKOTQ?fRRs(pr@KsUDq@*P`YUk4j=m?FIoIr)pHUCSE84|Qcf6GucZBRt;6oq_8Z zP^R{LRMo?8>5oaye)Jgg9?H}q?%m@2bBI!XOOP1B0s$%htwA&XuR`=chDc2)ebgna zFWvevD|V882V)@vt|>eeB+@<-L0^6NN%B5BREi8K=GwHVh6X>kCN+R3l{%oJw5g>F zrj$rp$9 zhepggNYDlBLM;Q*CB&%w zW+aY{Mj{=;Rc0dkUw~k)SwgT$RVEn+1QV;%<*FZg!1OcfOcLiF@~k$`IG|E8J0?R2 zk?iDGLR*b|9#WhNLtavx0&=Nx2NII{!@1T78VEA*I#65C`b5)8cGclxKQoVFM$P({ zLwJKo9!9xN4Q8a2F`xL&_>KZfN zOK?5jP%CT{^m4_jZahnn4DrqgTr%(e_({|z2`C2NrR6=v9 z*|55wrjpExm3M&wQ^P?rQPmkI9Z9jlcB~4IfYuLaBV95OGm#E|YwBvj5Z}L~f`&wc zrFo!zLX*C{d2}OGE{YCxyPDNV(%RZ7;;6oM*5a>5LmLy~_NIuhXTy-*>*^oo1L;`o zlY#igc#sXmsfGHA{Vu$lCq$&Ok|9~pSl5Q3csNqZc-!a;O@R$G28a@Sg#&gnrYFsk z&OjZtfIdsr%RV)bh>{>f883aoWuYCPDP{_)%yQhVdYh;6(EOO=;ztX1>n-LcOvCIr zKPLkb`WG2;>r)LTp!~AlXjf-Oe3k`Chvw$l7SB2bA=x3s$;;VTFL0QcHliysKd^*n zg-SNbtPnMAIBX7uiwi&vS)`dunX$}x)f=iwHH;OS6jZ9dYJ^wQ=F#j9U{wJ9eGH^#vzm$HIm->xSO>WQ~nwLYQ8FS|?l!vWL<%j1~P<+07ZMKkTqE0F*Oy1FchM z2(Nx-db%$WC~|loN~e!U`A4)V4@A|gPZh`TA18`yO1{ z(?VA_M6SYp-A#%JEppNHsV~kgW+*Ez=?H?GV!<$F^nOd+SZX(f0IoC#@A=TDv4B2M z%G-laS}yqR0f+qnYW_e7E;5$Q!eO-%XWZML++hz$Xaq@c%2&ognqB2%k;Cs!WA6vl z{6s3fwj*0Q_odHNXd(8234^=Asmc0#8ChzaSyIeCkO(wxqC=R`cZY1|TSK)EYx{W9 z!YXa8GER#Hx<^$eY>{d;u8*+0ocvY0f#D-}KO!`zyDD$%z1*2KI>T+Xmp)%%7c$P< zvTF;ea#Zfzz51>&s<=tS74(t=Hm0dIncn~&zaxiohmQn>6x`R+%vT%~Dhc%RQ=Cj^ z&%gxxQo!zAsu6Z+Ud#P!%3is<%*dJXe!*wZ-yidw|zw|C`cR z`fiF^(yZt?p{ZX|8Ita)UC$=fg6wOve?w+8ww|^7OQ0d zN(3dmJ@mV8>74I$kQl8NM%aC+2l?ZQ2pqkMs{&q(|4hwNM z^xYnjj)q6uAK@m|H$g2ARS2($e9aqGYlEED9sT?~{isH3Sk}kjmZ05Atkgh^M6VNP zX7@!i@k$yRsDK8RA1iqi0}#Phs7y(bKYAQbO9y=~10?8cXtIC4@gF#xZS;y3mAI`h zZ^VmqwJ%W>kisQ!J6R?Zjcgar;Il%$jI*@y)B+fn^53jQd0`)=C~w%Lo?qw!q3fVi{~2arObUM{s=q)hgBn64~)W0tyi?(vlFb z>tCE=B1cbfyY=V38fUGN(#vmn1aY!@v_c70}pa(Lrle-(-SH8Nd!emQF zf3kz0cE~KzB%37B24|e=l4)L}g1AF@v%J*A;5F7li!>I0`lfO9TR+ak`xyqWnj5iwJ$>t_vp(bet2p(jRD;5Q9x2*`|FA4#5cfo8SF@cW zeO{H7C0_YJ*P@_BEvm2dB}pUDYXq@G1^Ee#NY9Q`l`$BUXb01#lmQk^{g3?aaP~(* zD;INgi#8TDZ&*@ZKhx$jA^H-H1Lp`%`O{Y{@_o!+7ST}{Ng^P;X>~Bci{|Qdf1{}p z_kK+zL;>D30r6~R?|h!5NKYOi6X&I5)|ME+NG>d9^`hxKpU^)KBOpZiU^ z;|SzGWtbaclC-%9(zR-|q}kB8H&($nsB1LPAkgcm+Qs@cAov{IXxo5PHrH(8DuEMb z3_R#>7^jjGeS7$!`}m8!8$z|)I~{dhd)SvoH9oR9#LjO{{8O&r7w{d9V1z^syn&E6 z{DG0vlQF_Yb3*|>RzVop^{$mWp|%NDYj@4{d*-@O^<(=L=DMFIQHEp-dtz@1Rumd; zadt^4B#(uUyM6aeUJkGl0GfaULpR!2Ql&q$nEV^+SiDptdPbuJ=VJ)`czZ@&HPUuj zc5dSRB&xk)dI~;6N?wkzI}}4K3i%I=EnlKGpPJ9hu?mNzH7|H0j(mN3(ubdaps3GM z1i+9gk=!$mH=L#LRDf4!mXw0;uxSUIXhl|#h*uK+fQPilJc8RCK9GNPt=X^8`*;3$ zBBo77gkGB5F8a8)*OR10nK&~8CEMPVQyhY>i`PS{L^-*WAz$ljtU%zlG1lm%%U4Zw zms0oZR8b|`>4U1X*9JLQQ>m9MF5%ppoafz^;`7DbmmIENrc$hucekkE4I83WhT%(9 zMaE;f7`g4B#vl(#tNP8$3q{$&oY*oa0HLX6D?xTW3M6f<^{%CK4OE1Pmfue`M6Dh= z&Z-zrq$^xhP%|hU&)(+2KSSpeHgX^0?gRZ5wA8@%%9~@|*Ylux1M{WQ4ekG(T+_b` zb6I)QRGp%fRF)^T?i^j&JDBhfNU9?>Sl6WVMM%S?7< ze|4gaDbPooB=F4Y=>~_+y~Q1{Ox@%q>v+_ZIOfnz5y+qy zhi+^!CE*Lv-}>g^%G=bGLqD(aTN;yHDBH#tOC=X02}QU~Xdme``Wn>N>6{VwgU~Z>g+0 zxv0`>>iSfu$baHMw8(^FL6QWe;}(U>@;8j)t)yHAOj?SdeH;evFx-kpU@nT>lsrUt zqhV}2pD^5bC4786guG1`5|fK@pE6xcT#ns)vR|^?A08G62teHaE&p`ZrCBj_Swt*~dVt=5*RK6Y{% zABqK$X59BnrK3r3u=wxklRnA1uh+q`?T0kE1YhvDWF4OY#<(+V|R@R%tdkq2huF(!Ip+EpZF3zr*|9pmKHPo)Cu z;H+^s&`Ql}u=Jt~ZWj`bAw|i-3#7(2WuRU3DU{BW8`?!O?YO1M$*MMTsaEM!5Jyp~ z!gp6yR4$O%wQ8%dyz43ZPeoJwy;o;yg=S0^Y}%|)to>=N^`!3VMf1~}OZ`Dl$q&|w z9$!i3!i1uAgPTuKSWdBrDr*N$g=E#mdqfj*h;Z}OG`{n245+g;IKfdn!&gF2OtHaD zyGDzj@@d2!P(_Ux)3v;1ABTj__{w*kaRF-1YVU`})Acgk?(T*1YqEve3=5)8bkZK* z!Tus*e$h@^u z>#zV0771Bix~r&h2FJ9)%N{>s>?2tk1$bId)1#G;OKgn-U8jUo^AK;Hu)hQEi}swD(264kAS-SBCD$R(Ro0rh8~Le zzRwxbz_JHDbD+hTX15AWmVw!#rC)-zeZahQQmo6FG1)ah3uuyIuTMof}RO!`Y3^Fxn_-G$23RDOh(@NU?r6`*S?#E50)w zpcsgDZ-iO{;EesgDQq9;p*C#QH(sp~2w^zAJWaUL%@yo)iIL6y8;e_}=dwQc%k%;H zFt5lenH*`}LWd+fPqi;exJeRZgl&nLR%|a!%1x0RQ54cgyWBYrL>sskcAtPxi&8c( zw_K?sI*3n%S;lKiYpveBN08{rgV&-B1NN5Jiu07~%n#%&f!(R(z1)xsxtRBkg#+Lv zh21zX?aYDd_f}qdA`Os*j!eC<5)iUJ&Twj7?*p%vEOGElGhpRZsccM!<k}DeC;TY;rULQs3e}lZyP#UVb=6 zB$Dkm2FaHWUXr7<{R&46sfZ)&(HXxB_=e`%LZci`s7L6c-L7iF&wdmTJz`*^=jD~* zpOZ@jcq8LezVkE^M6D9^QgZqnX&x*mr1_Cf#R9R3&{i3%v#}V$UZzGC;Or*=Dw5SXBC6NV|sGZp^#%RTimyaj@!ZuyJ z6C+r}O1TsAzV9PAa*Gd!9#FQMl)ZLHzTr99biAqA(dz-m9LeIeKny3YB=*+|#-Gq# zaErUR5Z*Wh^e<+wcm70eW;f-g=YTbMiDX)AznDM6B73)T4r%nq+*hKcKF?)#vbv?K zPMe=sFCuC*ZqsBPh-?g!m*O`}6<}Pfj}Y1n9|Y@cUdD5GX_)6Sx9pPfS7 zxkt?g6ZwJ+50C7qrh6dMFmr7qah`FskT_H=GC92vkVh$WfZa2%5L99_DxyM{$#6HQ zx$VR-Wwt!q9JL2{ybEGJr$^?!V4m_BqDqt!mbs=QjHf340+^a{)waVvP0+98(BA$M ztWr&sM=juyYgvf`(SC}+y@QtYgU>0ghJ6VbU}|kEraR&&W%#;!#KI?le%g`e>ZVPiDrneh#&1(Y?uiMo^f5qo@{JEr(p9>8GhDa+PC9yG;lX+D?hQ^fZB&Sdox219zUj_5;+n<0@Wi3@DK`MU8FM!OFJ z8*_mTA-u!Ab#95FRVWTIqAL#BVQGxE_s?>Ql|@0o9vos&r<_4d!+Q6(_270)6#lu$ zV!j$a?_V0I<(3Z=J7C-K0a^Kc1Go9p&T6yQeAD+)dG-$a&%Fo0AOte~_Z&_m2@ue~ z9cKFf-A41Dz31Ooj9FSR`l?H5UtdP?JS=UU$jF#znE1k@0g%K?KQuwZkfDI3Ai)(q z#x_Yo6WR_Y@#6I_02S&NpcP<%sw!!M_3#*8qa+*4rS@x=i{-2K#*Qr)*Q$-{<_(<| z0730e+rubnT38*m;|$-4!1r6u&Ua2kO_s-(7*NGgDTe##%I>_9uW;X__b_k)xlv$; zW%K2hsmr>5e^Z~`tS-eUgWmSF9}Yg8E}qydSVX0nYZMX_x94QK?tw2>^;raVTqstR zIrNAX2`X~|h->dTOb9IrA!i5INpLV}99ES|i0ldzC`;R$FBY5&7+TIy8%GO8SZ37_ zw=^Swk?z+j-&0-cTE|LU0q@IKRa&C6ZlXbSa2vN5r-)*f<3{wLV*uJUw980AFkWN7 zKh{?97GmVu-0rs9FB6ludy|n`gN5p~?y51aJzBg6#+-=0pWdZ2n4xTiQ=&3As-!-6 zFlb|ssAJEJL#s8(=odfz8^9b#@RrvNE4gjuEITzAd7R4+rq$yEJKXP?6D@yM7xZ&^ z@%jnE3}bteJo{p(l`hu`Yvzg9I#~>(T;>c;ufeLfc!m3D&RaQS=gAtEO-WbI+f_#| zaVpq-<%~=27U8*qlVCuI6z9@j)#R!z3{jc>&I(qT-8IBW57_$z5Qm3gVC1TcWJNc% zDk?H3%QHno@fu9nT%L^K)=#sRiRNg|=%M zR;8BE)QA4#Dsg^EakzttRg9pkfIrF3iVYVM#*_+#3X+~qeZc^WQJvEyVlO@9=0pl!ayNOh|{j0j^a z+zi_$_0QKhwArW)sJ$wji;A`?$ecbr?(4x5%2pLgh#wggbt)#T^2R3a9m+>GcrUxU z*u-WTgHAN*e!0;Wa%1k)J_P(Vdp>vwrROTVae@6Wn04q4JL-)g&bWO6PWGuN2Q*s9 zn47Q2bIn4=!P1k0jN_U#+`Ah59zRD??jY?s;U;k@%q87=dM*_yvLN0->qswJWb zImaj{Ah&`)C$u#E0mfZh;iyyWNyEg;w0v%QS5 zGXqad{`>!XZJ%+nT+DiVm;lahOGmZyeqJ-;D&!S3d%CQS4ZFM zkzq5U^O|vIsU_erz_^^$|D0E3(i*&fF-fN}8!k3ugsUmW1{&dgnk!|>z2At?h^^T@ zWN_|`?#UM!FwqmSAgD6Hw%VM|fEAlhIA~^S@d@o<`-sxtE(|<><#76_5^l)Xr|l}Q zd@7Fa8Bj1ICqcy2fKl1rD4TYd84)PG5Ee2W4Nt@NNmpJWvc3q@@*c;~%^Vasf2H`y z+~U-19wtFT?@yIFc4SE_ab?s@wEUfSkOED}+qVjjy>=eac2^S^+|_3%cjH%EUTJ&r znp9q?RbStJcT*Vi{3KDa^jr4>{5x+?!1)8c2SqiCEzE$TQ+`3KPQQnG8_Qk<^)y_o zt1Q^f{#yCUt!1e(3;E6y?>p+7sGAYLp`lA3c~Y`re9q&`c6>0?c0E2Ap5seFv92#X z1Vldj!7A8@8tWr&?%;EBQ_Fwd)8A3!wIx`V!~~h(!$pCy7=&*+*uIzG@*d%*{qG#4 zX0^}}sRN^N=p{w(+yjv%xwb!%lnVTE7l1l6gJwQmq_G83J&Y98$S!r*L8}IiIa2E= zE!0tbOuEDb*No0-KB{zjo1k#_4FHtr{!)>o+Y@bll}Sa6D^xktI0H&l{jKAK)A(iz zB-N00F?~Z}Y7tG+vp)-q*v71(C}65$-=uXx^|R$xx9zZip-V>Hqeyfd(wteM)+!!H z$s+>g4I@+`h2>C|J;PhvtOq)`xm4;CyF}R<)!ma3T{Vf_5|zo;D4YI4ZDBkE(vMeE zb#ZV;n}CgA0w8x!UC2&5Z(K)9bibj#?~>R(72lFx_Am~jS?;7mo~p+05~XGD+(wV4 zEVYnf0N5+-7O+Gc1L!sPGUHv<6=cV8}*m$m`kBs@z zy;goR(?J^JrB7uXXpD00+SD0luk!vK3wwp(N%|X!HmO{xC#OMYQ&a7Yqv-54iEUK4 zVH;)rY6)pUX~ESvQK^w|&}>J{I?YlvOhpMgt-JB}m5Br`Q9X+^8+Xa%S81hO<1t#h zbS+MljFP1J0GGNR1}KwE=cfey%;@n&@Kli+Z5d>daJjbvuO3dW{r$1FT0j zR$c9$t~P50P+NhG^krLH%k}wsQ%mm+@#c;-c9>rYy;8#(jZ|KA8RrmnN2~>w0ciU7 zGiLC?Q^{^Ox-9F()RE^>Xq(MAbGaT0^6jc>M5^*&uc@YGt5Iw4i{6_z5}H$oO`arY z4BT(POK%DnxbH>P$A;OWPb@gYS96F7`jTn6JO@hdM za>_p!1mf?ULJZb1w-+HamqN__2CtI%VK`k^(++Ga0%z*z@k0wYJDqT^)~%|4O299; zh1_iRtc7you(kOK8?Q$R7v-@Qk4+i=8GD2_zI0%{Ra`_prF{+UPW^m5MCA&4ZUpZb z2*!)KA8b--Upp~U%f+rsmCmV~!Y>Gzl#yVvZER2h;f&rkdx{r#9mc8DZMJaQXs?SL zCg3#>xR6ve8&YkP*`Z=lng|Ow+h@t*!Ial*XQg3P;VS8@E1C)VS`?L9N+rxlD7bxC z3@Ag)Vu?#ykY`ND+GvRYTUP&-KDMiqly$Z~uFXt^)4Jjk9RIs*&$?-UPM*d7&m${m zm12kaN3mV1J|c6f$>V+{lvHp~XVW3DU0;cBR>7|)4bo{xa1-ts-lYU-Q-b)_fVVl`EP5X}+J9EzT20x8XIv=m7witdu7!3Lh=KE#OyKpT1GWk{YAo^ny|fvZt<+jmsFs=l*%e& zmRkBt5ccv4O7!HAyv2~rsq*(FmMTm?@TX3&1`nu|7C^F{ad%GLuoX}Rl}6`)uHF_xlx^gVca+mGH4T8u8;q{S*x3=j;kelz^atO~)v!Q_BT z4H6%IA}bvfuk0_vweELeEl8N5w-Q1GF!@f{VKnbyYB2?}d&QvI-j}~RI_+9t9$tC2 z94m=3eLi=sQb^S5;fqP?3aaXc&`}`lq z&M8dOXvxx9Y1^u_ZQHhO+qP}nwkvJhwoz$Mp6Qcq^7M#eWm}!3U@s07hop` zW24|J{t$aB`W>uBTssEvYMyi$hkaOqWh+^(RV_1MYnE0XPgW?7sBDk=Cqs(;$qrPEflqa0ZE?A3cBfW%0RPA235Wb6@=R_d>Sez; z`spwa50bq?-zh+id~Q!T`AYn`$GHzs;jxIw(A1_Ql&f|qP}|bon#H;sjKmSDM!nyn z>bU8l%3DB3F+$}|J^da!!pN|DO!Ndc2J)wMk!+Rr1hes#V}5o(?(yQSphn|9_aU<- zn|nsDS{^x&tweP;Ft`2ur>Koo2IdXJDsr6IN)7vB41Yy-^Wbo9*2th2QA@C zE0-0Gk12YOO?d_Guu6b3&(PIL`d zh4{`k54hu9o%v1K3PGuccez-wdC<&2fp)>`qIIaf)R{5un7-vwm=>LD7ibnJ$|KyE zzw`X*tM0S|V(I3vf454PY{yA5lbE+36_<1kd=&0Xy4jfvUKZ0$Jq!AG4KS7DrE9rph;dK^6*#CIU9qu7 z?)6O`TN&MCWGmUVd1@E2ow2`vZ1A#nGo8_n!dmX77DCgAP1va*ILU+!a&$zdm6Pa6 z4#|*&3dM+r_RJb%!0}7X!An&T4a4@ejqNJ;=1YVQ{J6|oURuj8MBZ8i7l=zz%S4-; zL}=M^wU43lZVwNJgN|#xIfo$aZfY#odZ6~z?aNn=oR1@zDb=a(o3w`IGu&j>6lYxL z&MtqINe4Z>bdsHNkVIu$Dbq0wc#X-xev221e~L zbm8kJ(Xzij$gF4Ij0(yuR?H1hShSy@{WXsHyKtAedk4O!IdpR{E32Oqp{1TD{usJi zGG@{3A$x%R*pp8b$RQo4w&eDhN`&b~iZ2m3U>@9p1o5kXoEVmHX7I6Uw4dn((mFw` zilWrqFd=F5sH$&*(eJB52zaLwRe zz`sruIc=Ck75>v5P5kd>B2u=drvGPg6s&k5^W!%CDxtRO)V6_Y_QP{%7B>E~vyMLG zhrfn8kijyK&bX+rZsnSJ26!j$1x+V!Pyn|ph%sXWr9^f&lf|C;+I^Fi_4;`-LJI&F zr;5O@#4jZX=Yaw0`pUyfF4J8A9wE#7_9!X|_s8~YUzWu&#E^%4NxUA3*jK-F5R3LP2|msHBLmiMIzVpPAEX)2 zLKYjm3VI4r#7|nP^}-}rL+Q4?LqlmBnbL+R8P%8VmV{`wP0=~2)LptW_i682*sUR# z+EifOk_cWVKg-iWr^Qf4cs^3&@BFRC6n0vu{HqZzNqW1{m)3K@gi$i}O(hT`f#bT- z8PqCdSj~FncPNmMKl9i9QPH1OMhvd42zLL~qWVup#nIJRg_?7KQ-g3jGTt5ywN;Qx zwmz4dddJYIOsC8VqC2R%NQ>zm=PJH70kS|EsEB>2Otmtf-18`jUGA6kMZL3vEASDN zNX%?0+=vgsUz!dxZ@~)eU17m4pN3xGC0T;#a@b9Iu0g_v*a3|ck^s_DVA^%yH-wt= zm1)7&q6&Rq#)nc9PQ6DKD{NU=&ul10rTiIe!)x^PS~=K(wX9|?k&{Mv&S$iL9@H7= zG0w~UxKXLF003zJ-H%fGA4Db9{~#p&Bl7ki^SWwv2sfoAlrLMvza)uh;7Aa_@FL4b z4G>`j5Mn9e5JrrN#R$wiB(!6@lU@49(tawM&oma6lB$-^!Pmmo;&j57CDmKi)yesg~P;lJPy9D(!;n;^1ql)$5uYf~f z&GywSWx=ABov_%8pCx=g-gww_u26?5st=rdeExu?5dvj^C?ZZxDv@Si^nX~2qA&K= z2jr;{=L(x~9GLXrIGXs>dehU^D}_NMCMegdtNVWyx)8xHT6Qu!R>?%@RvADs9er;NMkweUBFNrBm1F5e0_>^%CwM6ui}K_MpRqLS0*@lAcj zB6TTCBv>w2qh)qU3*kN+6tPmMQx|5Z0A4n67U-nss90Ec_rDF}r)IR4PE{$8;BSt= zT%6|jyD^(w6a*A5>_|TkMqx~e$n@8{`q?|)Q&Y4UWcI!yP-8AwBQ#P`%M&ib;}pli z9KAPU_9txQ3zOM#(x}*lN8q$2(Tq1yT4RN0!t~|&RdQMXfm!81d0ZuyD}aG3r4+g` z8Aevs3E_ssRAMR+&*Q30M!J5&o%^(3$ZJ=PLZ9<@x^0nb>dm17;8EQJE>hLgR(Wc% zn_LXw|5=b$6%X zS~ClDAZ?wdQrtKcV9>_v1_IXqy)?<@cGGq#!H`DNOE1hb4*P_@tGbMy6r@iCN=NiA zL1jLwuMw&N-e9H(v7>HGwqegSgD{GSzZ@sZ?g5Y`fuZ^X2hL=qeFO(;u|QZl1|HmW zYv+kq#fq_Kzr_LaezT zqIkG6R+ve#k6!xy*}@Kz@jcRaG9g|~j5fAYegGOE0k8+qtF?EgI99h*W}Cw z7TP&T0tz4QxiW!r zF4?|!WiNo=$ZCyrom-ep7y}(MVWOWxL+9?AlhX<>p||=VzvX`lUX(EdR^e5m%Rp_q zim6JL6{>S%OKoX(0FS>c1zY|;&!%i-sSE>ybYX3&^>zb`NPj7?N^ydh=s=0fpyyz% zraFILQ17_9<ettJJt~I+sl=&CPHwz zC9dEb#QFQcY?bk11Y=tEl{t+2IG`QFmYS>ECl;kv=N6&_xJLQt>}ZQiFSf+!D*4Ar zGJ~LFB7e_2AQaxg*h{$!eJ6=smO(d2ZNmwzcy3OG@)kNymCWS44|>fP^7QkJHkE9JmLryhcxFASKb4GYkJ|u^Fj=VdF0%6kgKllkt zC|_ov2R4cJ2QjjYjT6jE#J1J<xaNC>Xm;0SX<`LuW*}*{yQ3c9{Zl=<9NP z^2g5rAdO!-b4XfeBrXa4f{M0&VDrq+ps&2C8FYl@S59?edhp~7ee>GR$zQI4r8ONi zP^OA+8zrTAxOMx5ZBS03RS@J_V`3{QsOxznx6Yt*$IuEd3%R|Ki&zZkjNvrxlPD$m z%K+rwM!`E&Z46ogXCu!3 z8use`FJJ?g_xi?~?MxZYXEu=F=XTC8P3{W*CbG3Wk)^31nD~W>*cJ@W4xg%Qqo7rq z`pUu8wL!6Cm~@niI*YmQ+NbldAlQRh?L!)upVZ)|1{2;0gh38FD&8h#V{7tR&&J}I zX1?;dBqK}5XVyv;l(%?@IVMYj3lL4r)Wx9$<99}{B92UthUfHW3DvGth^Q0-=kcJ1 z!*I9xYAc$5N$~rXV>_VzPVv`6CeX(A_j3*ZkeB~lor#8O-k+0OOYzTkri@PVRRpOP zmBV|NKlJT?y4Q82er)@lK&P%CeLbRw8f+ZC9R)twg5ayJ-Va!hbpPlhs?>297lC8 zvD*WtsmSS{t{}hMPS;JjNf)`_WzqoEt~Pd0T;+_0g*?p=dEQ0#Aemzg_czxPUspzI z^H5oelpi$Z{#zG$emQJ#$q#|K%a0_x5`|;7XGMuQ7lQB9zsnh6b75B9@>ZatHR_6c z0(k}`kfHic{V|@;ghTu>UOZ_jFClp>UT#piDniL(5ZNYXWeW0VRfBerxamg4su5<; z(}Ct2AhR@I-ro0}DdZLRtgI@dm+V`cRZjgV-H+aXm5|Mgz`aZX63i<|oHk-E)cABn z0$NR?(>fla7)Ong28FZSi9Yk0LtYl5lZw5wT!K5=fYT$avgkMKJWx~V#i@7~6_{dM zxDDPIW2l{O2Elv#i^cjYg~lGHRj(W*9gD`(FILKY$R`tL2qo&rtU*c;li!V`O$aV{ z!m|n!FAB2>MR_FVN*Ktv5+2dW4rr3YmfEheyD+48%USM#q6)w%#2}~=5yZE1LLcth zF%VtefH&#AcMx7)JNC$P>~OFuG6sK}F7V$D7m!{ixz&inpAVpFXiu^QruAw@Sc7Y2 z_A^V(2W_+KTGRp2aQSMAgyV#b3@{?5q@hPEP6oF3^}|@8GuD6iKbX;!LI!L=P#Za zL$Zuv#=x3fseRMZ()#SQcXv->xW`C|6quwqL1M&KByBj z2V`}(uL4JB-hUs6304@%QL~S6VF^6ZI=e-Nm9Tc^7gWLd*HM-^S&0d1NuObw-Y3e> zqSXR3>u^~aDQx>tHzn9x?XRk}+__h_LvS~3Fa`#+m*MB9qG(g(GY-^;wO|i#x^?CR zVsOitW{)5m7YV{kb&Z!eXmI}pxP_^kI{}#_ zgjaG)(y7RO*u`io)9E{kXo@kDHrbP;mO`v2Hei32u~HxyuS)acL!R(MUiOKsKCRtv z#H4&dEtrDz|MLy<&(dV!`Pr-J2RVuX1OUME@1%*GzLOchqoc94!9QF$QnrTrRzl`K zYz}h+XD4&p|5Pg33fh+ch;6#w*H5`@6xA;;S5)H>i$}ii2d*l_1qHxY`L3g=t? z!-H0J5>kDt$4DQ{@V3$htxCI;N+$d^K^ad8q~&)NCV6wa5(D${P!Y2w(XF!8d0GpJ zRa=xLRQ;=8`J2+A334};LOIhU`HQ*0v4Upn?w|sciL|{AJSrG_(%-(W9EZb%>EAGG zpDY?z1rQLps`nbCtzqJ#@wxU4}(j!ZQ{`g`g*SXlLah*W9 zyuh)UWoRCknQtd~Lk#BT_qjwj&Kw8U)w=owaJ;A5ae}3)y>{neYNS`|VHJdcSEBF# zBJ6a;T)u;^i#L~LVF-X7!E$SggILXMlsEy~v}K*DM2)f@U~g|Q6I-Pss@)`>fgFWx zsq&7pe!|VA-h;@=fBF{(mR1^{1>ukTYUdyF^#A+(|I_&nm{_xaKn3h4&yMyym2k-wMFg(s@ez=DPmuB%`| z6;e@HQKB(|!PU1sW)W6~x|=8m6rL~4dQ9LTk|RzL-_(_77B4I~ZG=q7K%qHiv!FD8 zmt;Vnhb{ymaydv2V;X-5p zTt2ln?kaB9&(dH_X70^@rrCfz)nwfa9LYTHXO(IPcTEf$QiEhTpl??L+`Eetyqof8 zzl=q)?KdYni!C_9b8Z3xm7r5<5ZG-0uA`u^7Dm7k4mAsQ(rkoWy*^DZJa~#y6+hNG zh?7{D9$a9LS`a@SvZ5?C{JUHovWU9KI}z8YV4pWftx21v*Q;MpU{+b@>Or(}pwO^fu0qA3_k_Bo2}lIxvmMhucG-o>O=+R6YxZ zjs!o%K1AA*q#&bs@~%YA@C;}?!7yIml1`%lT3Cvq4)%A)U0o1)7HM;mm4-ZZK2`Lj zLo?!Kq1G1y1lk>$U~_tOW=%XFoyIui^Cdk511&V}x#n4JeB7>bpQkYIkpGQRHxH$L z%tS=WHC~upIXSem>=TTv?BLsQ37AO88(X+L1bI<;Bt>eY!}wjYoBn#2RGEP49&ZH-Z_}R_JK_ z>o*_y!pOI6?Vf*{x-XT;^(_0}2twfk`*)_lLl0H-g|}BC?dm7CU|^-gNJ~rx z($>97WTKf71$?2|V$Ybpf~Aj@ZZOcb3#uRq51%4^ts-#RMrJhgm|K3QpCsPGW=2dZ zAr5-HYX!D*o#Q&2;jL%X?0{}yH}j*(JC4ck;u%=a_D6CrXyBIM&O#7QWgc?@7MCsY zfH6&xgQmG$U6Miu$iF(*6d8Mq3Z+en_Fi`6VFF=i6L8+;Hr6J zmT=k0A2T{9Ghh9@)|G5R-<3A|qe_a#ipsFs6Yd!}Lcdl8k)I22-)F^4O&GP&1ljl~ z!REpRoer@}YTSWM&mueNci|^H?GbJcfC_Y@?Y+e4Yw?Qoy@VLy_8u2d#0W~C6j(pe zyO6SqpGhB-;)%3lwMGseMkWH0EgErnd9a_pLaxbWJug8$meJoY@o-5kNv&A$MJZ=U z^fXPLqV6m3#x%4V*OYD zUPS&WHikdN<{#Yj|EFQ`UojD4`Zh*CZO4Cv`w^&*FfqBi`iXsWg%%a< zk@*c%j1+xib(4q^nHHO^y5d8iNkvczbqZ5;^ZVu%*PJ!O?X-CoNP*&tOU!5%bwUEw zQN?P*a=KKlu{`7GoA}DE=#nDibRgecw>-*da~7&wgow}|DyCJq!-Lp8a~(zR@tO1 zgu(4s4HptPGn(HmN2ayYs@g+yx1n`nU3KM{tQHhMHBw7f#gwru$=C()`aKZAl^dYc ze7fC)8EZEXOryk6AD&-4L+4cJ&M@3;;{R)mi4=`ti7IZByr^|_HNsjcNFu?mIE)jD za2j)FPwRY!R_YR-P?URm0Pti*e#5jmfK)6EvaKCT{h)kbJl{AGr1Ekt}pG?^e z*botRf-RsB8q10BTroj{ZP**)2zkXTF+{9<4@$aNDreO7%tttKkR3z`3ljd?heAJEe<0%4zYK?};Ur*!a>PbGYFFi(OF-%wyzbKeBdbkjv^i9mn@UocSS z4;J%-Q$l`zb&r*Pb`U;3@qkc=8QaPE9KwmlVwAf01sa*uI2*N`9U^3*1lLsM9dJ(4 zZBkU}os|5YT#Z;PD8xVv!yo$-n{-n4JM5ukjnTciniiT`(cZ6sD6~67e5_?8am%!w zeCLUxq~7x-!Xg#PgKV&caC@7mu<86am{WaXo(lAemt4~I$utSp(URWpYNo$RvU*$N z#%iiA+h`(E;BUg;=I!#EaxO89bUK3*v5Nc3GPmURC5TqzC|))DsFNtJICH6oBW6#q z+B(N{ey+^mk_{!@ z)VhAWXG=_0j|0f9iJ;c404PiIFqK)(AD05Xh`Fk`r$^b`v+>*g+_+h@r)e+ELJ45) z?20~u<}HQyQ5AsBz(teF9!!_GLXnm{5Z0e{Ki*@!=&3x4-RcjBn##DDzHJ|KSZ5(E z9=tFZ)p~-}x%9sCY27)2i>(E-^OiYT?_)a;yXAGR$y+E`myMd;xDA#_Q49t*E}&ql#H~|x z2J2R1_#2lt91NnF!uqW%_=HlbF?A{B{n>}9$g5QF!bh_a7LTU~Jyz}7>W5{_LAov{ zy2_dmGy)d)&7^bJyUjEw%3xj{cuG0Eo zwL*XQB*Oi=r&HIIecC1%lbE;Y-*5|cL955S+2@uR18JDL<0;;Uc2Q9JEyo1R!!sz_ z#BqnkGfbLP#oQJk3y}nwMd(3Tt^PVA#zXnYF7D0W1)#+`i?@cm}fBkKD z+Mpcuim53|v7;8Tv(KraEyOK`HvJq^;rlNzOjIbW&HJDFqW>doN&j7)`RDv#v|PQ+ z03WnB4Y4X@Fe-@%3;He*FjY1MFmkyv0>64Cp~FIDKQTwmFP~_CxZOf{8gPy}I<=JC zo%_bmue&$UU0|GG%%99eI!m#5Y1MD3AsJqG#gt3u{%sj5&tQ&xZpP%fcKdYPtr<3$ zAeqgZ=vdjA;Xi##r%!J+yhK)TDP3%C7Y#J|&N^))dRk&qJSU*b;1W%t1;j#2{l~#{ zo8QYEny2AY>N{z4S6|uBzYp>7nP_tqX#!DfgQfeY6CO7ZRJ10&$5Rc+BEPb{ns!Bi z`y;v{>LQheel`}&OniUiNtQv@;EQP5iR&MitbPCYvoZgL76Tqu#lruAI`#g9F#j!= z^FLRVg0?m$=BCaL`u{ZnNKV>N`O$SuDvY`AoyfIzL9~ zo|bs1ADoXMr{tRGL% zA#cLu%kuMrYQXJq8(&qS|UYUxdCla(;SJLYIdQp)1luCxniVg~duy zUTPo9%ev2~W}Vbm-*=!DKv$%TktO$2rF~7-W-{ODp{sL%yQY_tcupR@HlA0f#^1l8 zbi>MV~o zz)zl1a?sGv)E}kP$4v3CQgTjpSJo?s>_$e>s2i+M^D5EfrwjFAo(8E%(^ROV0vz0o z-cg0jIk24n!wxZainfH)+?MGu@kg$XgaMY-^H}z^vG~XC7z2;p2Kv`b^3S#b5ssMOJ7724v>S36dD zeypxJ<=E~sD4f5wX060RIF-AR0#{Z z=&y$r8A-e6q18lIF{@O9Mi%dYSYT6erw!@zrl=uj>o(3=M*Bg4E$#bLhNUPO+Mn}>+IVN-`>5gM7tT7jre|&*_t;Tpk%PJL z%$qScr*q7OJ6?p&;VjEZ&*A;wHv2GdJ+fE;d(Qj#pmf2WL5#s^ZrXYC8x7)>5vq_7 zMCL}T{jNMA5`}6P5#PaMJDB2~TVt;!yEP)WEDAoi9PUt89S2Cj?+E0V(=_sv4Vn6b z_kS6~X!G;PKK>vZF@gWpg8Zuh%YX^2UYPdCg7?EH#^gkdOWpy(%RnXyyrhmJT~UJw zAR;%Zgb6z(mS+o9MT|Sc6O({!i0pzk;s9?Dq)%tTW3*XdM3zhPn*`z45$Bg!P4xfy zD*{>30*JsSk?bQ-DgG62v>Vw-w`SA}{*Za7%N(d-mr@~xq5&OvPa*F2Q3Mqzzf%Oe z4N$`+<=;f5_$9nBd=PhPRU>9_2N8M`tT<-fcvc&!qkoAo4J{e3&;6(YoF8Wd&A+>; z|MSKXb~83~{=byCWHm57tRs{!AI<5papN(zKssb_p_WT@0kL0T0Z5#KLbz%zfk?f7 zR!vXBs36XaNcq5usS7<>skM_*P$e*^8y1ksiuokbsGFQ_{-8BAMfu!Z6G=88;>Fxt z|F-RU{=9i6obkTa0k~L#g;9ot8GCSxjAsyeN~1;^E=o5`m%u7dO1C*nn1gklHCBUw z;R(LgZ}sHld`c%&=S+Vx%;_I1*36P`WYx%&AboA1W@P;BvuFW+ng*wh?^aH4-b7So zG?9kFs_6ma85@wo!Z`L)B#zQAZz{Mc7S%d<*_4cKYaKRSY`#<{w?}4*Z>f2gvK`P1 zfT~v?LkvzaxnV|3^^P5UZa1I@u*4>TdXADYkent$d1q;jzE~%v?@rFYC~jB;IM5n_U0;r>5Xmdu{;2%zCwa&n>vnRC^&+dUZKy zt=@Lfsb$dsMP}Bn;3sb+u76jBKX(|0P-^P!&CUJ!;M?R?z7)$0DXkMG*ccBLj+xI) zYP=jIl88MY5Jyf@wKN--x@We~_^#kM2#Xg$0yD+2Tu^MZ1w%AIpCToT-qQbctHpc_ z>Z97ECB%ak;R<4hEt6bVqgYm(!~^Yx9?6_FUDqQQVk=HETyWpi!O^`EZ_5AoSv@VbUzsqusIZ;yX!4CsMiznO}S{4e>^0`c<)c~mC#*{90@+T@%EQ~>bovc8n_$bvqkOU7CrYe8uI5~{3O7EijeX`js z-$LNz4pJA7_V5~JA_Wl*uSrQYSh9Wm($%@jowv^fSPW<~kK&M*hAleywHd?7v{`;Y zBhL2+-O+7QK_)7XOJAbdTV-S`!I)t~GE8z+fV7y;wp#!wj75drv;R*UdSh(}u$%{VSd0gLeFp;h6FkiVz%g=EY3G#>RU;alRy;vQmk*| z@x-ba0XKE%IyL4OYw6IXzMiS(q^UDk=t(#XgkuF`{P?=k8k3r)rmhkv`vg@kiWd34 z-~t+1aV3SabTbG=nQYs>3~E<}{5@0g**LAWi*~SfRZhGcgP{e5T!0M7CU}`f@r8xI z0bx%sI!?5);-wG+Mx&S=NRfIi>V-wP(n&$X0Bhd)qI^ch%96s6&u7qpiK8ijA=X_R zk&|9f$GXf-;VgnrxV83Cp-Q!!sHH`5O^o~qZu!xny1t?(Au(EAn)D??v<1Uo;#m7-M@ovk|()C(`o>QMTp}F?> zakm3bHBKUjH-MHXDow7#Z|@wea1X9ePH;%YA)fCZ9-MD)p^(p!2E`aU9nmJlm;CXQ zkx~$WQ`Yq{1h5k>E>Ex{Z=P=)N*0b8_O({IeKg?vqQ)hk=JHe z5iqUKm!~mLP0fnRwkCO(xxTV@&p+o8wdSP$jZofYP}yEkvSc z5yD-^>04{zTP7X44q9Af&-wgt7k|XtncO&L@y-wFFR44RsPu57FRvIBaI^Pqy_*DV z@i13CsaR5@X@xH=NT3}T`_vsy!a02n80eQqya=-p7#YW`Jc0z!QglGg`1zeg6uXwI zsB~hlNMo)kFL(V3Q1<%8yoI6X7ncn-&&Uh3rL@S(6@wKAXt6Wr=a2ObI7}8$D-FoI z>AJA>WsBEMi5ba6JhJ%9EAi&ocd(ZsD|MsXwu@X;2h#|(bSWu@2{+c7soC`%uo{sMYq&Vyufb)?OI59ds)O+kyE8@G z@tlpNr0UO~}qd0HQve6njJ zda2+l$gdX7AvvGhxM6OToCuQ|Zw|9!g1)O+7>~{KNvASjp9#Cqce-or+y5xdzWL3gLWt2oa+T(I+{j(&bF1laUsJB{fOgE-B}qslaS>C z)TjzG8XecbS%a+?yT!0QmTex?E478;D|sL*oS4C-g0Tq(YoH|eyxJ#1j088C|U-w5id`%Sz7X_w#l+U9+)$|2no<}5J zRb_9@0esSr?n}HvVGbD5@$p$8k4?qOe-GNOk3-K^Mw>Xg+drCKi5@$GTeijpI;;IG ziD<&go`ptLC&^<0jw^l0aY?_pUUK+xp#0Bk66iQ29vpR)VBE{JOJ&OL^gKsN<&t<| zCMLTYMSDG5Ie9O>6Dl#T{@cscz%)}?tC#?rj>iwQ0!YUk~R z$rB-k=fa9x&631Z9Mfqj_GRoS1MzqSMEdaZ2!isP19Sr>qG8!yL(WWF)_&{F)r>KnJGSciSp!P0fqHr+G=fGO02Q#9gHK zpwz+yhpC4w*<9JO@#(MdkZcWbdCO5B!H`Z|nV?UtcBo96$BgX+7VYMwp@b-%;BrJu zMd*K!{1txv{kHKPDs9?WZrz_^o1Tq2P=+=|E=Oy4#WE{>9}*9(apqhmE`&AeBzQgQ zELFLCmb~q|6y0FCt|B}*uI*ayZ#6=$BpGtF{Jfye#Q>FZ?BPnk)*Qmd?rNG^tvFUU z_b&antYsZnUR6Q9tQUy81r$&ovT#fy;(Db4F&M*C=KxQgHDrRcVR#d+ z0(D|*9#u`w_%2o3faI{?dNd9$#5nj1PROHNq z7HJ(;7B1ThyM>a@Fo^lJb2ls2lD`}ocREH|5pKN;$>gFyM6k)kZG;lA;@kSJIqUhf zX%dhcN(Jtomz4(rNng&1br3Xx33EvCWz%o8s;SpRiKEUFd+KJ+u|gn|J85dZ)Exc&=V|Ns8Xs#P>qv6PX&VAJXJ(ILZO!WJd0 z`+|f5HrEj~isRN7?dBHotcPI7;6W48*%J(9 zftl1Tr`bKH*WNdFx+h;BZ+`p!qKl~|Zt5izh}#pU9FQKE97#$@*pf38Hr8A+`N+50U3$6h%^!4fBN zjh^cl#8qW5OZbvxCfYzKHuyeKLF4z^@~+oqlz9(Hx8vypIiUlt!(vs}_t#4@nh$s; z>FYERg*KD#Xs+W4q-V-IBQK!)M1)Aa+h+V+is)z!_=gEn&^ci7<DEEmYcoSh?WdXUsP7O4)&lQXA(BVM5jI8s6;mO}94AC0gG(`>|T)yuV1l~i-ejCCt zoejDhX0nrZDP|x9u4zp%S2UeDzV`o#pBGu1tZ-$<9TIbN=ALwhQ0=9S{8#}Uu8n-~ z5~xIvUhLSz@c@0|me$CdZCpZl(vQw@a0Y4^{T0w_>pOkwI^x4KkBf3qGmm)nG|Ps5 z_XTY~^b^mL&_*yjl~RRIi&eS(>y?y}O4-)nWyTEPpQAb#Xz8SnnfIL+nAcNL9nqV9 zRL|eyF)RKI5-kJO6}>Q89XmgY@b1&!JI>g3ryZ@jN2v3vm7O`AL!BTWNouJzV+$+Y zYY}u%i>K6=IYU2O$2TAyVjGt?wgF9xCj;?EK(8fWu!!~48`3u^W$eUlCh*91PLxu1 zRY(F7Q3s7h$Q-p&L$ucN}it*-9KR z_<wHu?!dav0$P+PI3{J8?{+l|n&2YMLV2 z+hRta$A5WpCXl1RNbYBsX8IGX{2v>U|8_I-JD56K|GexW>}F_e_g_1r?08v8Kz{V$ zT=6aGMk>ibvRO@Yrc@ezaD0%ydHkXGHrR{7>q~~tO7ChJflwa4-xL|@#YIJejC5VT zInU4CjQ9V0+lClQY=vh^s4MadwQmk7li{54Y;Ht}gkZOIh9(vfK?3kXLoD72!lHD# zwI-Jg|IhT=Y#s|tso1PWp;|aJ2}M?Y{ETyYG<86woO_b+WVRh<9eJu#i5jxKu(s~3 z4mz+@3=aNl^xt{E2_xewFIsHJfCzEkqQ0<7e|{vT>{;WlICA|DW4c@^A*osWudRAP zJut4A^wh@}XW4*&iFq|rOUqg*x%1F+hu3U6Am;CLXMF&({;q0uEWG2w2lZtg)prt` z=5@!oRH~lpncz1yO4+)?>NkO4NEgP4U~VPmfw~CEWo`!#AeTySp3qOE#{oUW>FwHkZ3rBaFeISHfiVSB7%}M) z=10EZ1Ec&l;4 zG98m5sU!pVqojGEFh8P{2|!ReQ&hfDEH2dmTVkrS;$dN~G2v-qnxn^A2VeHqY@;P} zudZD5vHtVvB*loIDF1M7AEEvS&h0;X`u}!1vj6S-NmdbeL=r{*T2J6^VA7F`S`CDd zY|=AA6|9Tu8>ND6fQhfK4;L3vAdJPBA}d6YOyKP&ZVi%z6{lbkE|VyB*p1_julR^k zqBwjkqmFK=u&e8MfArjW-(Ei8{rWso1vt5NhUdN|zpXqK{ylJ8@}wq-nV~L4bIjtt zt$&(1FTIs+aw}{&0SO4*sa0H2h&7g}VN5uYjfed5h7eGp$2Wu*@m9WIr0kxOc}fX9eOWh zFKfV>+SD$@kESKYm{F*J90XQjr$!<~v(J%&RMuQM+6CkmnYZDGlOUdq}%)VA& zl#acS%XE2KuX~7IamK`og@C`21~*cEEc#PZM6HT*Veb_l&Ej~j0zL7p0Eo`mMu(=X zJ$v;&Lya75I4C^saKROgfi(fdP0C$GM3WyZn%mm3yEI>|S&O(u{{S<}ihUp#`X&_z zmQBma;82#`C;dR5Sx09e07FvtJLhZ{9R~|$FCdU6TDNUwTc9kNct?8e@o2MpQDrkg zN?G+aYtTjiUPA=RX5o{4RYu}6;)ET>TcgL^VpfIpluJ|lQR(_)>6k%L^FZmoK-Wm- zR5qy0P)hm8yvqOL>>Z;k4U}!s?%1~7v7K~m+gh=0c9Ip_9UC3nwr$%^I>yU6`;2kV z-uJ%y-afzA7;BC7jc-=XnpHK+Kf*tcOS>f5ab2&J&5hIOfXzs=&cz|Qmrpu6Z);`R z0%3^dioK5x?o7t~SK7u5m{dyUZ#QUPqBHYn@jETeG>VU=ieZuJ;mm^j>dZM7))cw?a`w8R z%3M0R=kdOt^W^$Kq5Z%aJ(a$(*qFpy^W}Ij$h+Jnmc9eaP(vB@{@8t zz=RQ$x4XYC#enS$fxh@;cSZ|D%7ug;0z{C8I8h{KocN-cyv3UG_nk99UNS4ki^OFkYea`q`rs zG@qdMI;4ogcd5Tr`di1JBg4I*6CFvCID_2SN5&)DZG&wXW{|c+BdQ4)G9_{YGA@A* zaf}o^hQFJCFtzt&*ua~%3NylCjLtqWTfmA-@zw;@*?d&RE3O8G&d;AVC|rZrU}jx# zC-9SF`9;CbQ(?07o8Q9E12vi)EP@tOIYKEKnO@-o!ggkC)^#L-c40iZtb4Y-cS>$I zTn~+>rn*Ts>*y*z^b3-fAlne+M-*%ecrI^rmKAVv23cB`aWD?JDJ5NIafRvRr*~~C z)99Afs`BPK!5BFT)b_^8GyH*{22}yDq;be`GnPl=vW+ITnaqzl(uYOHhXi}S!P+QZ z4SwfEPuu&z4t#?6Zaw}bvN{;|80DfxCTuOdz-}iY%AO}SBj1nx1(*F%3A-zdxU0aj z`zzw9-l?C(2H7rtBA*_)*rea>G?SnBgv#L)17oe57KFyDgzE36&tlDunHKKW$?}ta ztJc>6h<^^#x1@iTYrc}__pe0yf1OnQmoTjWaCG`#Cbdb?g5kXaXd-7;tfx?>Y-gI| zt7_K}yT5WM-2?bD-}ym*?~sZ{FgkQ9tXFSF zls=QGy?fZ=+(@M>P3Y>@O{f44yU^fP>zNzIQ0(&O$JCd_!p?2;} zI6E1j@`DxzgJvqcE@zgapQ?tophO14`=14DUZ*#@%rRi``pi0lkNgidSsHGjXK8gO{drQoNqR&tRjM4>^DtW`)fiRFO4LE=Z+nCBS~|B3gZsh`Y?-$g z@8@Z$D7C!L9l=SWoE;(+*YirPLWvBd$5Ztn3J3EaGM+#pW#@{3%yksGqy(2Bt5PVE zf*fICtPp77%}5j#0G8<=v=)LR>-a3dxja8cy3m$=MZ2#$8mbLvxE%NptMd+L?mG`v zF1cANFv17DqP^P5)AYHDQWHk*s~HFq6OaJ3h#BUqUOMkh)~!(ptZ2WP!_$TBV}!@>Ta#eQS_{ffgpfiRbyw1f)X4S z_iU`lNuTy86;%!sF3yh?$5zjW4F?6E9Ts-TnA zDyx5p1h$Z3IsHv7b*Q{5(bkPc{f`2Wfxg*Z#IvQ;W_q9|GqXGj<@abo)FyPtzI~i25&o zC!cJR%0!}lLf^L2eAfZg7Z69wp{J?D6UhXr%vvAn?%)7Ngct4Hrs@LZqD9qFHYAWy z4l=2LI?ER&$He2n`RiG&nsfLv?8$Cl)&d8a-~-N`I|&EPa@Y=v@>0Gl?jlt>AUY;H z`**5bpS#VGhdp4pKbf3iEF*>-eXg_$bqt5Dc%q0+)R50>zd^l7sN5R5Z)Ut+oz-8_ zJ`Z9HE9(=wRTD)T=%GZTEi9K5naPzlfE$|3GYGLRCLsnqLi8Sc6y&iskqA&Z$#7Ng z7Q@C0)6k;J$TlQ+VKZ5)-Ff_BNoIMm+~!@Cv1yAUI-U!R)LHc@+nSUzo$GlRb+8W< zYPG%NFfr;!(RlnvBbN~~EpT6Xj5*^Z&73tdIQ$LZu`vkfzdTKa5|JJtQ_rm4g$9LO zKtgYVdW=b<2WGM3I_j|Rd8gZ3j;)S#AT(aP^d>9wrtQS_+K>pZDX^?mN!Z>f^jP@1 zlJ;i79_MgOAJa`%S9EdVn>ip{d!k6c5%zizdIoB9Nr!n`*X#%6xP1?vHKc6*6+vKx zmEt|f^02)S_u_wlW_<`7uLQU%{wdH0iojOf_=}2=(krE<*!~kn%==#0Zz`?8v@4gP zPB=-O-W=OO3tD19%eX>PZj3YfrCt0sEjgTd#b$buAgBri#)wW14x7QcHf2Cneuizz z368r7`zpf`YltXY9|2V{stf8VCHgKXVGjv$m!hdDf0gi`(Q!(Pyg~FO28Vr#!BYP| zI)qG2?Ho=1Us9dTml}-ZOR?g5Vk)f+r=dbCN*N1=qNfG>UCLeA8pd3Ub-pRx1b3FA zEn`CIMf`2Mt3>>#3RkE19o}aMzi^C`+Z>8iIPHSdTdmjCdJBtNmd9o0^LrJc9|U9c zD~=FUnSyghk7jScMWT|SHkP(&DK$Z=n&lGm+FDTpGxfoIyKV)H6^nY~INQ#=OtIT! zyB*J=(#oHf=S)MNOncW->!c0r0H#=2QzobO&f@x&Y8sYi-)Ld;83zO$9@nPPhD}yt z{P`*fT@Z(?YAmF{1)C;o?G@dfd2$c+=Av*|;P@Yz1KnclB-Z-fJQ-=+T*g>0B7!g# zQH{dHt_%wj=wlmT&m59)TQ~xK)gB6f^EY$=1zcbGf~Q>p_PzDCHR6lndGmqPY2)&w z$Th^K%1v@KeY-5DpLr4zeJcHqB`HqX0A$e)AIm(Y(hNQk5uqovcuch0v=`DU5YC3y z-5i&?5@i$icVgS3@YrU<+aBw+WUaTr5Ya9$)S>!<@Q?5PsQIz560=q4wGE3Ycs*vK z8@ys>cpbG8Ff74#oVzfy)S@LK27V5-0h|;_~=j1TTZ9_1LrbBUHb?)F4fc)&F7hX1v160!vJc!aRI>vp*bYK=CB(Qbtw7 zDr2O^J%%#zHa7M5hGBh#8(2IBAk}zdhAk$`=QYe^0P6Bb+j5X)Grmi$ z6YH?*kx9hX>KCI04iaM_wzSVD+%EWS)@DR&nWsSBc2VIZ>C(jX((ZiV0=cp}rtTO&|GMvbmE4FpBF5Rd z6ZG=>X&>N3?ZN2^11pXEP4L?XUo`qrwxgQm4X~RCttXmZAhnhu4KDK=VkKq?@@Q_Z za`*xyHrsAEsR zV(7)2+|h)%EHHLD3>Qg{>G|ns_%5g5aSzA#z91R zMDKNuIt@|t?PkPsjCxUy&fu^At*yUYdBV!R_KOyVb?DO&z$GLJh9~b|3ELsysL7U6 zp24`RH+;%C(!bWHtX&*bF!l-jEXsR_|K~XL+9c+$`<11IzZ4>se?JZh1Ds60y#7sW zoh+O!Tuqd}w)1VxzL>W?;A=$xf1Os={m;|NbvBxm+JC@H^Fj$J=?t2XqL|2KWl$3+ zz$K+#_-KW(t)MEg6zBSF8XqU$IUhHj+&VwsZqd7) ztjz$#CZrccfmFdi_1$#&wl~A*RisBaBy~)w|txu1QrvR1?)2mb&m2N$C(5MS%hSX)VJnb@ZGXB5^%(<#1L@ zL^>fBd+dEe`&hxXM<0A9tviIs^BDkByJdc~mtTYr!%F7Q1XnK2$%h$Ob30*hSP$Bt zDd#w{2Z%x^Wpv8!)hm>6u01mY!xmPgwZ#Q0148)SxJc3Udt!-&}eRO^LN ze26pQB!Jhg&Z>#FD>`C`sU44><=v>O>tJdLs!HPpV#AM32^J@Za-9J(CQjKxpzXao zQfRkWP%g9P8XV21MmoHfx{DICLSc*t4qVeQL9t}&Pz0rM}YTba@XsD=XMW@FxFM{QYQJHvM(JsUSa3mcTUl9^qcVA zBveO--fqw%{#QGR1vy;x88+qMcgzmcYc#8U`CPPt6bl?uj%w_`b~9JliftnOa|ziW z|6(q&STs_*0{KNa(Z79@{`X&JY1^+;Xa69b|Dd7D&H!hVf6&hh4NZ5v0pt&DEsMpo zMr0ak4U%PP5+e(ja@sKj)2IONU+B`cVR&53WbXAm5=K>~>@0Qh7kK*=iU^KaC~-ir zYFQA7@!SSrZyYEp95i%GCj*1WgtDId*icG=rKu~O#ZtEB2^+&4+s_Tv1;2OIjh~pG zcfHczxNp>;OeocnVoL-HyKU!i!v0vWF_jJs&O1zm%4%40S7_FVNX1;R4h^c1u9V@f z`YzP6l>w>%a#*jk(Y82xQ@`@L(*zD&H>NY`iH(iyEU5R$qwTKC5jm4>BikQGHp^)u z-RQ`UCa70hJaYQeA=HtU1;fyxkcB2oY&q&->r-G9pis)t$`508$?eDDueFdW=n5hJ z08lH$dKN$y#OEE@k{#|<%GYY=_c~fHfC@pD54KSP9{Ek@T47ez$;m$}iwR}3?)hbkwS$@p2iVH0IM$lB*XYA+#}-re|UNzCE)SOYwy z=Y!fkG4&I%3J(_H#UsV#SjHulRIVcpJ`utDTY{k&6?#fzt~@Om=L(vs6cxAJxkIWI z@H7)f2h%9!jl@C!lm+X4uu;TT6o0pd7 zteFQ(ND@djf#o2kTkjcgT=dHs7ukmP0&l8{f;o3JuHGd2Op*?p7?Ct=jA*tIg{MZk z$2Lsc0e8Tdcwrjx|_Ok?9uB3Il|^2FF%X#ck}WoIvrzQXN%kT$9NI{79Wm~gZ3`8I+O`)`n30feZ( zDO-fl6IG3c^8S;Y_M-)+^CmM0tT^g0?H#>H8!oC8W%oU!~3|DJ?)~LT9*&GAQG13zOGq6gs*={cu|(V7{R$y@{-iV*9q@AD(#Ktb}J&3&k|5Djs$)9WM7!6#EaJ_ilvbfUvyh8c?-{n zfuFrC0u6}UJZ7aj@(cNG_(CKgjQQTA-UK@-MVmick zot}6F%@jhq(*}!rVFp5d6?dg|G}M*moyLriI!PQDI;E1L1eOa6>F9E6&mdLD>^0jJ z09l?1PptuV65gm=)VYiv<5?*<+MH~*G|$~9Z3XEy@B1-M(}o&*Fr9Sv6NYAP#`h{p zbwbUE3xeJ;vD}QMqECN)!yvDHRwb7c1s6IRmW!094`?Fm!l~45w)0X`Hg+6Y0-xf# zSMemBdE)Q=e^58HR{kWrL5-H0X6pDu%o{0=#!KxGp0A;6{N5kI+EoY_eTE%2q|rwm zekNeLY-R?htk!YP2|@dbd8TWG4#G)=bXlE{^ZTb^Q$}Er zz)Fp)ul24tBtQFIegdI37`K$VR3tVdi<(fIsu{#QMx=$&CK9M8oN%3Mk;>ZPd-;Q- zn|sSKSnc-S0yrw#TlA$+p{J~u=u98s>IoL@cNLOxH=+1m?;t1bR$vR=M$US&Z8DO3 z_&zhQuId1$wVNsS=X?&s(ecIi#00o{kuPs6kpYkL$jMyGW8U7mlCVaZeEL=HsIxqm zFRLxWin8B>!Dc#9Z#t0RNQiR-@5J+=;tC7|1D*~rxcwHa5iIVD@99cCFE@BukUC-S z^iJdt?dwU)kH2VY9?|zVShMbZctzFRz5Q4tiXa^>@U%jDYq}$rSyc#p2wXr}mc0qq z^lT>$y)N(Qg0dwmEwTopneoU(y)>Mj+f{iHM0o|>ZtCg-itPj4addYz??aE)Rp&hk z_SI)%XeSf=SjZq18h!Cc>Xy&EynnxdHQ){(x@g|ZA%`3LU^KzX02c5N;F#tEk1)7v z(|V9tO3>?^X|kQ*rRBf4>mWW2$-Lx})|M7z125&VHcxsCqB!<$l1F$zCrJ+nm0f3Z z%Hq^=SKpHyV2@Y*Cu2x>fXC0SscnR*($zEB{KOniJcpn@e`PMH*_Q6*0Z^8RNCEvZ z+UU9!927p9YZ&g=bnUvQUZcdisyn;-4;ACXOe-Xor9K8Qbp{ldE17+G@VQT+9ZJQ*9dZoXfU2ue|mMhrrZk2R7&~YjFW4`BTq45UwVc6JORKU)wBCTanITh0GD}s$`C5pb(9{b9 znwee6j%?-UV)_7opOioCf5@C?@w^@g& z&68+oMmV;5JW@TT63&CSDrfYL2$L)pVseDtAwPwleEM3F^-Ufn3PpfxFmx6o zQ`Wq9x#d$e`VKn5LOXNsrqhGao7~|s(u~drPrZ+;aP!C%z4NskZstCbAibD}O%8Ij zb~C(taxco~WzJLxhL1T}3ctXMbV6}_z=IZN9L0|SxLSe`$X`<)BhM`$1&&)e_}fCh z=idVL<+u6Vn{&ksP*ZLlMo$fC`dtzF_?~L?4Rril2G4%v5^7sUa^&8aMtMX&mtapl zD(dW|cisM3fqMaB`8?QbkyiUl2g>hMB5EoS&IB8TdoC~)b$nT=`%GgU`k-)+8}`)F*~I~DXMaTP%kZftx11~?iALs5J+&Rom#p%Y z>dH}-euH4u=_V3hc6^*2WMtL!9%yRTJ93p}@aV0zdY*?xchFI>m+UivV=;aMFp0P~ zwB8P)wvV6D-GL?6hJ#g7Hy7=2i^&Od#S=j!;Rc_yjO!*4aN7{vqzg2t-R|Dav%_NDk z`H_FVlSi==(~f-#65VmQ{EE92x<03lwo5p)s=ZJ^L7PlS>132Whr zR6v~t(#I+(`usYLCoO;Rt8j&b^5g_xgs*98Gp|N}b>-`HtVm)MscD)71y?(K6DRCZV26RsHPHKk)EKKZA%C99t3$t^B0-k5@?E>A-YMbFe?>ms?J?_guHHNU(;id*>xH zTrtam+Aq?n@-y@uY@A?hy?1qX^eLu_RaH4Ave?A8NapgQF=C%XI7wlcCf4<6BRo_% zBXxxc*A6-3CruF?3i8HOdbc%>N=-iiOF+9HX|ht6SCkz;A^am&qi_I&qk1B(x<=(m z>QG)nswCOLl_1{SZ@_eE#m^qb6#6DoMsB*)`17ui+XvF%(}|J4G$z2G*;E!1ERnAH z@q%=#uV6kBddqy4=g>!VTV)9*1=i{wJ}Ep!I*?)uJdA(LwE?(!?;}_u=^M2NShWC_ z*7l4aBJ=!QVU2-iehgb`$vOI8zkm{W%QO~?xOD;NgI;Iqa3#^$^U5D&McReLe&qs# zR<^@QpR4#W~Laz+QBsPt@3L#KF`Yr8}jgHe;5(cfpQ=;Zjtbt;c%y^#-m=hqOT z;KAYakW+$w0&F}>K10&SiPcD9SrDOuczj@U#W})5jGU-_htU`U6Q%wdy((%?J}y+$ z=$4jw1N nJo)qTxG{D(`3*#8tY|67hJRF;)r6F|#I`Ar6I0aafRa=kr-Z0I^}9xf^u;G5iEQCbpv3b#S#%H|HYHsQaHK$! zU#3Fpz8*^pK%RRmX<_09eIVziB0jOgPgFnI-*QcwEBtBiO#v!>{W1cLNXyw3D9M|A z*oGy(u8BkDA1c;MsXmpK^-~pl=We^RYnhZ4bz*)Q)C2G+E3tgx9PzU0T>c|1ilS!T zyE=bz`=wskDiOi!@!l?Y))#%{FM`}7r~X)i1)1*c6_2Q!_1{)fp%cS|YF+Q-CB%d< z=zYus`Vt@Mx*a7V)=mpLS$-5viaKgNB=+zN657qy0qR94!cTtX-Z%KBCg4OKw7b=t zr=`7q5Ox=lJ%!G5WIyNQC1xpqYU0{!I$hyrk!6%De$gp<_*Gc?ES(OwY8U^)Kjgc{ zSlhpXDb|;{+y9`u{EuMz54rlky2~p6xX2>MV6BZ&k`$q%q7v(xYps2wr9e8^4<;CB zc)eAT~B^rjzO6<4BDDH;il6 zFsM8jL+agQ;zazW(uiQjM%fPf2N~_p{cy29XP11_lQFpt`t#9nlk}>fv((FZt-dBa zuMIc4HmPHW04n0TTG9ug9;&OV9euL$Ib|+M7}}L~z4e%%%b|r~6OQj(S2d7XfYn#xp8;KQ55UYu#gY*De5j6Cc z#R%?rqwpy7I1(kpU7B*Pq=etXeYUn04jg%ZPjYqQNa$==yTG=6KX+=;i2Xg+kjV2T*Gc!(ef z`Q4fR*TA=M5-}z+s%YO+!K{k}S**ic&>o4_Tmv$EQTOp7F6TXPCj-UTXy?OQ=%*y62Qajk{rXbR%jMCOFMiVE3KekQa4xR}B%=iPtd8BXo~q$OX_ zSp910{Ew;m|GATsq_XiJ3w@s(jrj^NDtr(Dp!`Ve!Oq?|EJ9=vY2>IfrV{rT%(jiY zi}W@jA2iqd=?q>s;3%?@oi7~Ndo3Ge-2!zX58j(w&zVlPuXm3rcHb7O0RsM|!Ys(b zh(=*&Aywo3vuJoWZnU!u2_4bNkDTc&&bCYc%T zM~~xYxS#3KXFzQ@OXdc%9QDOxqiTd_> zT;(DX9{5dIuC4pO_xy+3{Ov)1I7j!Z)6&nHUvTRP>VU5dm#849icG)cvl0QOPkCIzG^lOp4#UcNr`VhBp(Ha%8@KPlvT*5u!v_$b#b~%sn3K{mu zaxeD%Q~{;Lw03ZAq(Pc-IVj>n*h3l2{sqioCMGatQY0kx zi`1(WWDQ=;gmLSGptEQ%UFC)th@|71<8eiRtX&Mx@#1q#nMF_BMfQdS>!!Qkx2o}= zuqRi?`UOX5P3fP%M+71Q$ctH4Av}bXED#fQ`KR4!b~60nsAv^*M7c-x`|~B}XIuq% zlqIJOf>WvlhQ@Uw$du|14)tZ?; zPNZ|xZSwp1y+d4sut8E4*l2JWR|~o0A9vD-?zC-w zDc@=wE1YKb*OMSi_Kx}&w;#h3>sHp|8^hnA3w?-WK)X?@Z2dgV7`9Cupf-B2RE4x^ zwlw+~!V9C^tyb`J;m2}ksD`w}G9`yu(^--{SQ+wt^Fu4Li~Fft!3QO`upSkAU?o;# z(1Q%GUVWbbkTK-M=T+ULkk3s6Dc9`G4CO6|=&-S&D+rbJQ$`Y-xL~ol;kc(l)VbU>{&>bV+*?ua;$bnDc29RW+Ig16)Vf6=L|fMR_P2b7>6}0 zdlB#-gj|j*C~M=F^2=K*k~=tl6YM3SXXi&K-`EvEXnWz&4D-^hQRBJI3gKKDj^6|> z*WhHSim1qAffNt60Mve9lfw^+&0bx-AM0%j>QP3%W=S@(l=(nrJ678mRQ(#+sI@d{ zdb#5fo#T;hK7xJ=M58wZf|?DHwD%!OZ3JrTGV5#{cfQwuiMvz%!CQ}CubJ7`z?@rSF<+KHNV2goc)a6hP0oHB@3LLKSH2w{um&J*z1Ka2 zLIR>lvOvh>Oxe%?3A@v<_T|}${zf_&@C~^FCo#jB(W9VLO?DX{)n(BQ0(V0`mI|9Y z#U3WwxixJkU_NTvA>5q(A@r2dnEXJp#6B=pww$XGU}~1~c``UKqQb=^*2P|4Dq*_! zhY^i61Sy%T5$Td0O6^C>h(xVvT!}Y##WeT8+s+Uuz=7)~V$>!zU;%d>H)rm*6^IrsCma%|cifwDLk_ z!^W2voQ)D;I$=v2E>iSaBw!d7aD+|LWl2iD!cBw`Q5p1~fk_xGiPi8e^mY&#viTAk zmaKL8m;JQ4bY(n6uBZt02z#noMMxTfF-RzjKre-c+@B)#J3pN-Zv7F}JtAwNk3j?OkpVCL6W1)Q$FLAj zGI!tX;g`O{%pt=0|q54Jyj##w*4e*|_;Us2Tn?!#^R(>u}|FAw1G_ z#wQsagnj9$TAC`2B_XgB$wNq~Sxgl?#0+QWWcB{G`c6~&SosbtRt}Tukw`TQ!oG1= zYyL(y<;Wh+H24>=E}Gs=Hs2%fg;&Qdvr74{E!R?Bd zIRQ?{{xkLJ_44P@y3^#(Be%(pk%$liKbUUo76wSoVfJmt9iTKL3z{uW6L&?jYg>EY zsx{kRiW@q%<$VZvbS(TKKTO4{Ad6l^IeY(F^3}=mX9|FZmQ`~RErNxlBPl3ast}W$T4V?SW=6kIGn@-^`qJv| zZXwhK4Kl1a4E}nLI`rdOi?^pd6;LZ-|8G&INHgOeC5q{_#s+SXb0r(;5ryHFsoTJD zx$VtNDh=-Tx3t!NTlk=hgAaSM)#U}e>_-Ex(|JoX*hWmBPPdTIa-2(BIOUJ|Iddy| zwY*J%z%W$}*;uSoB!BIJB6N6UhQUIQE_yz_qzI>J^KBi}BY>=s6i!&Tc@qiz!=i?7 zxiX$U`wY+pL|g$eMs`>($`tgd_(wYg79#sL4Fo+aAXig?OQz2#X0Qak(8U8^&8==C z#-0^IygzQfJG4SWwS5vko2aaOJn*kM+f1-)aG{T43VJAgxdP(fJ4&U{XR90*#a)G8+clOwdF?hJ?D) zmxu>0>M|g_QRHe_7G|q6o`C>9x4xd$Gl7lAuR~+FtNid=%DRsnf}YI*yOToWO%xnP zY*1G5yDnTGv{{xg5FhWU65q3-|-(+-rJ2WCeSJn(7Az>ej4Jp9+l-GyZ_| zJ8}>iA4g|}q1AhEEv#uWR&$g&Uyht?fVU(qk(j?^D`))s>oG08pow!f>P1u71P%oL2)UC4GeS87&G?{)NE;D=my1Q9{~;y zJULE=bG6jXE28Y11YmoZoo945`MM*`v%5b=_02*0cwzDve#3(4M}NPt`)?SCa|7*q z-94ks(R6WH-l9fE4m4}10WSu&O`|;ZCIT%vL$_pbABY!}s33@~gIvZ0H4co|=_-T$ zF#lC7r`89_+RL9wYN=E3YwR?2{$^ki(KKd>smX(Wh*^VmQh|Ob5$n_%N{!{9xP~LJO0^=V?BK8AbCEFBhDd$^yih$>U z(o{RReCU{#zHSEavFNdc8Yt<%N9pd1flD{ZVSWQu*ea1t#$J5f6*6;tCx=&;EIN^S}*3s%=M#)`~=nz!&Q0&{EP|9nzWyS<#!QxP;!E8&3D}?QKh^ zqGum|+;xu9QE=F#fe2ws5+y1Igr&l`fLyLKry=1}(W+2W`waeOR`ZXlW1B{|;4sE3 zn^ZVlR11hiV~p<~TaSen8I~ay#7Ql=-_|U@$8yjZsZ=Vi+^`JV2+kn+oiSUi%omO_+7}saXnJ9 z5ETilbag(g#jZPopCgJu+n@(i7g}3EK2@N zd64$77H5a`i%b%a^iRjMaprwzWz(`=7E6QY)o)gek7H)yZ-BLw^6FAoHwTj9nJtWc ztKaytMlWGLg29W{?gr|rx&snb@XyvR_}x3fmC>d=-nQp5ab3*whTw}DfUcKlMDDx` z-%?ek^*|Kqooy#>2lfklZ|jN4X$&n6f)RNNPl(+0S>t(8xSeOGj~X0CGRrWmm(WXT z))DDW_t&y$D#2`9<-+JT0x1==26*gpWPV~IF=rePVF%e-I&y$@5eo~A+>yZ&z6&7> z*INESfBHGNegTWga&d@;n;FSCGyW?}e_Qw#GTLHo*fWxuuG@I~5VA!A1pOdRTiPA~ z^AGe(yo=9bwLJD}@oDf$d+34~=(vIuPtOKiP}obDc|?@hY}J*@V|UynBeAkYa?S{@ z_f$U=K+>deTAi&=a*xv>Ruyw$UsTWY=Yn=xjf;s)6NQu>_niQ_idmzIwuL`Scf)f= zyzK?D5a5)^D@H&qN%F6Zd0JeXX*Knbe~VLe^gi|?JK67&mB4jrapV-$`hCQT;C{%T z*pjxB+Y|~LD9bmMN%Iq}S$F$x1yWU7@GcR91V8h;!O2I5MN_rq*gRx(k8T!1WSDTp zr9eJO4$~H94aG^6k5p8k=kFJ>4lnY0q_Bsa$@vTRW6uY?slH|Qt)Yu6Yun&pfJ zBi!h;6x?FDs&79#PT*HSCEUsKws#s%TFy*=2PAfb`>gEPBn+D-WdfXA?MkB=<8kb_ z1+4D11mdHG0EcAyg4dneLtfJ8)RyHQl@6hWJNe(d_EjyCHf7%Xsd)S4A-4COz{G@% z5xQ!P>AS@H@;4Ws)N91)3A6PleMe2<& z!(zv#%Uc?N`(Xmm)OJPYt)BM`nRjoWA&P0Yxl@c9Y02zlPH1J5l$nhPrMwu=atkz4 z)a-1+OEL;d@ctx=s<<+3Sv1VYy0RYmiji|#hy$66#`5;u~BkH4^$EGZ-Y4xyZ=%3KuaeLYKAUr$xMtIh_5mga> zPz<#G0mQ7IxEw-yO}BueN}RaFlg$RwCDB)vLF$wDu%qZyLYsPKdcbHD23$qn9i#JFqIo#OK?u7db2-$GatzO!On87%}Br};~#}n zziVB;qf_4(K$u>Qyz$ln_kBGS!CD-t4Y}9oxL@7@Sx*?NOAzdeINUD>Hl#*V%pfA; zSA`==YatS*G*crJ3`3ll4)vKss&)UtY#7ZxiVoG%9(4<%`WWcjX2jV(^g7Yhj+h5J z$5=?S=tuCyEt74^6jo@6y|@~N>&cVfFNtaRl=)Gm!vR;Bc$3-;ySCI$%kdmjQ|si` z{$q_YCe6vjy6re9jGN|`43D``)1PODtz0)vhV4XV36nVpOnMx2uM%qZ<3TtcI%>BQ zf0(J`{JqPPJxw>k#&nIvoZ5e9Sno)B2r+E0G} z@&M|zf4E0Q$O*NBR2I;?i7N} z@2^Su#`%qeX}m3cbSojiLk#84kvW1fICNPS`OyT0SpUoA0(s^2m~J<^eKE!dhJx_N zG_T}0&(<*an>oF=@?6?55g&IxSgY3?7|@pmDRE6gJyJNPH6un~%0hZ@?h=hI6O$b^ z)29#<4$E)cE-5IFbRpk9JVrw$$966UDyw;Iym4OY4Fc!&s1ZH4BJ1-$9<)Zt1c)N- zU^&9hsk6z?3%<9kGKHW|6~k;&cghtWz`oz`_YjVuvy;B;T67=L2c6=8`7WyTBv*QH zNv*bo1#KOk{O&)@&pkd*?v+kcJ8tM>AGx$~WMhH{L40_N=bkrVg+^p!H)IqXCQf2_ z0fPig=8CEo>p4vE(nc^DKbZ|9_Xo}$i4zJ`jVh95; z5%aNP3@``=EJ=Vt9U`y+$YtX;%OPzgZ_3+;+mh{p#W&y4-%%Bf`LhOy-*kB0qnB^m z_nBTz_b?-`F$*ymByshU>D)za2g`0j^ioo;A#QeL@x3@|+_!=YXA5f6Xg(Ack&WOg zJ<2i|Fd6OmyH!@YSMVxb;=M)ZDhBt)4`5T*>cUXWPG#%@$&*>K&u3#|`fm2mj*FKVf?du{xZ}WKWETTFhq6_fO$PS5(ItF=3~pFp~*j z!ys1<4EL1)#{`mz@gW|t-FpPkd%pK)n_Rb)F;z7cQ6dym_>YI3&e!=!m006oS3Mjq{q ze%hNzW=G0jpfl2K(x`CDuZCsJV*hm9T~%5n7R_g}VFpk`G((D^MWVMAmRp--T{`P; zwMgD<;e`fm`g3|fPns|6qnd{|FCHY*YAguXH(?%sx%4+Gu|Y)_8mk4EljxmP+MP`* z`SUbI{TCIN2OV+$y#g->Jqv#$wL;}4xJmah#$0`v^ughM_XjTA$B}ux)JZuY5-GW4 zKy440I+w=ZtE-_i+0xImq}vyzD68?8;94-5L~_O6Ty>X3itdA-x?6P(c4jkr+f!H( zUDeqiG>3bn^Sf8(`_YwqPeJ9&-@OCQZm4X{FfRMeBtN4E9Ca@;GVpU*L>lVb;@=PH zTQvTr?^jKyCKh&ZVOI*<y%T*Aw(XCPrFC=39*y$A`FSzxBiQ#W+uW10d8&gYp4{teh;^p@anft+z$5!Hv&@h0X-@xJG>hbTCxjDwMiWK@1b%8wYL6BrV zT41m}tX8g-`P@vj4T!Mlk8F0S!MA`^J=SCy9-jdwDe^hVDa`WwyI^H@ryt=F5y6>b zT8&iI6&j8edAfX^ycgWbnMZQ26Q~`LmdEScKC8|~$Jgyw(>18NAQ$9AwCRmri!96L zp^)b0P2CR-9S%cG$#rU}MXnx21T#031o>2VrDs@sa-FpjfvgLPW>Q&LHUoNOtmkt# zoDZ=5OGp{^vO~=p29^`aXd8K?(+f-bW`N$U;-o;%f?RcR!k02Nod2h^^8ly%Z67#E zC3|IOuj~^YBO=Fklo@3mvd6I{Z*&FZ>iq* zxh|JuJoo2$p8MJ3zO@dQ;%1#~Mrm48 zB0053{1bDi_a@jo<4!@!`w4}B(&Qb`~IeSBh zu+_yIYl2Wgk+?x4pCmAM>x_SqBPUj#c`C`k>_fp@qPlAAwD$!zOxRkL7;=|nu(#ut zyF^;&hm-D_;ji{d6rOloACu5*NkF4IC3@rifMG(|^Skv$H&^YnYL*rpw=UCi;JOuz zN*NX(7wZXS4tF@6PIWAs%*j!$RoL*3sh)}iry%thDvN5AUM888q_(>|Tzt|Yea3AyMYBgm$H_`F^v2%)bux)3s znFIEBDK;-JS5SH|;1?afJb<*=c5puu=w%tv#ihn*R!^Hd$KWAp4$#`joJ*)$kNtZ z2Al6h>Z>(u?3tmzA4^d+jLKx{97!Pb4;CX&u;M||**7zXI7hO6nrdMx*Xa=|-`#1^ zBQ?Ha&7cd7hN=%y4yUp?zl8~Lo;%mQrDe8!ce-W_K94FFMN*g(w8q-_K5S+c0{o29X&PzpV;UJE^!xnFc%b@>kvW4m#xiOj-L*DadC&2N#0Us z;<-(m1WB7$=j6hjcPC6JB)D3T2#IC`ibu#yi!uK7W2!j|Z>~RaJ*&XXy#ytIk2DIp z5?Qd^s90_?ILjU#>ZWk5HXts}grg_!Gmgm!d?eLGR7xEP zvTCrslV~94ym5_i<5oqy(@@?wN}lIdtiY8=?|Ng!XeYnly`@9wCGx2S$3x|0x8T2h zz7A85Vb2>s44rKpI_4Y7_Pnd2^mYj2%^jM|Du>u4`^Psda^JIP%*DK6bo`Vf&f{!% zDTYCwF5Nhi=)QhU2$@eQv&ZzxsX+Hl+gP6kW|e!n9IU2>Vh~cioI{>4WvR}t*4Hpz z%5z?HjLGoka}Q3AbX9AkY|Yjf^M(>@tBAI9JO5pDCQu0R3Nns>)LC#vB2p96C*?K? zvX$un$sBDx$1=+NNj*@Oa@u*b@O*XBr_sg@8sCUq-|LK!MUmC)epklrv}5O_^<{NP zX16|c$9Wtbks3y7geI^tF5oRZJu;v zwkW8j+8Ccxo9stEDOT_Go&j%$KCgVO7pm+^%PKEPBZqbMw%s@732XS{cX+wCSjH1s z5)bc=g**<^NNsroY` z?}fHHlgu^B?2r{^^gQ&j zbF~T((>|Yg&C5WKL8DCnl1}Z3!YHFW2S1|;Xr0`Uz-;=FxEwYc4QpeAtnm7^f~uzX zl;xA!?>MLR?tL80Iudm;mi{!ewL91KhG7Hsa-XepKi<2mc6%zf0GwtbfJ1Zf-<@Xu z#|XWDzv|04t)&9Id!UxAAkN{t5qC%%8-WV3i;3duS19%m2||Y{!3pR1=g|zQYAMqc zff)_2nj-O4wfxy;UNM?|Uieo!^J$A*uDe>@V(NKH;KS;Y_dtE8${p>RdcrW;=2*fj4~d?OG0l-(g?ik}vz} z)5-wDppVts>K-=|@{=!53?=8)Jw#RGpS_FWpbwtn}{v!JEJ$q-sr7F6&OPBuI# zuVNFMPte79XgEu!P&qRq8u4J>r%$l-IQ00Lin90(_KtC)aR_de zxN=pY2<1b29_^AG2WJIGmmX4rv3$!`l15{e(H!1^+x9voZ6;882YAE12q7+lgy+>) zj|s0CyzI9=Mo!R}&LXB`&DYpZ7c?0r(&KNV+~TULd0y^e;G{KVR4nL0KvU9mr8&$^ zxrM-9P8zE`J?aZ(iB~Rz<{vvnk2HaZU#K$aVFfYnbAXVUOLU#As5JvS%+26 zi$sNuPY}dLGUS$0g&;oBqhzv2dY`l3@6Na403M!Sh${B|7(y|_cONa;6BrtUe@ZzV z7SThtHT8k?Rwc)(Z}@BP#H@JJHz&GR&M=E@P9KJ89yQKmRh&I~%vbL1L-K3E>7>CH z)Y!=jXVb1iPrAoAZZ3}3wU*5~nrV!ZjL5zqJ<@NwjHCZC>68Cc<{&E_#S;E*jOdjtg?uKN|l`P8sjz&Qf7a^z9 z;{3-8T+H4y99_zc;JYIvs!sk$G}` z??mt*Mm9Z@glCZb!X?!xXD-21sFDPEpZOK{sbQseQ$%6~b;n+*z0hRoR}0Pe>B|#t z$XrVcXv8M|q*Z8MY&r9J0A=d^1bHpjrUXu)qEj~$%%=gZp`^~%O*lzxUquG^p6;n; z^(3HL+hx4gRP?4N*b2p9!^|2~rcw3!9nQj$vmZusbXYz_x^AVc`3qBFm(jS9ueU5h z^AnNnbswfQ2Jq=W=T+p-V|nQco@bOAH$pLQZ+BKH8E$iM>IDz z3|wc?QP`yI=X5YTlp8h}%p6{Deq?S0QD$Ug>ih1SdPZg237Rl{S~=Ha4~-ckMoIWMn+X@@`V6 z#HHZj>MQbt$Qqp*9T(cjc^lxZ7UO(>PwzF-qEr(wo`vaulxdall|KP`7p4gd`23&Jy=#sAes*0diLB(U$Nx46VQvP)8idSs8^zaV91xw*O-JMH=)FoJshRob|_)O)ojtfP))WHCr(;*2;VMQ75^ zfN@a^f#o<|*9X;3IcGodLUz-3i~FAu+zI4c5h+nW^h_!^)b*B_xw-l4O$TB(ixaqW ziMoa%i=BeS<-F45kMO;Tw|FWa`G2c!SuOA3CbowPhF6csf1|&qqugUrj;UgGHm| z;j^yoH?MZhR;AYOW_XW2Lg2j%%ejL)B@*bUMD`g<#Z${1+fa57r7X82 zcqY-cfPnK%Y^3@szRner zt)bBToYCph6Jv*W+&t?&9FG4(Iu2w46 z4B#AcFy_^J@f*6<{>CN}Sj969*DYV*e7<61U>GoN{tz!Do90+jApFueVY_IW(MQF; zl?4yA_(MvMwN&pWKVyg{3uU_+y6RMdot2vu%mC?st=N0pf-~JZXE?3JFf)j<{1xsU z`2ephz)#HzsWEP!inHm2hI(V(~@W zY7gGU-lO52cHD&SY)>QHgy$=>^X%u0TQZfCizro!*weMyvZC=;MWOawdAx~`3C*W` z%^#^$uRP;gyqEE0<(i8xcQY$oc+6mY#z{-XFxsO1(cN8Y)>p;^q9|5bk`Z*p|c!?(rErw#y;yT(%@c7trQBv6cj)$3>pI z>tz+;IB?D=aQV=s(n)o63*yn8dX1m7#Z4G{%fF@K2o5n3jxR~mU?nzMi#;}8e#(>{ zy{Z4!AI)jZ8TY;nq1aq}tq;~=zzoTv)er06oeX3;9{uP{LWR*2%9cmE%S^`~!BW>X zn3PZFTf3g*dG68~^1*q@#^Ge(_8puPEFLD8OS|0b2a{5e=N4S%;~f3tC>F6UxK#v9 z)N-#Mv8=ePCh1KsUKD1A8jF_%$MPf|_yCN9oy%*@um6D{w*2|4GY zb}gafrSC+f=b*W{)!a!fqwZ9)K>fk=i4qf!4M?0v{CMNTo2A9}mQzV=%3UT&i{3{W z>ulG#M!K7%jPf6Mjff9BMslgQq3zIogY);Cv3v;&b#;^=sh#(Bn%W)H*bHNaLwdpq z85%fUTUJJNjYO_426T2TBj0D{6t zw&S_HZ|C?pI_2q(9Fas&@uJs6nVX;P*5K#6p|#)_(8PM-{L(;2wl`ma{ZAd5gA)?y z>0GSLoK<*FwW+G8@-M3vcffg7I(qm7lzF)n`Q9iCvp*mn7=|CjlpG{x z&r0n}XLWZ!>=lynUr7D`6n`7a_ZgT< zm!i;&?Fb0Q2QmqmCHfZ7ex=_tU~(7b)L?RIvPyEAU=gLIZ-VTAA~WR00yKyTXg^(G zqWLZJs!FnQYMOH3*fN&Tn(IKMLf{Ki?pRo8zZJ6YVyj)y0^)-sR}2-)%mI(Aw2AgT zbbp1T{qB(OSNJd0cVBH^tI>HR(q+#*lmi@LWe*rZz&M2h1L_=50uZ1e*n#E*`6?aw zj`ka&JpceRGe@}Ey1)Q~O}0qHRg4K_u>4e1arvJ7Q9!=t5AuzG`n=a-f0}{+lnCE#zu$`oVn44eS&T?N*wz~t~E&oQDBrB_MSg z_yVrQehWbD0xHX|v-hpselAu;O7s;P*!uAT`dr~}Lie=tknaGoiU?;*8Cwgala-65 zosOB4mATbdXJFujzgA4?UkCKE093A1KM?W&Pw>A?IACqg1z~IZYkdP70EeCfjii(n z3k%ax?4|rY(87N&_vhsyVK1zp@uils|B%`(V4e3%sj5f|i(eIhiSg-fHK1Pb0-mS^ zeh?WA7#{hhNci5e;?n*iVy|)iJiR>|8{TN3!=VBC2dN)~^ISSW_(g<^rHr$)nVrdA z39BMa5wl5q+5F@)4b%5-> zA^-P20l_e^S2PTa&HE2wf3jf)#)2ITVXzndeuMpPo8}kphQKhegB%QO+yBpDpgkcl z1nlPp14#+^bIA7__h16pMFECzKJ3p4`;Rf$gnr%{!5#oG42AH&X8hV8061%4W91ku z`OW_hyI+uBOqYXkVC&BqoKWmv;|{O|4d#Nay<)gkxBr^^N48(VDF7Sj#H1i3>9138 zkhxAU7;M)I18&d!Yw!V9zQA0tp(G4<8U5GX{YoYCQ?p56FxcD-2FwO5fqyx@__=$L zeK6Sg3>XQv)qz1?zW-k$_j`-)tf+yRU_%fXrenc>$^70d1Q-W?T#vy;6#Y-Q-<2)+ z5iTl6MA7j9m&oBhRXTKr*$3gec z3E;zX457RGZwUvD$l&8e42Qb^cbq>zYy@ive8`2N9vk=#6+AQlZZ7qk=?(ap1q0n0 z{B9Fte-{Gi-Tvax1)M+d1}Fyg@9X~sh1m|hsDcZuYOnxriBPN;z)q3<=-yBN2iM6V A?*IS* literal 0 HcmV?d00001 diff --git a/chapter08/03 - customfieldfinal/.mvn/wrapper/maven-wrapper.properties b/chapter08/03 - customfieldfinal/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter08/03 - customfieldfinal/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter08/03 - customfieldfinal/docker-compose.yaml b/chapter08/03 - customfieldfinal/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter08/03 - customfieldfinal/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter08/03 - customfieldfinal/mvnw b/chapter08/03 - customfieldfinal/mvnw index 5bf251c..66df285 100755 --- a/chapter08/03 - customfieldfinal/mvnw +++ b/chapter08/03 - customfieldfinal/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter08/03 - customfieldfinal/mvnw.cmd b/chapter08/03 - customfieldfinal/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter08/03 - customfieldfinal/mvnw.cmd +++ b/chapter08/03 - customfieldfinal/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter08/03 - customfieldfinal/pom.xml b/chapter08/03 - customfieldfinal/pom.xml index 468a4d0..43db322 100644 --- a/chapter08/03 - customfieldfinal/pom.xml +++ b/chapter08/03 - customfieldfinal/pom.xml @@ -1,231 +1,208 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 27.1-jre + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 2.0.3.RELEASE - 1.11.2 - - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - org.postgresql - postgresql - - - org.flywaydb - flyway-core - + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter08/03 - customfieldfinal/src/docs/asciidoc/Copsboot REST API Guide.adoc b/chapter08/03 - customfieldfinal/src/docs/asciidoc/Copsboot REST API Guide.adoc new file mode 100644 index 0000000..b0b91ae --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -0,0 +1,14 @@ += Copsboot REST API Guide +:icons: font +:toc: +:toclevels: 2 + +:numbered: + +== Introduction + +The Copsboot project uses a REST API for interfacing with the server. + +This documentation covers version {project-version} of the application. + +include::_users.adoc[] diff --git a/chapter08/03 - customfieldfinal/src/docs/asciidoc/_users.adoc b/chapter08/03 - customfieldfinal/src/docs/asciidoc/_users.adoc new file mode 100644 index 0000000..2becf75 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/docs/asciidoc/_users.adoc @@ -0,0 +1,24 @@ +//tag::initial-doc[] +== User Management + +=== User information + +The API allows to get information on the currently logged on user +via a `GET` on `/api/users/me`. If you are not a logged on user, the +following response will be returned: + +operation::own-details-unauthorized[snippets='http-request,http-response'] +//end::initial-doc[] + +If you do log on as a user, you get more information on that user: + +operation::own-details[snippets='http-request,http-response,response-fields'] + + +//tag::create-user[] +=== Create a user + +To create an new user, do a `POST` on `/api/users`: + +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter08/03 - customfieldfinal/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter08/03 - customfieldfinal/src/main/asciidoc/Copsboot REST API Guide.adoc deleted file mode 100644 index 255bc8e..0000000 --- a/chapter08/03 - customfieldfinal/src/main/asciidoc/Copsboot REST API Guide.adoc +++ /dev/null @@ -1,14 +0,0 @@ -= Copsboot REST API Guide -:icons: font -:toc: -:toclevels: 2 - -:numbered: - -== Introduction - -The Copsboot project uses a REST API for interfacing with the server. - -This documentation covers version {project-version} of the application. - -include::_users.adoc[] \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/main/asciidoc/_users.adoc b/chapter08/03 - customfieldfinal/src/main/asciidoc/_users.adoc deleted file mode 100644 index a033db8..0000000 --- a/chapter08/03 - customfieldfinal/src/main/asciidoc/_users.adoc +++ /dev/null @@ -1,24 +0,0 @@ -//tag::initial-doc[] -== User Management - -=== User information - -The API allows to get information on the currently logged on user -via a `GET` on `/api/users/me`. If you are not a logged on user, the -following response will be returned: - -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] -//end::initial-doc[] - -If you do log on as a user, you get more information on that user: - -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] - - -//tag::create-user[] -=== Create a user - -To create an new user, do a `POST` on `/api/users`: - -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java index 344a5fe..fb1cc59 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java @@ -6,6 +6,7 @@ public final class SpringProfiles { public static final String TEST = "test"; public static final String STAGING = "staging"; public static final String PROD = "prod"; + public static final String REPOSITORY_TEST = "repository-test"; public static final String INTEGRATION_TEST = "integration-test"; private SpringProfiles() { diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java index 9c92c49..8d26775 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java @@ -1,11 +1,4 @@ package com.example.copsboot.infrastructure.mvc; -import lombok.Value; - -//tag::class[] -@Value -public class FieldErrorResponse { - private String fieldName; - private String errorMessage; +public record FieldErrorResponse(String fieldName, String errorMesesage) { } -//end::class[] \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/CreateReportParameters.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/CreateReportParameters.java new file mode 100644 index 0000000..64aeea6 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/CreateReportParameters.java @@ -0,0 +1,8 @@ +package com.example.copsboot.report; + +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +public record CreateReportParameters(UserId userId, Instant dateTime, String description) { +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/Report.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/Report.java index c0f9c66..b10756f 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/Report.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/Report.java @@ -1,36 +1,36 @@ package com.example.copsboot.report; -import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; import com.example.orm.jpa.AbstractEntity; import com.example.util.ArtifactForFramework; +import jakarta.persistence.Entity; + +import java.time.Instant; -import javax.persistence.Entity; -import javax.persistence.ManyToOne; -import java.time.ZonedDateTime; //tag::class[] @Entity public class Report extends AbstractEntity { - @ManyToOne - private User reporter; - private ZonedDateTime dateTime; + + private UserId reporterId; + private Instant dateTime; private String description; @ArtifactForFramework protected Report() { } - public Report(ReportId id, User reporter, ZonedDateTime dateTime, String description) { + public Report(ReportId id, UserId reporterId, Instant dateTime, String description) { super(id); - this.reporter = reporter; + this.reporterId = reporterId; this.dateTime = dateTime; this.description = description; } - public User getReporter() { - return reporter; + public UserId getReporterId() { + return reporterId; } - public ZonedDateTime getDateTime() { + public Instant getDateTime() { return dateTime; } diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/ReportService.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/ReportService.java index f6ed620..613248b 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/ReportService.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/ReportService.java @@ -1,9 +1,23 @@ package com.example.copsboot.report; -import com.example.copsboot.user.UserId; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.ZonedDateTime; -public interface ReportService { - Report createReport(UserId reporterId, ZonedDateTime dateTime, String description); +@Service +@Transactional +public class ReportService { + private final ReportRepository repository; + + public ReportService(ReportRepository repository) { + this.repository = repository; + } + + public Report createReport(CreateReportParameters parameters) { + return repository.save(new Report(repository.nextId(), + parameters.userId(), + parameters.dateTime(), + parameters.description())); + } } diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/ReportServiceImpl.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/ReportServiceImpl.java deleted file mode 100644 index 67c21e1..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/ReportServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserNotFoundException; -import com.example.copsboot.user.UserService; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.ZonedDateTime; - -@Service -@Transactional -public class ReportServiceImpl implements ReportService { - private final ReportRepository repository; - private final UserService userService; - - public ReportServiceImpl(ReportRepository repository, UserService userService) { - this.repository = repository; - this.userService = userService; - } - - @Override - public Report createReport(UserId reporterId, ZonedDateTime dateTime, String description) { - return repository.save(new Report(repository.nextId(), - userService.getUser(reporterId) - .orElseThrow(() -> new UserNotFoundException(reporterId)), - dateTime, - description)); - } -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java deleted file mode 100644 index 9a169e4..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.report.web; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.ZonedDateTime; - -//tag::class[] -@Data -@AllArgsConstructor -@NoArgsConstructor -public class CreateReportParameters { - private ZonedDateTime dateTime; - - @ValidReportDescription - private String description; -} -//end::class[] \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java new file mode 100644 index 0000000..2c7fac1 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java @@ -0,0 +1,12 @@ +package com.example.copsboot.report.web; + +import com.example.copsboot.report.CreateReportParameters; +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +public record CreateReportRequest(Instant dateTime, @ValidReportDescription String description) { + public CreateReportParameters toParameters(UserId userId) { + return new CreateReportParameters(userId, dateTime, description); + } +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java index e16971d..aa30ca4 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; public class ReportDescriptionValidator implements ConstraintValidator { //<1> diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportDto.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportDto.java index 0adc7f8..28e606e 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportDto.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportDto.java @@ -2,23 +2,21 @@ import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; -import lombok.Value; +import com.example.copsboot.user.UserService; -import java.time.ZonedDateTime; +import java.time.Instant; //tag::class[] -@Value -public class ReportDto { - private ReportId id; - private String reporter; - private ZonedDateTime dateTime; - private String description; +public record ReportDto(ReportId id, + String reporter, + Instant dateTime, + String description) { - public static ReportDto fromReport(Report report) { + public static ReportDto fromReport(Report report, UserService userService) { return new ReportDto(report.getId(), - report.getReporter().getEmail(), - report.getDateTime(), - report.getDescription()); + userService.getUserById(report.getReporterId()).getEmail(), + report.getDateTime(), + report.getDescription()); } } //end::class[] diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportRestController.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportRestController.java index 28b79ae..83f9d54 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportRestController.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportRestController.java @@ -1,31 +1,42 @@ package com.example.copsboot.report.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportService; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserNotFoundException; +import com.example.copsboot.user.UserService; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.UUID; //tag::class[] @RestController @RequestMapping("/api/reports") public class ReportRestController { private final ReportService service; + private final UserService userService; - public ReportRestController(ReportService service) { + public ReportRestController(ReportService service, UserService userService) { this.service = service; + this.userService = userService; } @PostMapping @ResponseStatus(HttpStatus.CREATED) - public ReportDto createReport(@AuthenticationPrincipal ApplicationUserDetails userDetails, - @Valid @RequestBody CreateReportParameters parameters) { - return ReportDto.fromReport(service.createReport(userDetails.getUserId(), - parameters.getDateTime(), - parameters.getDescription())); + public ReportDto createReport(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateReportRequest request) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + User user = userService.findUserByAuthServerId(authServerId) + .orElseThrow(() -> new UserNotFoundException(authServerId)); + CreateReportParameters parameters = request.toParameters(user.getId()); + Report report = service.createReport(parameters); + return ReportDto.fromReport(report, userService); } } //end::class[] diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java index 41d39e9..ba8fa56 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -16,4 +16,4 @@ Class[] groups() default {}; //<5> Class[] payload() default {}; //<6> -} \ No newline at end of file +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/User.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/User.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java new file mode 100644 index 0000000..2a434e3 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class UserIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(UserId attribute) { + return attribute.getId(); + } + + @Override + public UserId convertToEntityAttribute(UUID dbData) { + return new UserId(dbData); + } +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserNotFoundException.java index 1f65f04..97d0813 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserNotFoundException.java @@ -3,9 +3,13 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> +@ResponseStatus(HttpStatus.NOT_FOUND) public class UserNotFoundException extends RuntimeException { public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); + super(String.format("Unable to find user with id %s", userId)); + } + + public UserNotFoundException(AuthServerId authServerId) { + super(String.format("Unable to find user with auth server id %s", authServerId)); } } diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserService.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..ec5aa13 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,33 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } + + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } - Optional getUser(UserId userId); + public User getUserById(UserId userId) { + return repository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + } + // end::createUser[] } diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..b87302d --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,17 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(@NotEmpty String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + String email = jwt.getClaimAsString("email"); + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/UserRestController.java index b5aa1a8..e0a6545 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,49 +1,53 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import lombok.Value; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Valid; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] + // tag::createUser[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { - User officer = service.createOfficer(parameters.getEmail(), - parameters.getPassword()); - return UserDto.fromUser(officer); + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/Entity.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/Entity.java index a573e0e..3a45231 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/Entity.java @@ -1,6 +1,5 @@ package com.example.orm.jpa; -import java.io.Serializable; /** * Interface for entity objects. diff --git a/chapter08/03 - customfieldfinal/src/main/resources/application-dev.properties b/chapter08/03 - customfieldfinal/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter08/03 - customfieldfinal/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/main/resources/application-local.properties b/chapter08/03 - customfieldfinal/src/main/resources/application-local.properties index 8f13f3f..7e354d5 100644 --- a/chapter08/03 - customfieldfinal/src/main/resources/application-local.properties +++ b/chapter08/03 - customfieldfinal/src/main/resources/application-local.properties @@ -3,13 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +#spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter08/03 - customfieldfinal/src/main/resources/application.properties b/chapter08/03 - customfieldfinal/src/main/resources/application.properties index e69de29..3e80adf 100644 --- a/chapter08/03 - customfieldfinal/src/main/resources/application.properties +++ b/chapter08/03 - customfieldfinal/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/copsboot + +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter08/03 - customfieldfinal/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter08/03 - customfieldfinal/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter08/03 - customfieldfinal/src/main/resources/db/migration/V1.0.0.2__reports.sql b/chapter08/03 - customfieldfinal/src/main/resources/db/migration/V1.0.0.2__reports.sql new file mode 100644 index 0000000..cc2e26c --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/resources/db/migration/V1.0.0.2__reports.sql @@ -0,0 +1,8 @@ +CREATE TABLE report +( + date_time TIMESTAMP(6) WITH TIME ZONE, + id uuid NOT NULL, + description VARCHAR(255), + reporter_id uuid, + PRIMARY KEY (id) +); diff --git a/chapter08/03 - customfieldfinal/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter08/03 - customfieldfinal/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter08/03 - customfieldfinal/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter08/03 - customfieldfinal/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter08/03 - customfieldfinal/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter08/03 - customfieldfinal/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter08/03 - customfieldfinal/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter08/03 - customfieldfinal/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..5feb390 100644 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,16 @@ package com.example.copsboot; import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/Reports.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/Reports.java deleted file mode 100644 index 2302dc9..0000000 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/Reports.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.Users; - -import java.time.ZonedDateTime; -import java.util.UUID; - -public class Reports { - public static Report createRandomReport(String description) { - return new Report(new ReportId(UUID.randomUUID()), - Users.newRandomOfficer(), - ZonedDateTime.now(), - description); - } -} diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java index 199247a..1b1ec35 100644 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java @@ -1,12 +1,12 @@ package com.example.copsboot.report.web; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; import org.junit.Test; -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.time.ZonedDateTime; +import java.time.Instant; import java.util.Set; import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; @@ -16,25 +16,27 @@ public class ReportDescriptionValidatorTest { //tag::invalid[] @Test public void givenEmptyString_notValid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); //<1> - Validator validator = factory.getValidator(); //<2> + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { //<1> + Validator validator = factory.getValidator(); //<2> - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), ""); - Set> violationSet = validator.validate(parameters); //<3> - assertThat(violationSet).hasViolationOnPath("description"); //<4> + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), ""); + Set> violationSet = validator.validate(parameters); //<3> + assertThat(violationSet).hasViolationOnPath("description"); //<4> + } } //end::invalid[] //tag::valid[] @Test public void givenSuspectWordPresent_valid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), - "The suspect was wearing a black hat."); - Set> violationSet = validator.validate(parameters); - assertThat(violationSet).hasNoViolations(); + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat."); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } } //end::valid[] -} \ No newline at end of file +} diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java index 49705e9..d6c6e5f 100644 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java @@ -1,61 +1,76 @@ package com.example.copsboot.report.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; import com.example.copsboot.report.ReportService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.time.ZonedDateTime; +import java.time.Instant; +import java.util.Optional; import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; //tag::class[] -@RunWith(SpringRunner.class) @CopsbootControllerTest(ReportRestController.class) public class ReportRestControllerTest { @Autowired - private MockMvc mvc; - - @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; @MockBean private ReportService service; + @MockBean + private UserService userService; @Test public void officerIsAbleToPostAReport() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - ZonedDateTime dateTime = ZonedDateTime.parse("2018-04-11T22:59:03.189+02:00"); - String description = "The suspect is wearing a black hat."; - CreateReportParameters parameters = new CreateReportParameters(dateTime, - description); - when(service.createReport(eq(Users.officer().getId()), any(ZonedDateTime.class), eq(description))) - .thenReturn(new Report(new ReportId(UUID.randomUUID()), Users.officer(), dateTime, description)); - mvc.perform(post("/api/reports") - .header(HEADER_AUTHORIZATION, bearer(accessToken)) - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("reporter").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("dateTime").value("2018-04-11T22:59:03.189+02:00")) - .andExpect(jsonPath("description").value(description)); + UserId userId = new UserId(UUID.randomUUID()); + AuthServerId authServerId = new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); + User user = new User(userId, + "wim@example.com", + authServerId, + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"); + when(userService.findUserByAuthServerId(authServerId)) + .thenReturn(Optional.of(user)); + when(userService.getUserById(userId)) + .thenReturn(user); + when(service.createReport(any(CreateReportParameters.class))) + .thenReturn(new Report(new ReportId(UUID.randomUUID()), + userId, + Instant.parse("2023-04-11T22:59:03.189+02:00"), + "This is a test report description. The suspect was wearing a black hat.")); + mockMvc.perform(post("/api/reports") + .with(jwt().jwt(builder -> builder.subject(authServerId.value().toString()) + .claim("email", "wim@example.com")) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "dateTime": "2023-04-11T22:59:03.189+02:00", + "description": "This is a test report description. The suspect was wearing a black hat." + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("id").exists()) + .andExpect(jsonPath("reporter").value("wim@example.com")) + .andExpect(jsonPath("dateTime").value("2023-04-11T20:59:03.189Z")) + .andExpect(jsonPath("description").value("This is a test report description. The suspect was wearing a black hat.")); } } -//end::class[] \ No newline at end of file +//end::class[] diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java deleted file mode 100644 index 720f959..0000000 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.copsboot.user; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.HashSet; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> -@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) //<2> -public class UserRepositoryIntegrationTest { - @Autowired - private UserRepository repository; - @PersistenceContext - private EntityManager entityManager; - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void testSaveUser() { - Set roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); - - assertThat(repository.count()).isEqualTo(1L); - - entityManager.flush(); //<3> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM user_roles", Long.class)).isEqualTo(1L); - assertThat(jdbcTemplate.queryForObject("SELECT roles FROM user_roles", String.class)).isEqualTo("OFFICER"); - } -} diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..19c23fe 100644 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -3,14 +3,16 @@ import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,62 +21,34 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) //<2> public class UserRepositoryTest { @Autowired private UserRepository repository; + @PersistenceContext + private EntityManager entityManager; + @Autowired + private JdbcTemplate jdbcTemplate; - //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } + assertThat(repository.count()).isEqualTo(1L); - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); + entityManager.flush(); //<3> - assertThat(optional).isEmpty(); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> + assertThat(jdbcTemplate.queryForObject("SELECT email FROM copsboot_user", String.class)).isEqualTo("alex.foley@beverly-hills.com"); } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @Bean @@ -82,5 +56,4 @@ public UniqueIdGenerator generator() { return new InMemoryUniqueIdGenerator(); } } - //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/Users.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index e0d24b0..805c501 100644 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,133 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index eb07c50..a20d744 100644 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,118 +1,104 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } + // tag::emptyToken[] @Test - public void testCreateOfficerIfPasswordIsTooShort() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("errors[0].fieldName").value("password")); - - verify(service, never()).createOfficer(email, password); + void givenEmptyMobileToken_badRequestIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "" + } + """)) //<.> + .andExpect(status().isBadRequest()) //<.> + .andDo(print()); //<.> + + verify(userService, never()).createUser(any(CreateUserParameters.class)); //<.> } + // end::emptyToken[] } diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java index 77c5f4c..21556a5 100644 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java @@ -2,7 +2,8 @@ import org.assertj.core.api.AbstractAssert; -import javax.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolation; + import java.util.Set; import java.util.stream.Collectors; @@ -21,12 +22,12 @@ public ConstraintViolationSetAssert hasViolationOnPath(String path) { // check condition if (!containsViolationWithPath(actual, path)) { failWithMessage("There was no violation with path <%s>. Violation paths: <%s>", path, actual.stream() - .map(violation -> violation - .getPropertyPath() - .toString()) - .collect( - Collectors - .toList())); + .map(violation -> violation + .getPropertyPath() + .toString()) + .collect( + Collectors + .toList())); } return this; diff --git a/chapter08/03 - customfieldfinal/src/test/resources/application-integration-test.properties b/chapter08/03 - customfieldfinal/src/test/resources/application-integration-test.properties index 159536c..c61e563 100644 --- a/chapter08/03 - customfieldfinal/src/test/resources/application-integration-test.properties +++ b/chapter08/03 - customfieldfinal/src/test/resources/application-integration-test.properties @@ -1,11 +1,6 @@ -spring.datasource.url=jdbc:tc:postgresql://localhost/copsbootdb +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.username=user spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none - -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/03 - customfieldfinal/src/test/resources/application-repository-test.properties b/chapter08/03 - customfieldfinal/src/test/resources/application-repository-test.properties new file mode 100644 index 0000000..c61e563 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/test/resources/application-repository-test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=user +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/03 - customfieldfinal/src/test/resources/application-test.properties b/chapter08/03 - customfieldfinal/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter08/03 - customfieldfinal/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/test/resources/logback-test.xml b/chapter08/03 - customfieldfinal/src/test/resources/logback-test.xml index bf47fec..164429c 100644 --- a/chapter08/03 - customfieldfinal/src/test/resources/logback-test.xml +++ b/chapter08/03 - customfieldfinal/src/test/resources/logback-test.xml @@ -5,7 +5,7 @@ - + @@ -17,14 +17,8 @@ - - - - - - - \ No newline at end of file + diff --git a/chapter08/04 - objectvalidation/.mvn/wrapper/maven-wrapper.jar b/chapter08/04 - objectvalidation/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter08/04 - objectvalidation/.mvn/wrapper/maven-wrapper.properties b/chapter08/04 - objectvalidation/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter08/04 - objectvalidation/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter08/04 - objectvalidation/docker-compose.yaml b/chapter08/04 - objectvalidation/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter08/04 - objectvalidation/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter08/04 - objectvalidation/mvnw b/chapter08/04 - objectvalidation/mvnw index 5bf251c..66df285 100755 --- a/chapter08/04 - objectvalidation/mvnw +++ b/chapter08/04 - objectvalidation/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter08/04 - objectvalidation/mvnw.cmd b/chapter08/04 - objectvalidation/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter08/04 - objectvalidation/mvnw.cmd +++ b/chapter08/04 - objectvalidation/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter08/04 - objectvalidation/pom.xml b/chapter08/04 - objectvalidation/pom.xml index 468a4d0..43db322 100644 --- a/chapter08/04 - objectvalidation/pom.xml +++ b/chapter08/04 - objectvalidation/pom.xml @@ -1,231 +1,208 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 27.1-jre + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 2.0.3.RELEASE - 1.11.2 - - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - org.postgresql - postgresql - - - org.flywaydb - flyway-core - + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter08/04 - objectvalidation/src/docs/asciidoc/Copsboot REST API Guide.adoc b/chapter08/04 - objectvalidation/src/docs/asciidoc/Copsboot REST API Guide.adoc new file mode 100644 index 0000000..b0b91ae --- /dev/null +++ b/chapter08/04 - objectvalidation/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -0,0 +1,14 @@ += Copsboot REST API Guide +:icons: font +:toc: +:toclevels: 2 + +:numbered: + +== Introduction + +The Copsboot project uses a REST API for interfacing with the server. + +This documentation covers version {project-version} of the application. + +include::_users.adoc[] diff --git a/chapter08/04 - objectvalidation/src/docs/asciidoc/_users.adoc b/chapter08/04 - objectvalidation/src/docs/asciidoc/_users.adoc new file mode 100644 index 0000000..2becf75 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/docs/asciidoc/_users.adoc @@ -0,0 +1,24 @@ +//tag::initial-doc[] +== User Management + +=== User information + +The API allows to get information on the currently logged on user +via a `GET` on `/api/users/me`. If you are not a logged on user, the +following response will be returned: + +operation::own-details-unauthorized[snippets='http-request,http-response'] +//end::initial-doc[] + +If you do log on as a user, you get more information on that user: + +operation::own-details[snippets='http-request,http-response,response-fields'] + + +//tag::create-user[] +=== Create a user + +To create an new user, do a `POST` on `/api/users`: + +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter08/04 - objectvalidation/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter08/04 - objectvalidation/src/main/asciidoc/Copsboot REST API Guide.adoc deleted file mode 100644 index 255bc8e..0000000 --- a/chapter08/04 - objectvalidation/src/main/asciidoc/Copsboot REST API Guide.adoc +++ /dev/null @@ -1,14 +0,0 @@ -= Copsboot REST API Guide -:icons: font -:toc: -:toclevels: 2 - -:numbered: - -== Introduction - -The Copsboot project uses a REST API for interfacing with the server. - -This documentation covers version {project-version} of the application. - -include::_users.adoc[] \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/main/asciidoc/_users.adoc b/chapter08/04 - objectvalidation/src/main/asciidoc/_users.adoc deleted file mode 100644 index a033db8..0000000 --- a/chapter08/04 - objectvalidation/src/main/asciidoc/_users.adoc +++ /dev/null @@ -1,24 +0,0 @@ -//tag::initial-doc[] -== User Management - -=== User information - -The API allows to get information on the currently logged on user -via a `GET` on `/api/users/me`. If you are not a logged on user, the -following response will be returned: - -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] -//end::initial-doc[] - -If you do log on as a user, you get more information on that user: - -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] - - -//tag::create-user[] -=== Create a user - -To create an new user, do a `POST` on `/api/users`: - -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java index 344a5fe..fb1cc59 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java @@ -6,6 +6,7 @@ public final class SpringProfiles { public static final String TEST = "test"; public static final String STAGING = "staging"; public static final String PROD = "prod"; + public static final String REPOSITORY_TEST = "repository-test"; public static final String INTEGRATION_TEST = "integration-test"; private SpringProfiles() { diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java index 9c92c49..8d26775 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java @@ -1,11 +1,4 @@ package com.example.copsboot.infrastructure.mvc; -import lombok.Value; - -//tag::class[] -@Value -public class FieldErrorResponse { - private String fieldName; - private String errorMessage; +public record FieldErrorResponse(String fieldName, String errorMesesage) { } -//end::class[] \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/CreateReportParameters.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/CreateReportParameters.java new file mode 100644 index 0000000..64aeea6 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/CreateReportParameters.java @@ -0,0 +1,8 @@ +package com.example.copsboot.report; + +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +public record CreateReportParameters(UserId userId, Instant dateTime, String description) { +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/Report.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/Report.java index c0f9c66..b10756f 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/Report.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/Report.java @@ -1,36 +1,36 @@ package com.example.copsboot.report; -import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; import com.example.orm.jpa.AbstractEntity; import com.example.util.ArtifactForFramework; +import jakarta.persistence.Entity; + +import java.time.Instant; -import javax.persistence.Entity; -import javax.persistence.ManyToOne; -import java.time.ZonedDateTime; //tag::class[] @Entity public class Report extends AbstractEntity { - @ManyToOne - private User reporter; - private ZonedDateTime dateTime; + + private UserId reporterId; + private Instant dateTime; private String description; @ArtifactForFramework protected Report() { } - public Report(ReportId id, User reporter, ZonedDateTime dateTime, String description) { + public Report(ReportId id, UserId reporterId, Instant dateTime, String description) { super(id); - this.reporter = reporter; + this.reporterId = reporterId; this.dateTime = dateTime; this.description = description; } - public User getReporter() { - return reporter; + public UserId getReporterId() { + return reporterId; } - public ZonedDateTime getDateTime() { + public Instant getDateTime() { return dateTime; } diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/ReportService.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/ReportService.java index f6ed620..613248b 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/ReportService.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/ReportService.java @@ -1,9 +1,23 @@ package com.example.copsboot.report; -import com.example.copsboot.user.UserId; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.ZonedDateTime; -public interface ReportService { - Report createReport(UserId reporterId, ZonedDateTime dateTime, String description); +@Service +@Transactional +public class ReportService { + private final ReportRepository repository; + + public ReportService(ReportRepository repository) { + this.repository = repository; + } + + public Report createReport(CreateReportParameters parameters) { + return repository.save(new Report(repository.nextId(), + parameters.userId(), + parameters.dateTime(), + parameters.description())); + } } diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/ReportServiceImpl.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/ReportServiceImpl.java deleted file mode 100644 index 67c21e1..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/ReportServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserNotFoundException; -import com.example.copsboot.user.UserService; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.ZonedDateTime; - -@Service -@Transactional -public class ReportServiceImpl implements ReportService { - private final ReportRepository repository; - private final UserService userService; - - public ReportServiceImpl(ReportRepository repository, UserService userService) { - this.repository = repository; - this.userService = userService; - } - - @Override - public Report createReport(UserId reporterId, ZonedDateTime dateTime, String description) { - return repository.save(new Report(repository.nextId(), - userService.getUser(reporterId) - .orElseThrow(() -> new UserNotFoundException(reporterId)), - dateTime, - description)); - } -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java deleted file mode 100644 index 1fd3b6e..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.example.copsboot.report.web; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.ZonedDateTime; - -//tag::class[] -@Data -@AllArgsConstructor -@NoArgsConstructor -@ValidCreateReportParameters -public class CreateReportParameters { - private ZonedDateTime dateTime; - - @ValidReportDescription - private String description; - - private boolean trafficIncident; - private int numberOfInvolvedCars; -} -//end::class[] \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportParametersValidator.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportParametersValidator.java deleted file mode 100644 index 80c38ed..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportParametersValidator.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.report.web; - -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; - -//tag::class[] -public class CreateReportParametersValidator implements ConstraintValidator { //<1> - - @Override - public void initialize(ValidCreateReportParameters constraintAnnotation) { - } - - @Override - public boolean isValid(CreateReportParameters value, ConstraintValidatorContext context) { - boolean result = true; - if (value.isTrafficIncident() && value.getNumberOfInvolvedCars() <= 0) { //<2> - result = false; - } - return result; - } -} //end::class[] diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java new file mode 100644 index 0000000..c39c4e9 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java @@ -0,0 +1,16 @@ +package com.example.copsboot.report.web; + +import com.example.copsboot.report.CreateReportParameters; +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +@ValidCreateReportRequest +public record CreateReportRequest(Instant dateTime, + @ValidReportDescription String description, + boolean trafficIncident, + int numberOfInvolvedCars) { + public CreateReportParameters toParameters(UserId userId) { + return new CreateReportParameters(userId, dateTime, description); + } +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java new file mode 100644 index 0000000..fbad4ea --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java @@ -0,0 +1,21 @@ +package com.example.copsboot.report.web; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +//tag::class[] +public class CreateReportRequestValidator implements ConstraintValidator { //<1> + + @Override + public void initialize(ValidCreateReportRequest constraintAnnotation) { + } + + @Override + public boolean isValid(CreateReportRequest value, ConstraintValidatorContext context) { + boolean result = true; + if (value.trafficIncident() && value.numberOfInvolvedCars() <= 0) { //<2> + result = false; + } + return result; + } +} //end::class[] diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java index e16971d..aa30ca4 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; public class ReportDescriptionValidator implements ConstraintValidator { //<1> diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportDto.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportDto.java index 0adc7f8..28e606e 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportDto.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportDto.java @@ -2,23 +2,21 @@ import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; -import lombok.Value; +import com.example.copsboot.user.UserService; -import java.time.ZonedDateTime; +import java.time.Instant; //tag::class[] -@Value -public class ReportDto { - private ReportId id; - private String reporter; - private ZonedDateTime dateTime; - private String description; +public record ReportDto(ReportId id, + String reporter, + Instant dateTime, + String description) { - public static ReportDto fromReport(Report report) { + public static ReportDto fromReport(Report report, UserService userService) { return new ReportDto(report.getId(), - report.getReporter().getEmail(), - report.getDateTime(), - report.getDescription()); + userService.getUserById(report.getReporterId()).getEmail(), + report.getDateTime(), + report.getDescription()); } } //end::class[] diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportRestController.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportRestController.java index 28b79ae..83f9d54 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportRestController.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportRestController.java @@ -1,31 +1,42 @@ package com.example.copsboot.report.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportService; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserNotFoundException; +import com.example.copsboot.user.UserService; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.UUID; //tag::class[] @RestController @RequestMapping("/api/reports") public class ReportRestController { private final ReportService service; + private final UserService userService; - public ReportRestController(ReportService service) { + public ReportRestController(ReportService service, UserService userService) { this.service = service; + this.userService = userService; } @PostMapping @ResponseStatus(HttpStatus.CREATED) - public ReportDto createReport(@AuthenticationPrincipal ApplicationUserDetails userDetails, - @Valid @RequestBody CreateReportParameters parameters) { - return ReportDto.fromReport(service.createReport(userDetails.getUserId(), - parameters.getDateTime(), - parameters.getDescription())); + public ReportDto createReport(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateReportRequest request) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + User user = userService.findUserByAuthServerId(authServerId) + .orElseThrow(() -> new UserNotFoundException(authServerId)); + CreateReportParameters parameters = request.toParameters(user.getId()); + Report report = service.createReport(parameters); + return ReportDto.fromReport(report, userService); } } //end::class[] diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidCreateReportParameters.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java similarity index 67% rename from chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidCreateReportParameters.java rename to chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java index 1dc95fd..895ce6c 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidCreateReportParameters.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -10,11 +10,11 @@ //tag::class[] @Target(ElementType.TYPE) //<1> @Retention(RetentionPolicy.RUNTIME) -@Constraint(validatedBy = {CreateReportParametersValidator.class}) //<2> -public @interface ValidCreateReportParameters { +@Constraint(validatedBy = {CreateReportRequestValidator.class}) //<2> +public @interface ValidCreateReportRequest { String message() default "Invalid report"; Class[] groups() default {}; Class[] payload() default {}; -}//end::class[] \ No newline at end of file +}//end::class[] diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java index 41d39e9..ba8fa56 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -16,4 +16,4 @@ Class[] groups() default {}; //<5> Class[] payload() default {}; //<6> -} \ No newline at end of file +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/User.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/User.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java new file mode 100644 index 0000000..2a434e3 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class UserIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(UserId attribute) { + return attribute.getId(); + } + + @Override + public UserId convertToEntityAttribute(UUID dbData) { + return new UserId(dbData); + } +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserNotFoundException.java index 1f65f04..97d0813 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserNotFoundException.java @@ -3,9 +3,13 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> +@ResponseStatus(HttpStatus.NOT_FOUND) public class UserNotFoundException extends RuntimeException { public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); + super(String.format("Unable to find user with id %s", userId)); + } + + public UserNotFoundException(AuthServerId authServerId) { + super(String.format("Unable to find user with auth server id %s", authServerId)); } } diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserService.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..ec5aa13 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,33 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } + + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } - Optional getUser(UserId userId); + public User getUserById(UserId userId) { + return repository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + } + // end::createUser[] } diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..b87302d --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,17 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(@NotEmpty String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + String email = jwt.getClaimAsString("email"); + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/UserRestController.java index b5aa1a8..e0a6545 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,49 +1,53 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import lombok.Value; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Valid; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] + // tag::createUser[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { - User officer = service.createOfficer(parameters.getEmail(), - parameters.getPassword()); - return UserDto.fromUser(officer); + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/Entity.java b/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/Entity.java index a573e0e..3a45231 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/Entity.java @@ -1,6 +1,5 @@ package com.example.orm.jpa; -import java.io.Serializable; /** * Interface for entity objects. diff --git a/chapter08/04 - objectvalidation/src/main/resources/application-dev.properties b/chapter08/04 - objectvalidation/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter08/04 - objectvalidation/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/main/resources/application-local.properties b/chapter08/04 - objectvalidation/src/main/resources/application-local.properties index 8f13f3f..7e354d5 100644 --- a/chapter08/04 - objectvalidation/src/main/resources/application-local.properties +++ b/chapter08/04 - objectvalidation/src/main/resources/application-local.properties @@ -3,13 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +#spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter08/04 - objectvalidation/src/main/resources/application.properties b/chapter08/04 - objectvalidation/src/main/resources/application.properties index e69de29..3e80adf 100644 --- a/chapter08/04 - objectvalidation/src/main/resources/application.properties +++ b/chapter08/04 - objectvalidation/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/copsboot + +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter08/04 - objectvalidation/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter08/04 - objectvalidation/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter08/04 - objectvalidation/src/main/resources/db/migration/V1.0.0.2__reports.sql b/chapter08/04 - objectvalidation/src/main/resources/db/migration/V1.0.0.2__reports.sql new file mode 100644 index 0000000..cc2e26c --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/resources/db/migration/V1.0.0.2__reports.sql @@ -0,0 +1,8 @@ +CREATE TABLE report +( + date_time TIMESTAMP(6) WITH TIME ZONE, + id uuid NOT NULL, + description VARCHAR(255), + reporter_id uuid, + PRIMARY KEY (id) +); diff --git a/chapter08/04 - objectvalidation/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter08/04 - objectvalidation/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter08/04 - objectvalidation/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter08/04 - objectvalidation/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter08/04 - objectvalidation/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter08/04 - objectvalidation/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter08/04 - objectvalidation/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter08/04 - objectvalidation/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..5feb390 100644 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,16 @@ package com.example.copsboot; import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/Reports.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/Reports.java deleted file mode 100644 index 2302dc9..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/Reports.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.Users; - -import java.time.ZonedDateTime; -import java.util.UUID; - -public class Reports { - public static Report createRandomReport(String description) { - return new Report(new ReportId(UUID.randomUUID()), - Users.newRandomOfficer(), - ZonedDateTime.now(), - description); - } -} diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/CreateReportParametersValidatorTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/CreateReportParametersValidatorTest.java deleted file mode 100644 index bc37179..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/CreateReportParametersValidatorTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.example.copsboot.report.web; - - -import org.junit.Test; - -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.time.ZonedDateTime; -import java.util.Set; - -import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; - -public class CreateReportParametersValidatorTest { - //tag::invalid[] - @Test - public void givenTrafficIndicentButInvolvedCarsZero_invalid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), - "The suspect was wearing a black hat", - true, - 0); - Set> violationSet = validator.validate(parameters); - assertThat(violationSet).hasViolationOnPath(""); - } - //end::invalid[] - - //tag::valid[] - @Test - public void givenTrafficIndicent_involvedCarsMustBePositive() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), - "The suspect was wearing a black hat.", - true, - 2); - Set> violationSet = validator.validate(parameters); - assertThat(violationSet).hasNoViolations(); - } - //end::valid[] - - //tag::valid-no-cars[] - @Test - public void givenNoTrafficIndicent_involvedCarsDoesNotMatter() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), - "The suspect was wearing a black hat.", - false, - 0); - Set> violationSet = validator.validate(parameters); - assertThat(violationSet).hasNoViolations(); - } - //end::valid-no-cars[] -} \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java new file mode 100644 index 0000000..a6bb390 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java @@ -0,0 +1,63 @@ +package com.example.copsboot.report.web; + + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.Test; + +import java.time.Instant; +import java.util.Set; + +import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; + +public class CreateReportRequestValidatorTest { + //tag::invalid[] + @Test + public void givenTrafficIndicentButInvolvedCarsZero_invalid() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat", + true, + 0); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasViolationOnPath(""); + } + } + //end::invalid[] + + //tag::valid[] + @Test + public void givenTrafficIndicent_involvedCarsMustBePositive() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", + true, + 2); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } + } + //end::valid[] + + //tag::valid-no-cars[] + @Test + public void givenNoTrafficIndicent_involvedCarsDoesNotMatter() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", + false, + 0); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } + } + //end::valid-no-cars[] +} diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java index fe3a377..f40d47c 100644 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java @@ -1,12 +1,12 @@ package com.example.copsboot.report.web; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; import org.junit.Test; -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.time.ZonedDateTime; +import java.time.Instant; import java.util.Set; import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; @@ -16,26 +16,27 @@ public class ReportDescriptionValidatorTest { //tag::invalid[] @Test public void givenEmptyString_notValid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); //<1> - Validator validator = factory.getValidator(); //<2> + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { //<1> + Validator validator = factory.getValidator(); //<2> - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), "", false, 0); - Set> violationSet = validator.validate(parameters); //<3> - assertThat(violationSet).hasViolationOnPath("description"); //<4> + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), "", false, 0); + Set> violationSet = validator.validate(parameters); //<3> + assertThat(violationSet).hasViolationOnPath("description"); //<4> + } } //end::invalid[] //tag::valid[] @Test public void givenSuspectWordPresent_valid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), - "The suspect was wearing a black hat.", - false, 0); - Set> violationSet = validator.validate(parameters); - assertThat(violationSet).hasNoViolations(); + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", false, 0); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } } //end::valid[] -} \ No newline at end of file +} diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java index d46c329..d6c6e5f 100644 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java @@ -1,63 +1,76 @@ package com.example.copsboot.report.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; import com.example.copsboot.report.ReportService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.time.ZonedDateTime; +import java.time.Instant; +import java.util.Optional; import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; //tag::class[] -@RunWith(SpringRunner.class) @CopsbootControllerTest(ReportRestController.class) public class ReportRestControllerTest { @Autowired - private MockMvc mvc; - - @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; @MockBean private ReportService service; + @MockBean + private UserService userService; @Test public void officerIsAbleToPostAReport() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - ZonedDateTime dateTime = ZonedDateTime.parse("2018-04-11T22:59:03.189+02:00"); - String description = "The suspect is wearing a black hat."; - CreateReportParameters parameters = new CreateReportParameters(dateTime, - description, - false, - 0); - when(service.createReport(eq(Users.officer().getId()), any(ZonedDateTime.class), eq(description))) - .thenReturn(new Report(new ReportId(UUID.randomUUID()), Users.officer(), dateTime, description)); - mvc.perform(post("/api/reports") - .header(HEADER_AUTHORIZATION, bearer(accessToken)) - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("reporter").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("dateTime").value("2018-04-11T22:59:03.189+02:00")) - .andExpect(jsonPath("description").value(description)); + UserId userId = new UserId(UUID.randomUUID()); + AuthServerId authServerId = new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); + User user = new User(userId, + "wim@example.com", + authServerId, + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"); + when(userService.findUserByAuthServerId(authServerId)) + .thenReturn(Optional.of(user)); + when(userService.getUserById(userId)) + .thenReturn(user); + when(service.createReport(any(CreateReportParameters.class))) + .thenReturn(new Report(new ReportId(UUID.randomUUID()), + userId, + Instant.parse("2023-04-11T22:59:03.189+02:00"), + "This is a test report description. The suspect was wearing a black hat.")); + mockMvc.perform(post("/api/reports") + .with(jwt().jwt(builder -> builder.subject(authServerId.value().toString()) + .claim("email", "wim@example.com")) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "dateTime": "2023-04-11T22:59:03.189+02:00", + "description": "This is a test report description. The suspect was wearing a black hat." + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("id").exists()) + .andExpect(jsonPath("reporter").value("wim@example.com")) + .andExpect(jsonPath("dateTime").value("2023-04-11T20:59:03.189Z")) + .andExpect(jsonPath("description").value("This is a test report description. The suspect was wearing a black hat.")); } } -//end::class[] \ No newline at end of file +//end::class[] diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java deleted file mode 100644 index 720f959..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.copsboot.user; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.HashSet; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> -@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) //<2> -public class UserRepositoryIntegrationTest { - @Autowired - private UserRepository repository; - @PersistenceContext - private EntityManager entityManager; - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void testSaveUser() { - Set roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); - - assertThat(repository.count()).isEqualTo(1L); - - entityManager.flush(); //<3> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM user_roles", Long.class)).isEqualTo(1L); - assertThat(jdbcTemplate.queryForObject("SELECT roles FROM user_roles", String.class)).isEqualTo("OFFICER"); - } -} diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..19c23fe 100644 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -3,14 +3,16 @@ import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,62 +21,34 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) //<2> public class UserRepositoryTest { @Autowired private UserRepository repository; + @PersistenceContext + private EntityManager entityManager; + @Autowired + private JdbcTemplate jdbcTemplate; - //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } + assertThat(repository.count()).isEqualTo(1L); - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); + entityManager.flush(); //<3> - assertThat(optional).isEmpty(); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> + assertThat(jdbcTemplate.queryForObject("SELECT email FROM copsboot_user", String.class)).isEqualTo("alex.foley@beverly-hills.com"); } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @Bean @@ -82,5 +56,4 @@ public UniqueIdGenerator generator() { return new InMemoryUniqueIdGenerator(); } } - //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/Users.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index e0d24b0..805c501 100644 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,133 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index eb07c50..a20d744 100644 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,118 +1,104 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } + // tag::emptyToken[] @Test - public void testCreateOfficerIfPasswordIsTooShort() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("errors[0].fieldName").value("password")); - - verify(service, never()).createOfficer(email, password); + void givenEmptyMobileToken_badRequestIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "" + } + """)) //<.> + .andExpect(status().isBadRequest()) //<.> + .andDo(print()); //<.> + + verify(userService, never()).createUser(any(CreateUserParameters.class)); //<.> } + // end::emptyToken[] } diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java index 77c5f4c..21556a5 100644 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java @@ -2,7 +2,8 @@ import org.assertj.core.api.AbstractAssert; -import javax.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolation; + import java.util.Set; import java.util.stream.Collectors; @@ -21,12 +22,12 @@ public ConstraintViolationSetAssert hasViolationOnPath(String path) { // check condition if (!containsViolationWithPath(actual, path)) { failWithMessage("There was no violation with path <%s>. Violation paths: <%s>", path, actual.stream() - .map(violation -> violation - .getPropertyPath() - .toString()) - .collect( - Collectors - .toList())); + .map(violation -> violation + .getPropertyPath() + .toString()) + .collect( + Collectors + .toList())); } return this; diff --git a/chapter08/04 - objectvalidation/src/test/resources/application-integration-test.properties b/chapter08/04 - objectvalidation/src/test/resources/application-integration-test.properties index 159536c..c61e563 100644 --- a/chapter08/04 - objectvalidation/src/test/resources/application-integration-test.properties +++ b/chapter08/04 - objectvalidation/src/test/resources/application-integration-test.properties @@ -1,11 +1,6 @@ -spring.datasource.url=jdbc:tc:postgresql://localhost/copsbootdb +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.username=user spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none - -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/04 - objectvalidation/src/test/resources/application-repository-test.properties b/chapter08/04 - objectvalidation/src/test/resources/application-repository-test.properties new file mode 100644 index 0000000..c61e563 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/test/resources/application-repository-test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=user +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/04 - objectvalidation/src/test/resources/application-test.properties b/chapter08/04 - objectvalidation/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter08/04 - objectvalidation/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/test/resources/logback-test.xml b/chapter08/04 - objectvalidation/src/test/resources/logback-test.xml index bf47fec..164429c 100644 --- a/chapter08/04 - objectvalidation/src/test/resources/logback-test.xml +++ b/chapter08/04 - objectvalidation/src/test/resources/logback-test.xml @@ -5,7 +5,7 @@ - + @@ -17,14 +17,8 @@ - - - - - - - \ No newline at end of file + diff --git a/chapter08/05 - validatorspringbean/.mvn/wrapper/maven-wrapper.jar b/chapter08/05 - validatorspringbean/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter08/05 - validatorspringbean/.mvn/wrapper/maven-wrapper.properties b/chapter08/05 - validatorspringbean/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter08/05 - validatorspringbean/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter08/05 - validatorspringbean/docker-compose.yaml b/chapter08/05 - validatorspringbean/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter08/05 - validatorspringbean/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter08/05 - validatorspringbean/mvnw b/chapter08/05 - validatorspringbean/mvnw index 5bf251c..66df285 100755 --- a/chapter08/05 - validatorspringbean/mvnw +++ b/chapter08/05 - validatorspringbean/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter08/05 - validatorspringbean/mvnw.cmd b/chapter08/05 - validatorspringbean/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter08/05 - validatorspringbean/mvnw.cmd +++ b/chapter08/05 - validatorspringbean/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter08/05 - validatorspringbean/pom.xml b/chapter08/05 - validatorspringbean/pom.xml index 468a4d0..43db322 100644 --- a/chapter08/05 - validatorspringbean/pom.xml +++ b/chapter08/05 - validatorspringbean/pom.xml @@ -1,231 +1,208 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 27.1-jre + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 2.0.3.RELEASE - 1.11.2 - - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - org.postgresql - postgresql - - - org.flywaydb - flyway-core - + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter08/05 - validatorspringbean/src/docs/asciidoc/Copsboot REST API Guide.adoc b/chapter08/05 - validatorspringbean/src/docs/asciidoc/Copsboot REST API Guide.adoc new file mode 100644 index 0000000..b0b91ae --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -0,0 +1,14 @@ += Copsboot REST API Guide +:icons: font +:toc: +:toclevels: 2 + +:numbered: + +== Introduction + +The Copsboot project uses a REST API for interfacing with the server. + +This documentation covers version {project-version} of the application. + +include::_users.adoc[] diff --git a/chapter08/05 - validatorspringbean/src/docs/asciidoc/_users.adoc b/chapter08/05 - validatorspringbean/src/docs/asciidoc/_users.adoc new file mode 100644 index 0000000..2becf75 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/docs/asciidoc/_users.adoc @@ -0,0 +1,24 @@ +//tag::initial-doc[] +== User Management + +=== User information + +The API allows to get information on the currently logged on user +via a `GET` on `/api/users/me`. If you are not a logged on user, the +following response will be returned: + +operation::own-details-unauthorized[snippets='http-request,http-response'] +//end::initial-doc[] + +If you do log on as a user, you get more information on that user: + +operation::own-details[snippets='http-request,http-response,response-fields'] + + +//tag::create-user[] +=== Create a user + +To create an new user, do a `POST` on `/api/users`: + +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter08/05 - validatorspringbean/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter08/05 - validatorspringbean/src/main/asciidoc/Copsboot REST API Guide.adoc deleted file mode 100644 index 255bc8e..0000000 --- a/chapter08/05 - validatorspringbean/src/main/asciidoc/Copsboot REST API Guide.adoc +++ /dev/null @@ -1,14 +0,0 @@ -= Copsboot REST API Guide -:icons: font -:toc: -:toclevels: 2 - -:numbered: - -== Introduction - -The Copsboot project uses a REST API for interfacing with the server. - -This documentation covers version {project-version} of the application. - -include::_users.adoc[] \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/main/asciidoc/_users.adoc b/chapter08/05 - validatorspringbean/src/main/asciidoc/_users.adoc deleted file mode 100644 index a033db8..0000000 --- a/chapter08/05 - validatorspringbean/src/main/asciidoc/_users.adoc +++ /dev/null @@ -1,24 +0,0 @@ -//tag::initial-doc[] -== User Management - -=== User information - -The API allows to get information on the currently logged on user -via a `GET` on `/api/users/me`. If you are not a logged on user, the -following response will be returned: - -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] -//end::initial-doc[] - -If you do log on as a user, you get more information on that user: - -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] - - -//tag::create-user[] -=== Create a user - -To create an new user, do a `POST` on `/api/users`: - -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java index 344a5fe..fb1cc59 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java @@ -6,6 +6,7 @@ public final class SpringProfiles { public static final String TEST = "test"; public static final String STAGING = "staging"; public static final String PROD = "prod"; + public static final String REPOSITORY_TEST = "repository-test"; public static final String INTEGRATION_TEST = "integration-test"; private SpringProfiles() { diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java index 9c92c49..8d26775 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java @@ -1,11 +1,4 @@ package com.example.copsboot.infrastructure.mvc; -import lombok.Value; - -//tag::class[] -@Value -public class FieldErrorResponse { - private String fieldName; - private String errorMessage; +public record FieldErrorResponse(String fieldName, String errorMesesage) { } -//end::class[] \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/CreateReportParameters.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/CreateReportParameters.java new file mode 100644 index 0000000..64aeea6 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/CreateReportParameters.java @@ -0,0 +1,8 @@ +package com.example.copsboot.report; + +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +public record CreateReportParameters(UserId userId, Instant dateTime, String description) { +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/Report.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/Report.java index c0f9c66..b10756f 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/Report.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/Report.java @@ -1,36 +1,36 @@ package com.example.copsboot.report; -import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; import com.example.orm.jpa.AbstractEntity; import com.example.util.ArtifactForFramework; +import jakarta.persistence.Entity; + +import java.time.Instant; -import javax.persistence.Entity; -import javax.persistence.ManyToOne; -import java.time.ZonedDateTime; //tag::class[] @Entity public class Report extends AbstractEntity { - @ManyToOne - private User reporter; - private ZonedDateTime dateTime; + + private UserId reporterId; + private Instant dateTime; private String description; @ArtifactForFramework protected Report() { } - public Report(ReportId id, User reporter, ZonedDateTime dateTime, String description) { + public Report(ReportId id, UserId reporterId, Instant dateTime, String description) { super(id); - this.reporter = reporter; + this.reporterId = reporterId; this.dateTime = dateTime; this.description = description; } - public User getReporter() { - return reporter; + public UserId getReporterId() { + return reporterId; } - public ZonedDateTime getDateTime() { + public Instant getDateTime() { return dateTime; } diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/ReportService.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/ReportService.java index f6ed620..613248b 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/ReportService.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/ReportService.java @@ -1,9 +1,23 @@ package com.example.copsboot.report; -import com.example.copsboot.user.UserId; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.ZonedDateTime; -public interface ReportService { - Report createReport(UserId reporterId, ZonedDateTime dateTime, String description); +@Service +@Transactional +public class ReportService { + private final ReportRepository repository; + + public ReportService(ReportRepository repository) { + this.repository = repository; + } + + public Report createReport(CreateReportParameters parameters) { + return repository.save(new Report(repository.nextId(), + parameters.userId(), + parameters.dateTime(), + parameters.description())); + } } diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/ReportServiceImpl.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/ReportServiceImpl.java deleted file mode 100644 index 67c21e1..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/ReportServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserNotFoundException; -import com.example.copsboot.user.UserService; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.ZonedDateTime; - -@Service -@Transactional -public class ReportServiceImpl implements ReportService { - private final ReportRepository repository; - private final UserService userService; - - public ReportServiceImpl(ReportRepository repository, UserService userService) { - this.repository = repository; - this.userService = userService; - } - - @Override - public Report createReport(UserId reporterId, ZonedDateTime dateTime, String description) { - return repository.save(new Report(repository.nextId(), - userService.getUser(reporterId) - .orElseThrow(() -> new UserNotFoundException(reporterId)), - dateTime, - description)); - } -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java deleted file mode 100644 index 9a169e4..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.report.web; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.ZonedDateTime; - -//tag::class[] -@Data -@AllArgsConstructor -@NoArgsConstructor -public class CreateReportParameters { - private ZonedDateTime dateTime; - - @ValidReportDescription - private String description; -} -//end::class[] \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java new file mode 100644 index 0000000..6a0ce81 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java @@ -0,0 +1,14 @@ +package com.example.copsboot.report.web; + +import com.example.copsboot.report.CreateReportParameters; +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +@ValidCreateReportRequest +public record CreateReportRequest(Instant dateTime, @ValidReportDescription String description, + boolean trafficIncident, int numberOfInvolvedCars) { + public CreateReportParameters toParameters(UserId userId) { + return new CreateReportParameters(userId, dateTime, description); + } +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java new file mode 100644 index 0000000..fbad4ea --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java @@ -0,0 +1,21 @@ +package com.example.copsboot.report.web; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +//tag::class[] +public class CreateReportRequestValidator implements ConstraintValidator { //<1> + + @Override + public void initialize(ValidCreateReportRequest constraintAnnotation) { + } + + @Override + public boolean isValid(CreateReportRequest value, ConstraintValidatorContext context) { + boolean result = true; + if (value.trafficIncident() && value.numberOfInvolvedCars() <= 0) { //<2> + result = false; + } + return result; + } +} //end::class[] diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java index e16971d..aa30ca4 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; public class ReportDescriptionValidator implements ConstraintValidator { //<1> diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportDto.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportDto.java index 0adc7f8..28e606e 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportDto.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportDto.java @@ -2,23 +2,21 @@ import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; -import lombok.Value; +import com.example.copsboot.user.UserService; -import java.time.ZonedDateTime; +import java.time.Instant; //tag::class[] -@Value -public class ReportDto { - private ReportId id; - private String reporter; - private ZonedDateTime dateTime; - private String description; +public record ReportDto(ReportId id, + String reporter, + Instant dateTime, + String description) { - public static ReportDto fromReport(Report report) { + public static ReportDto fromReport(Report report, UserService userService) { return new ReportDto(report.getId(), - report.getReporter().getEmail(), - report.getDateTime(), - report.getDescription()); + userService.getUserById(report.getReporterId()).getEmail(), + report.getDateTime(), + report.getDescription()); } } //end::class[] diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportRestController.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportRestController.java index 1f4eb4b..3fcc153 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportRestController.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportRestController.java @@ -1,33 +1,45 @@ package com.example.copsboot.report.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportService; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserNotFoundException; +import com.example.copsboot.user.UserService; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.UUID; //tag::class[] @RestController @RequestMapping("/api/reports") public class ReportRestController { private final ReportService service; + private final UserService userService; - public ReportRestController(ReportService service) { + public ReportRestController(ReportService service, UserService userService) { this.service = service; + this.userService = userService; } - //tag::create-report-method-signature[] + + // tag::create-report-method-signature[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public ReportDto createReport(@AuthenticationPrincipal ApplicationUserDetails userDetails, - @Valid @RequestBody CreateReportParameters parameters) { - //end::create-report-method-signature[] - return ReportDto.fromReport(service.createReport(userDetails.getUserId(), - parameters.getDateTime(), - parameters.getDescription())); + public ReportDto createReport(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateReportRequest request) { + // end::create-report-method-signature[] + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + User user = userService.findUserByAuthServerId(authServerId) + .orElseThrow(() -> new UserNotFoundException(authServerId)); + CreateReportParameters parameters = request.toParameters(user.getId()); + Report report = service.createReport(parameters); + return ReportDto.fromReport(report, userService); } } //end::class[] diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java new file mode 100644 index 0000000..895ce6c --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java @@ -0,0 +1,20 @@ +package com.example.copsboot.report.web; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +//tag::class[] +@Target(ElementType.TYPE) //<1> +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {CreateReportRequestValidator.class}) //<2> +public @interface ValidCreateReportRequest { + String message() default "Invalid report"; + + Class[] groups() default {}; + + Class[] payload() default {}; +}//end::class[] diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java index 41d39e9..ba8fa56 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -16,4 +16,4 @@ Class[] groups() default {}; //<5> Class[] payload() default {}; //<6> -} \ No newline at end of file +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/User.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/User.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java new file mode 100644 index 0000000..2a434e3 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class UserIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(UserId attribute) { + return attribute.getId(); + } + + @Override + public UserId convertToEntityAttribute(UUID dbData) { + return new UserId(dbData); + } +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserNotFoundException.java index 1f65f04..97d0813 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserNotFoundException.java @@ -3,9 +3,13 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> +@ResponseStatus(HttpStatus.NOT_FOUND) public class UserNotFoundException extends RuntimeException { public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); + super(String.format("Unable to find user with id %s", userId)); + } + + public UserNotFoundException(AuthServerId authServerId) { + super(String.format("Unable to find user with auth server id %s", authServerId)); } } diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..741d2e0 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,11 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); + + Optional findByMobileToken(String mobileToken); } //end::class[] diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserService.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserService.java index d5630f0..ba1d4ab 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserService.java @@ -1,11 +1,37 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } + + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } - Optional getUser(UserId userId); + public User getUserById(UserId userId) { + return repository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + } - Optional findUserByEmail(String email); + public Optional findUserByMobileToken(String mobileToken) { + return repository.findByMobileToken(mobileToken); + } + // end::createUser[] } diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 6918081..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } - - @Override - public Optional findUserByEmail(String email) { - return repository.findByEmailIgnoreCase(email); - } -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index f96ee54..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -@ValidCreateUserParameters -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java deleted file mode 100644 index 3f86d70..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; - -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; - -//tag::class[] -public class CreateUserParametersValidator implements ConstraintValidator { - - private final UserService userService; - - @Autowired - public CreateUserParametersValidator(UserService userService) { //<1> - this.userService = userService; - } - - @Override - public void initialize(ValidCreateUserParameters constraintAnnotation) { - - } - - @Override - public boolean isValid(CreateOfficerParameters userParameters, ConstraintValidatorContext context) { - - boolean result = true; - - if (userService.findUserByEmail(userParameters.getEmail()).isPresent()) { //<2> - context.buildConstraintViolationWithTemplate( - "There is already a user with the given email address.") - .addPropertyNode("email").addConstraintViolation(); //<3> - - result = false; //<4> - } - - return result; - } -} -//end::class[] diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..83c56a1 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,18 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +@ValidCreateUserRequest +public record CreateUserRequest(@NotEmpty String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + String email = jwt.getClaimAsString("email"); + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java new file mode 100644 index 0000000..bdd4aa5 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java @@ -0,0 +1,40 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.UserService; +import org.springframework.beans.factory.annotation.Autowired; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +//tag::class[] +public class CreateUserRequestValidator implements ConstraintValidator { + + private final UserService userService; + + @Autowired + public CreateUserRequestValidator(UserService userService) { //<1> + this.userService = userService; + } + + @Override + public void initialize(ValidCreateUserRequest constraintAnnotation) { + + } + + @Override + public boolean isValid(CreateUserRequest userRequest, ConstraintValidatorContext context) { + + boolean result = true; + + if (userService.findUserByMobileToken(userRequest.mobileToken()).isPresent()) { //<2> + context.buildConstraintViolationWithTemplate( + "There is already a user with the given mobile token.") + .addPropertyNode("mobileToken").addConstraintViolation(); //<3> + + result = false; //<4> + } + + return result; + } +} +//end::class[] diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/UserRestController.java index b5aa1a8..e0a6545 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,49 +1,53 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import lombok.Value; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Valid; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] + // tag::createUser[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { - User officer = service.createOfficer(parameters.getEmail(), - parameters.getPassword()); - return UserDto.fromUser(officer); + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java similarity index 70% rename from chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java rename to chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java index a7ec388..e6a975e 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java @@ -1,7 +1,7 @@ package com.example.copsboot.user.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -10,8 +10,8 @@ //tag::class[] @Target(ElementType.TYPE) // <1> @Retention(RetentionPolicy.RUNTIME) -@Constraint(validatedBy = {CreateUserParametersValidator.class}) //<2> -public @interface ValidCreateUserParameters { +@Constraint(validatedBy = {CreateUserRequestValidator.class}) //<2> +public @interface ValidCreateUserRequest { String message() default "Invalid user"; Class[] groups() default {}; diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/Entity.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/Entity.java index 96cadf0..3a45231 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/Entity.java @@ -1,5 +1,6 @@ package com.example.orm.jpa; + /** * Interface for entity objects. * diff --git a/chapter08/05 - validatorspringbean/src/main/resources/application-dev.properties b/chapter08/05 - validatorspringbean/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter08/05 - validatorspringbean/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/main/resources/application-local.properties b/chapter08/05 - validatorspringbean/src/main/resources/application-local.properties index 8f13f3f..7e354d5 100644 --- a/chapter08/05 - validatorspringbean/src/main/resources/application-local.properties +++ b/chapter08/05 - validatorspringbean/src/main/resources/application-local.properties @@ -3,13 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +#spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter08/05 - validatorspringbean/src/main/resources/application.properties b/chapter08/05 - validatorspringbean/src/main/resources/application.properties index e69de29..3e80adf 100644 --- a/chapter08/05 - validatorspringbean/src/main/resources/application.properties +++ b/chapter08/05 - validatorspringbean/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/copsboot + +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter08/05 - validatorspringbean/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter08/05 - validatorspringbean/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter08/05 - validatorspringbean/src/main/resources/db/migration/V1.0.0.2__reports.sql b/chapter08/05 - validatorspringbean/src/main/resources/db/migration/V1.0.0.2__reports.sql new file mode 100644 index 0000000..cc2e26c --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/resources/db/migration/V1.0.0.2__reports.sql @@ -0,0 +1,8 @@ +CREATE TABLE report +( + date_time TIMESTAMP(6) WITH TIME ZONE, + id uuid NOT NULL, + description VARCHAR(255), + reporter_id uuid, + PRIMARY KEY (id) +); diff --git a/chapter08/05 - validatorspringbean/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter08/05 - validatorspringbean/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter08/05 - validatorspringbean/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter08/05 - validatorspringbean/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter08/05 - validatorspringbean/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter08/05 - validatorspringbean/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter08/05 - validatorspringbean/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter08/05 - validatorspringbean/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..5feb390 100644 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,16 @@ package com.example.copsboot; import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/Reports.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/Reports.java deleted file mode 100644 index 2302dc9..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/Reports.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.Users; - -import java.time.ZonedDateTime; -import java.util.UUID; - -public class Reports { - public static Report createRandomReport(String description) { - return new Report(new ReportId(UUID.randomUUID()), - Users.newRandomOfficer(), - ZonedDateTime.now(), - description); - } -} diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java new file mode 100644 index 0000000..a6bb390 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java @@ -0,0 +1,63 @@ +package com.example.copsboot.report.web; + + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.Test; + +import java.time.Instant; +import java.util.Set; + +import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; + +public class CreateReportRequestValidatorTest { + //tag::invalid[] + @Test + public void givenTrafficIndicentButInvolvedCarsZero_invalid() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat", + true, + 0); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasViolationOnPath(""); + } + } + //end::invalid[] + + //tag::valid[] + @Test + public void givenTrafficIndicent_involvedCarsMustBePositive() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", + true, + 2); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } + } + //end::valid[] + + //tag::valid-no-cars[] + @Test + public void givenNoTrafficIndicent_involvedCarsDoesNotMatter() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", + false, + 0); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } + } + //end::valid-no-cars[] +} diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java index 199247a..f40d47c 100644 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java @@ -1,12 +1,12 @@ package com.example.copsboot.report.web; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; import org.junit.Test; -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.time.ZonedDateTime; +import java.time.Instant; import java.util.Set; import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; @@ -16,25 +16,27 @@ public class ReportDescriptionValidatorTest { //tag::invalid[] @Test public void givenEmptyString_notValid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); //<1> - Validator validator = factory.getValidator(); //<2> + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { //<1> + Validator validator = factory.getValidator(); //<2> - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), ""); - Set> violationSet = validator.validate(parameters); //<3> - assertThat(violationSet).hasViolationOnPath("description"); //<4> + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), "", false, 0); + Set> violationSet = validator.validate(parameters); //<3> + assertThat(violationSet).hasViolationOnPath("description"); //<4> + } } //end::invalid[] //tag::valid[] @Test public void givenSuspectWordPresent_valid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), - "The suspect was wearing a black hat."); - Set> violationSet = validator.validate(parameters); - assertThat(violationSet).hasNoViolations(); + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", false, 0); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } } //end::valid[] -} \ No newline at end of file +} diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java index 49705e9..d6c6e5f 100644 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java @@ -1,61 +1,76 @@ package com.example.copsboot.report.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; import com.example.copsboot.report.ReportService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.time.ZonedDateTime; +import java.time.Instant; +import java.util.Optional; import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; //tag::class[] -@RunWith(SpringRunner.class) @CopsbootControllerTest(ReportRestController.class) public class ReportRestControllerTest { @Autowired - private MockMvc mvc; - - @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; @MockBean private ReportService service; + @MockBean + private UserService userService; @Test public void officerIsAbleToPostAReport() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - ZonedDateTime dateTime = ZonedDateTime.parse("2018-04-11T22:59:03.189+02:00"); - String description = "The suspect is wearing a black hat."; - CreateReportParameters parameters = new CreateReportParameters(dateTime, - description); - when(service.createReport(eq(Users.officer().getId()), any(ZonedDateTime.class), eq(description))) - .thenReturn(new Report(new ReportId(UUID.randomUUID()), Users.officer(), dateTime, description)); - mvc.perform(post("/api/reports") - .header(HEADER_AUTHORIZATION, bearer(accessToken)) - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("reporter").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("dateTime").value("2018-04-11T22:59:03.189+02:00")) - .andExpect(jsonPath("description").value(description)); + UserId userId = new UserId(UUID.randomUUID()); + AuthServerId authServerId = new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); + User user = new User(userId, + "wim@example.com", + authServerId, + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"); + when(userService.findUserByAuthServerId(authServerId)) + .thenReturn(Optional.of(user)); + when(userService.getUserById(userId)) + .thenReturn(user); + when(service.createReport(any(CreateReportParameters.class))) + .thenReturn(new Report(new ReportId(UUID.randomUUID()), + userId, + Instant.parse("2023-04-11T22:59:03.189+02:00"), + "This is a test report description. The suspect was wearing a black hat.")); + mockMvc.perform(post("/api/reports") + .with(jwt().jwt(builder -> builder.subject(authServerId.value().toString()) + .claim("email", "wim@example.com")) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "dateTime": "2023-04-11T22:59:03.189+02:00", + "description": "This is a test report description. The suspect was wearing a black hat." + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("id").exists()) + .andExpect(jsonPath("reporter").value("wim@example.com")) + .andExpect(jsonPath("dateTime").value("2023-04-11T20:59:03.189Z")) + .andExpect(jsonPath("description").value("This is a test report description. The suspect was wearing a black hat.")); } } -//end::class[] \ No newline at end of file +//end::class[] diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java deleted file mode 100644 index 720f959..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.copsboot.user; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.HashSet; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> -@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) //<2> -public class UserRepositoryIntegrationTest { - @Autowired - private UserRepository repository; - @PersistenceContext - private EntityManager entityManager; - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void testSaveUser() { - Set roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); - - assertThat(repository.count()).isEqualTo(1L); - - entityManager.flush(); //<3> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM user_roles", Long.class)).isEqualTo(1L); - assertThat(jdbcTemplate.queryForObject("SELECT roles FROM user_roles", String.class)).isEqualTo("OFFICER"); - } -} diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..19c23fe 100644 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -3,14 +3,16 @@ import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,62 +21,34 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) //<2> public class UserRepositoryTest { @Autowired private UserRepository repository; + @PersistenceContext + private EntityManager entityManager; + @Autowired + private JdbcTemplate jdbcTemplate; - //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } + assertThat(repository.count()).isEqualTo(1L); - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); + entityManager.flush(); //<3> - assertThat(optional).isEmpty(); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> + assertThat(jdbcTemplate.queryForObject("SELECT email FROM copsboot_user", String.class)).isEqualTo("alex.foley@beverly-hills.com"); } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @Bean @@ -82,5 +56,4 @@ public UniqueIdGenerator generator() { return new InMemoryUniqueIdGenerator(); } } - //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/Users.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java deleted file mode 100644 index 7b94df3..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserService; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.validation.ConstraintViolation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; - -import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; -import static org.mockito.Mockito.when; - -//tag::class[] -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@ActiveProfiles(SpringProfiles.TEST) -public class CreateUserParametersValidatorTest { - - @MockBean - private UserService userService; //<2> - @Autowired - private PasswordEncoder encoder; - @Autowired - private ValidatorFactory factory; //<3> - - @Test - public void invalidIfAlreadyUserWithGivenEmail() { - - String email = "wim.deblauwe@example.com"; - when(userService.findUserByEmail(email)) - .thenReturn(Optional.of( - User.createOfficer(new UserId(UUID.randomUUID()), - email, - encoder.encode("testing1234")))); - - Validator validator = factory.getValidator(); //<4> - - CreateOfficerParameters userParameters = new CreateOfficerParameters(); - userParameters.setEmail(email); - userParameters.setPassword("my-secret-pwd-1234"); - Set> violationSet = validator.validate(userParameters); //<5> - assertThat(violationSet).hasViolationSize(2) - .hasViolationOnPath("email"); //<6> - } - - @Test - public void validIfNoUserWithGivenEmail() { - String email = "wim.deblauwe@example.com"; - when(userService.findUserByEmail(email)) - .thenReturn(Optional.empty()); - - Validator validator = factory.getValidator(); - - CreateOfficerParameters userParameters = new CreateOfficerParameters(); - userParameters.setEmail(email); - userParameters.setPassword("my-secret-pwd-1234"); - Set> violationSet = validator.validate(userParameters); - assertThat(violationSet).hasNoViolations(); - } -} -//end::class[] \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java new file mode 100644 index 0000000..d058abd --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java @@ -0,0 +1,65 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.infrastructure.SpringProfiles; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; +import static org.mockito.Mockito.when; + +//tag::class[] +@SpringBootTest //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) +public class CreateUserRequestValidatorTest { + + @MockBean + private UserService userService; //<2> + @Autowired + private ValidatorFactory factory; //<3> + + @Test + public void invalidIfAlreadyUserWithGivenMobileToken() { + + String mobileToken = "abc123"; + when(userService.findUserByMobileToken(mobileToken)) + .thenReturn(Optional.of(new User(new UserId(UUID.randomUUID()), + "wim@example.com", + new AuthServerId(UUID.randomUUID()), + mobileToken))); + + Validator validator = factory.getValidator(); //<4> + + CreateUserRequest request = new CreateUserRequest(mobileToken); + Set> violationSet = validator.validate(request); //<5> + assertThat(violationSet).hasViolationSize(2) + .hasViolationOnPath("mobileToken"); //<6> + } + + @Test + public void validIfNoUserWithGivenMobileToken() { + String mobileToken = "abc123"; + when(userService.findUserByMobileToken(mobileToken)) + .thenReturn(Optional.empty()); + + Validator validator = factory.getValidator(); + + CreateUserRequest request = new CreateUserRequest(mobileToken); + Set> violationSet = validator.validate(request); + assertThat(violationSet).hasNoViolations(); + } +} +//end::class[] diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index b1c3165..805c501 100644 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,134 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index f55e6c6..a20d744 100644 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,121 +1,104 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } + // tag::emptyToken[] @Test - public void testCreateOfficerIfPasswordIsTooShort() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("errors[0].fieldName").value("password")); - - verify(service, never()).createOfficer(email, password); + void givenEmptyMobileToken_badRequestIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "" + } + """)) //<.> + .andExpect(status().isBadRequest()) //<.> + .andDo(print()); //<.> + + verify(userService, never()).createUser(any(CreateUserParameters.class)); //<.> } + // end::emptyToken[] } diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java index 77c5f4c..21556a5 100644 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java @@ -2,7 +2,8 @@ import org.assertj.core.api.AbstractAssert; -import javax.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolation; + import java.util.Set; import java.util.stream.Collectors; @@ -21,12 +22,12 @@ public ConstraintViolationSetAssert hasViolationOnPath(String path) { // check condition if (!containsViolationWithPath(actual, path)) { failWithMessage("There was no violation with path <%s>. Violation paths: <%s>", path, actual.stream() - .map(violation -> violation - .getPropertyPath() - .toString()) - .collect( - Collectors - .toList())); + .map(violation -> violation + .getPropertyPath() + .toString()) + .collect( + Collectors + .toList())); } return this; diff --git a/chapter08/05 - validatorspringbean/src/test/resources/application-integration-test.properties b/chapter08/05 - validatorspringbean/src/test/resources/application-integration-test.properties index 159536c..c61e563 100644 --- a/chapter08/05 - validatorspringbean/src/test/resources/application-integration-test.properties +++ b/chapter08/05 - validatorspringbean/src/test/resources/application-integration-test.properties @@ -1,11 +1,6 @@ -spring.datasource.url=jdbc:tc:postgresql://localhost/copsbootdb +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.username=user spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none - -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/05 - validatorspringbean/src/test/resources/application-repository-test.properties b/chapter08/05 - validatorspringbean/src/test/resources/application-repository-test.properties new file mode 100644 index 0000000..c61e563 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/test/resources/application-repository-test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=user +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/05 - validatorspringbean/src/test/resources/application-test.properties b/chapter08/05 - validatorspringbean/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter08/05 - validatorspringbean/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/test/resources/logback-test.xml b/chapter08/05 - validatorspringbean/src/test/resources/logback-test.xml index bf47fec..164429c 100644 --- a/chapter08/05 - validatorspringbean/src/test/resources/logback-test.xml +++ b/chapter08/05 - validatorspringbean/src/test/resources/logback-test.xml @@ -5,7 +5,7 @@ - + @@ -17,14 +17,8 @@ - - - - - - - \ No newline at end of file + diff --git a/chapter09/01 - fileupload/.mvn/wrapper/maven-wrapper.jar b/chapter09/01 - fileupload/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter09/01 - fileupload/.mvn/wrapper/maven-wrapper.properties b/chapter09/01 - fileupload/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter09/01 - fileupload/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter09/01 - fileupload/docker-compose.yaml b/chapter09/01 - fileupload/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter09/01 - fileupload/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter09/01 - fileupload/mvnw b/chapter09/01 - fileupload/mvnw index 5bf251c..66df285 100755 --- a/chapter09/01 - fileupload/mvnw +++ b/chapter09/01 - fileupload/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter09/01 - fileupload/mvnw.cmd b/chapter09/01 - fileupload/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter09/01 - fileupload/mvnw.cmd +++ b/chapter09/01 - fileupload/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter09/01 - fileupload/pom.xml b/chapter09/01 - fileupload/pom.xml index 468a4d0..43db322 100644 --- a/chapter09/01 - fileupload/pom.xml +++ b/chapter09/01 - fileupload/pom.xml @@ -1,231 +1,208 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 27.1-jre + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 2.0.3.RELEASE - 1.11.2 - - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - org.postgresql - postgresql - - - org.flywaydb - flyway-core - + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter09/01 - fileupload/src/docs/asciidoc/Copsboot REST API Guide.adoc b/chapter09/01 - fileupload/src/docs/asciidoc/Copsboot REST API Guide.adoc new file mode 100644 index 0000000..b0b91ae --- /dev/null +++ b/chapter09/01 - fileupload/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -0,0 +1,14 @@ += Copsboot REST API Guide +:icons: font +:toc: +:toclevels: 2 + +:numbered: + +== Introduction + +The Copsboot project uses a REST API for interfacing with the server. + +This documentation covers version {project-version} of the application. + +include::_users.adoc[] diff --git a/chapter09/01 - fileupload/src/docs/asciidoc/_users.adoc b/chapter09/01 - fileupload/src/docs/asciidoc/_users.adoc new file mode 100644 index 0000000..2becf75 --- /dev/null +++ b/chapter09/01 - fileupload/src/docs/asciidoc/_users.adoc @@ -0,0 +1,24 @@ +//tag::initial-doc[] +== User Management + +=== User information + +The API allows to get information on the currently logged on user +via a `GET` on `/api/users/me`. If you are not a logged on user, the +following response will be returned: + +operation::own-details-unauthorized[snippets='http-request,http-response'] +//end::initial-doc[] + +If you do log on as a user, you get more information on that user: + +operation::own-details[snippets='http-request,http-response,response-fields'] + + +//tag::create-user[] +=== Create a user + +To create an new user, do a `POST` on `/api/users`: + +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter09/01 - fileupload/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter09/01 - fileupload/src/main/asciidoc/Copsboot REST API Guide.adoc deleted file mode 100644 index 255bc8e..0000000 --- a/chapter09/01 - fileupload/src/main/asciidoc/Copsboot REST API Guide.adoc +++ /dev/null @@ -1,14 +0,0 @@ -= Copsboot REST API Guide -:icons: font -:toc: -:toclevels: 2 - -:numbered: - -== Introduction - -The Copsboot project uses a REST API for interfacing with the server. - -This documentation covers version {project-version} of the application. - -include::_users.adoc[] \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/main/asciidoc/_users.adoc b/chapter09/01 - fileupload/src/main/asciidoc/_users.adoc deleted file mode 100644 index a033db8..0000000 --- a/chapter09/01 - fileupload/src/main/asciidoc/_users.adoc +++ /dev/null @@ -1,24 +0,0 @@ -//tag::initial-doc[] -== User Management - -=== User information - -The API allows to get information on the currently logged on user -via a `GET` on `/api/users/me`. If you are not a logged on user, the -following response will be returned: - -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] -//end::initial-doc[] - -If you do log on as a user, you get more information on that user: - -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] - - -//tag::create-user[] -=== Create a user - -To create an new user, do a `POST` on `/api/users`: - -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java index 344a5fe..fb1cc59 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java @@ -6,6 +6,7 @@ public final class SpringProfiles { public static final String TEST = "test"; public static final String STAGING = "staging"; public static final String PROD = "prod"; + public static final String REPOSITORY_TEST = "repository-test"; public static final String INTEGRATION_TEST = "integration-test"; private SpringProfiles() { diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java index 9c92c49..8d26775 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java @@ -1,11 +1,4 @@ package com.example.copsboot.infrastructure.mvc; -import lombok.Value; - -//tag::class[] -@Value -public class FieldErrorResponse { - private String fieldName; - private String errorMessage; +public record FieldErrorResponse(String fieldName, String errorMesesage) { } -//end::class[] \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/mvc/RestControllerExceptionHandler.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/mvc/RestControllerExceptionHandler.java index f302a4f..b356ea7 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/mvc/RestControllerExceptionHandler.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/mvc/RestControllerExceptionHandler.java @@ -1,7 +1,6 @@ package com.example.copsboot.infrastructure.mvc; import org.springframework.http.HttpStatus; -import org.springframework.validation.BindException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -14,29 +13,17 @@ import java.util.stream.Collectors; //tag::class[] -@ControllerAdvice +@ControllerAdvice //<1> public class RestControllerExceptionHandler { - @ExceptionHandler - @ResponseBody - @ResponseStatus(HttpStatus.BAD_REQUEST) - public Map> handle(MethodArgumentNotValidException exception) { + @ExceptionHandler //<2> + @ResponseBody //<3> + @ResponseStatus(HttpStatus.BAD_REQUEST) //<4> + public Map> handle(MethodArgumentNotValidException exception) { //<5> return error(exception.getBindingResult() .getFieldErrors() .stream() - .map(fieldError -> new FieldErrorResponse(fieldError.getField(), - fieldError.getDefaultMessage())) - .collect(Collectors.toList())); - } - - @ExceptionHandler - @ResponseBody - @ResponseStatus(HttpStatus.BAD_REQUEST) - public Map handle(BindException exception) { - return error(exception.getBindingResult() - .getFieldErrors() - .stream() - .map(fieldError -> new FieldErrorResponse(fieldError.getField(), + .map(fieldError -> new FieldErrorResponse(fieldError.getField(), //<6> fieldError.getDefaultMessage())) .collect(Collectors.toList())); } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/CreateReportParameters.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/CreateReportParameters.java new file mode 100644 index 0000000..64aeea6 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/CreateReportParameters.java @@ -0,0 +1,8 @@ +package com.example.copsboot.report; + +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +public record CreateReportParameters(UserId userId, Instant dateTime, String description) { +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/Report.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/Report.java index c0f9c66..b10756f 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/Report.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/Report.java @@ -1,36 +1,36 @@ package com.example.copsboot.report; -import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; import com.example.orm.jpa.AbstractEntity; import com.example.util.ArtifactForFramework; +import jakarta.persistence.Entity; + +import java.time.Instant; -import javax.persistence.Entity; -import javax.persistence.ManyToOne; -import java.time.ZonedDateTime; //tag::class[] @Entity public class Report extends AbstractEntity { - @ManyToOne - private User reporter; - private ZonedDateTime dateTime; + + private UserId reporterId; + private Instant dateTime; private String description; @ArtifactForFramework protected Report() { } - public Report(ReportId id, User reporter, ZonedDateTime dateTime, String description) { + public Report(ReportId id, UserId reporterId, Instant dateTime, String description) { super(id); - this.reporter = reporter; + this.reporterId = reporterId; this.dateTime = dateTime; this.description = description; } - public User getReporter() { - return reporter; + public UserId getReporterId() { + return reporterId; } - public ZonedDateTime getDateTime() { + public Instant getDateTime() { return dateTime; } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/ReportService.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/ReportService.java index 4d02935..613248b 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/ReportService.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/ReportService.java @@ -1,10 +1,23 @@ package com.example.copsboot.report; -import com.example.copsboot.user.UserId; -import org.springframework.web.multipart.MultipartFile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.ZonedDateTime; -public interface ReportService { - Report createReport(UserId reporterId, ZonedDateTime dateTime, String description, MultipartFile image); +@Service +@Transactional +public class ReportService { + private final ReportRepository repository; + + public ReportService(ReportRepository repository) { + this.repository = repository; + } + + public Report createReport(CreateReportParameters parameters) { + return repository.save(new Report(repository.nextId(), + parameters.userId(), + parameters.dateTime(), + parameters.description())); + } } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/ReportServiceImpl.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/ReportServiceImpl.java deleted file mode 100644 index 403fd0e..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/ReportServiceImpl.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserNotFoundException; -import com.example.copsboot.user.UserService; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -import java.time.ZonedDateTime; - -@Service -@Transactional -public class ReportServiceImpl implements ReportService { - private final ReportRepository repository; - private final UserService userService; - - public ReportServiceImpl(ReportRepository repository, UserService userService) { - this.repository = repository; - this.userService = userService; - } - - @Override - public Report createReport(UserId reporterId, ZonedDateTime dateTime, String description, MultipartFile image) { - return repository.save(new Report(repository.nextId(), - userService.getUser(reporterId) - .orElseThrow(() -> new UserNotFoundException(reporterId)), - dateTime, - description)); - } -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java deleted file mode 100644 index efeb69b..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.report.web; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.web.multipart.MultipartFile; - -import javax.validation.constraints.NotNull; -import java.time.ZonedDateTime; - -//tag::class[] -@Data -@AllArgsConstructor -@NoArgsConstructor -public class CreateReportParameters { - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) - private ZonedDateTime dateTime; - - @ValidReportDescription - private String description; - - @NotNull - private MultipartFile image; //<1> -} -//end::class[] \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java new file mode 100644 index 0000000..d4b215f --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java @@ -0,0 +1,21 @@ +package com.example.copsboot.report.web; + +import com.example.copsboot.report.CreateReportParameters; +import com.example.copsboot.user.UserId; +import jakarta.validation.constraints.NotNull; +import org.springframework.web.multipart.MultipartFile; + +import java.time.Instant; + +@ValidCreateReportRequest +public record CreateReportRequest( + Instant dateTime, + @ValidReportDescription String description, + boolean trafficIncident, + int numberOfInvolvedCars, + @NotNull MultipartFile image //<.> +) { + public CreateReportParameters toParameters(UserId userId) { + return new CreateReportParameters(userId, dateTime, description); + } +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java new file mode 100644 index 0000000..fbad4ea --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java @@ -0,0 +1,21 @@ +package com.example.copsboot.report.web; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +//tag::class[] +public class CreateReportRequestValidator implements ConstraintValidator { //<1> + + @Override + public void initialize(ValidCreateReportRequest constraintAnnotation) { + } + + @Override + public boolean isValid(CreateReportRequest value, ConstraintValidatorContext context) { + boolean result = true; + if (value.trafficIncident() && value.numberOfInvolvedCars() <= 0) { //<2> + result = false; + } + return result; + } +} //end::class[] diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java index d1bff04..aa30ca4 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java @@ -1,10 +1,9 @@ package com.example.copsboot.report.web; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; -public class ReportDescriptionValidator - implements ConstraintValidator { //<1> +public class ReportDescriptionValidator implements ConstraintValidator { //<1> @Override public void initialize(ValidReportDescription constraintAnnotation) { //<2> diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportDto.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportDto.java index 0adc7f8..28e606e 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportDto.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportDto.java @@ -2,23 +2,21 @@ import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; -import lombok.Value; +import com.example.copsboot.user.UserService; -import java.time.ZonedDateTime; +import java.time.Instant; //tag::class[] -@Value -public class ReportDto { - private ReportId id; - private String reporter; - private ZonedDateTime dateTime; - private String description; +public record ReportDto(ReportId id, + String reporter, + Instant dateTime, + String description) { - public static ReportDto fromReport(Report report) { + public static ReportDto fromReport(Report report, UserService userService) { return new ReportDto(report.getId(), - report.getReporter().getEmail(), - report.getDateTime(), - report.getDescription()); + userService.getUserById(report.getReporterId()).getEmail(), + report.getDateTime(), + report.getDescription()); } } //end::class[] diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportRestController.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportRestController.java index 6de180b..12387e0 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportRestController.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportRestController.java @@ -1,36 +1,44 @@ package com.example.copsboot.report.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.report.CreateReportParameters; +import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportService; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserNotFoundException; +import com.example.copsboot.user.UserService; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.UUID; //tag::class[] @RestController @RequestMapping("/api/reports") public class ReportRestController { private final ReportService service; + private final UserService userService; - public ReportRestController(ReportService service) { + public ReportRestController(ReportService service, UserService userService) { this.service = service; + this.userService = userService; } - //tag::create-report-method-signature[] + // tag::create-report-method-signature[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public ReportDto createReport(@AuthenticationPrincipal ApplicationUserDetails userDetails, - @Valid CreateReportParameters parameters) { - //end::create-report-method-signature[] - return ReportDto.fromReport(service.createReport(userDetails.getUserId(), - parameters.getDateTime(), - parameters.getDescription(), - parameters.getImage())); + public ReportDto createReport(@AuthenticationPrincipal Jwt jwt, + @Valid CreateReportRequest request) { + // end::create-report-method-signature[] + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + User user = userService.findUserByAuthServerId(authServerId) + .orElseThrow(() -> new UserNotFoundException(authServerId)); + CreateReportParameters parameters = request.toParameters(user.getId()); + Report report = service.createReport(parameters); + return ReportDto.fromReport(report, userService); } } //end::class[] diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java new file mode 100644 index 0000000..895ce6c --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java @@ -0,0 +1,20 @@ +package com.example.copsboot.report.web; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +//tag::class[] +@Target(ElementType.TYPE) //<1> +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {CreateReportRequestValidator.class}) //<2> +public @interface ValidCreateReportRequest { + String message() default "Invalid report"; + + Class[] groups() default {}; + + Class[] payload() default {}; +}//end::class[] diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java index 41d39e9..ba8fa56 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -16,4 +16,4 @@ Class[] groups() default {}; //<5> Class[] payload() default {}; //<6> -} \ No newline at end of file +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/User.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/User.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java new file mode 100644 index 0000000..2a434e3 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class UserIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(UserId attribute) { + return attribute.getId(); + } + + @Override + public UserId convertToEntityAttribute(UUID dbData) { + return new UserId(dbData); + } +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserNotFoundException.java index 1f65f04..97d0813 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserNotFoundException.java @@ -3,9 +3,13 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> +@ResponseStatus(HttpStatus.NOT_FOUND) public class UserNotFoundException extends RuntimeException { public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); + super(String.format("Unable to find user with id %s", userId)); + } + + public UserNotFoundException(AuthServerId authServerId) { + super(String.format("Unable to find user with auth server id %s", authServerId)); } } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..741d2e0 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,11 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); + + Optional findByMobileToken(String mobileToken); } //end::class[] diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserService.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserService.java index d5630f0..ba1d4ab 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserService.java @@ -1,11 +1,37 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } + + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } - Optional getUser(UserId userId); + public User getUserById(UserId userId) { + return repository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + } - Optional findUserByEmail(String email); + public Optional findUserByMobileToken(String mobileToken) { + return repository.findByMobileToken(mobileToken); + } + // end::createUser[] } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 6918081..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } - - @Override - public Optional findUserByEmail(String email) { - return repository.findByEmailIgnoreCase(email); - } -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index f96ee54..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -@ValidCreateUserParameters -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java deleted file mode 100644 index 3f86d70..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; - -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; - -//tag::class[] -public class CreateUserParametersValidator implements ConstraintValidator { - - private final UserService userService; - - @Autowired - public CreateUserParametersValidator(UserService userService) { //<1> - this.userService = userService; - } - - @Override - public void initialize(ValidCreateUserParameters constraintAnnotation) { - - } - - @Override - public boolean isValid(CreateOfficerParameters userParameters, ConstraintValidatorContext context) { - - boolean result = true; - - if (userService.findUserByEmail(userParameters.getEmail()).isPresent()) { //<2> - context.buildConstraintViolationWithTemplate( - "There is already a user with the given email address.") - .addPropertyNode("email").addConstraintViolation(); //<3> - - result = false; //<4> - } - - return result; - } -} -//end::class[] diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..83c56a1 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,18 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +@ValidCreateUserRequest +public record CreateUserRequest(@NotEmpty String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + String email = jwt.getClaimAsString("email"); + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java new file mode 100644 index 0000000..bdd4aa5 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java @@ -0,0 +1,40 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.UserService; +import org.springframework.beans.factory.annotation.Autowired; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +//tag::class[] +public class CreateUserRequestValidator implements ConstraintValidator { + + private final UserService userService; + + @Autowired + public CreateUserRequestValidator(UserService userService) { //<1> + this.userService = userService; + } + + @Override + public void initialize(ValidCreateUserRequest constraintAnnotation) { + + } + + @Override + public boolean isValid(CreateUserRequest userRequest, ConstraintValidatorContext context) { + + boolean result = true; + + if (userService.findUserByMobileToken(userRequest.mobileToken()).isPresent()) { //<2> + context.buildConstraintViolationWithTemplate( + "There is already a user with the given mobile token.") + .addPropertyNode("mobileToken").addConstraintViolation(); //<3> + + result = false; //<4> + } + + return result; + } +} +//end::class[] diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/UserRestController.java index b5aa1a8..e0a6545 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,49 +1,53 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import lombok.Value; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Valid; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] + // tag::createUser[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { - User officer = service.createOfficer(parameters.getEmail(), - parameters.getPassword()); - return UserDto.fromUser(officer); + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java similarity index 70% rename from chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java rename to chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java index a7ec388..e6a975e 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java @@ -1,7 +1,7 @@ package com.example.copsboot.user.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -10,8 +10,8 @@ //tag::class[] @Target(ElementType.TYPE) // <1> @Retention(RetentionPolicy.RUNTIME) -@Constraint(validatedBy = {CreateUserParametersValidator.class}) //<2> -public @interface ValidCreateUserParameters { +@Constraint(validatedBy = {CreateUserRequestValidator.class}) //<2> +public @interface ValidCreateUserRequest { String message() default "Invalid user"; Class[] groups() default {}; diff --git a/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/Entity.java b/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/Entity.java index 96cadf0..3a45231 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/Entity.java @@ -1,5 +1,6 @@ package com.example.orm.jpa; + /** * Interface for entity objects. * diff --git a/chapter09/01 - fileupload/src/main/resources/application-dev.properties b/chapter09/01 - fileupload/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter09/01 - fileupload/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/main/resources/application-local.properties b/chapter09/01 - fileupload/src/main/resources/application-local.properties index 8f13f3f..7e354d5 100644 --- a/chapter09/01 - fileupload/src/main/resources/application-local.properties +++ b/chapter09/01 - fileupload/src/main/resources/application-local.properties @@ -3,13 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +#spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter09/01 - fileupload/src/main/resources/application.properties b/chapter09/01 - fileupload/src/main/resources/application.properties index e69de29..3e80adf 100644 --- a/chapter09/01 - fileupload/src/main/resources/application.properties +++ b/chapter09/01 - fileupload/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/copsboot + +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter09/01 - fileupload/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter09/01 - fileupload/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter09/01 - fileupload/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter09/01 - fileupload/src/main/resources/db/migration/V1.0.0.2__reports.sql b/chapter09/01 - fileupload/src/main/resources/db/migration/V1.0.0.2__reports.sql new file mode 100644 index 0000000..cc2e26c --- /dev/null +++ b/chapter09/01 - fileupload/src/main/resources/db/migration/V1.0.0.2__reports.sql @@ -0,0 +1,8 @@ +CREATE TABLE report +( + date_time TIMESTAMP(6) WITH TIME ZONE, + id uuid NOT NULL, + description VARCHAR(255), + reporter_id uuid, + PRIMARY KEY (id) +); diff --git a/chapter09/01 - fileupload/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter09/01 - fileupload/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter09/01 - fileupload/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter09/01 - fileupload/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter09/01 - fileupload/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter09/01 - fileupload/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter09/01 - fileupload/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter09/01 - fileupload/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..5feb390 100644 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,16 @@ package com.example.copsboot; import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/Reports.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/Reports.java deleted file mode 100644 index 2302dc9..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/Reports.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.Users; - -import java.time.ZonedDateTime; -import java.util.UUID; - -public class Reports { - public static Report createRandomReport(String description) { - return new Report(new ReportId(UUID.randomUUID()), - Users.newRandomOfficer(), - ZonedDateTime.now(), - description); - } -} diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java new file mode 100644 index 0000000..04744f4 --- /dev/null +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java @@ -0,0 +1,75 @@ +package com.example.copsboot.report.web; + + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; + +import java.time.Instant; +import java.util.Set; + +import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; + +public class CreateReportRequestValidatorTest { + //tag::invalid[] + @Test + public void givenTrafficIndicentButInvolvedCarsZero_invalid() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat", + true, + 0, + createImage()); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasViolationOnPath(""); + } + } + //end::invalid[] + + //tag::valid[] + @Test + public void givenTrafficIndicent_involvedCarsMustBePositive() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", + true, + 2, + createImage()); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } + } + //end::valid[] + + //tag::valid-no-cars[] + @Test + public void givenNoTrafficIndicent_involvedCarsDoesNotMatter() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", + false, + 0, + createImage()); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } + } + //end::valid-no-cars[] + + @NotNull + private static MockMultipartFile createImage() { + return new MockMultipartFile("image", "picture.png", MediaType.IMAGE_PNG_VALUE, new byte[]{1, 2, 3}); + } + +} diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java index 0715dc3..b3b6724 100644 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java @@ -1,13 +1,15 @@ package com.example.copsboot.report.web; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.jetbrains.annotations.NotNull; import org.junit.Test; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.time.ZonedDateTime; +import java.time.Instant; import java.util.Set; import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; @@ -17,33 +19,34 @@ public class ReportDescriptionValidatorTest { //tag::invalid[] @Test public void givenEmptyString_notValid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); //<1> - Validator validator = factory.getValidator(); //<2> - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), "", createMockImage()); - Set> violationSet = validator.validate(parameters); //<3> - assertThat(violationSet).hasViolationOnPath("description"); //<4> + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { //<1> + Validator validator = factory.getValidator(); //<2> + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), "", false, 0, + createImage()); + Set> violationSet = validator.validate(parameters); //<3> + assertThat(violationSet).hasViolationOnPath("description"); //<4> + } } //end::invalid[] //tag::valid[] @Test public void givenSuspectWordPresent_valid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), - "The suspect was wearing a black hat.", - createMockImage()); - Set> violationSet = validator.validate(parameters); - assertThat(violationSet).hasNoViolations(); + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", false, 0, + createImage()); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } } //end::valid[] - private MockMultipartFile createMockImage() { - return new MockMultipartFile("image", - "picture.png", - "image/png", - new byte[]{1, 2, 3}); + @NotNull + private static MockMultipartFile createImage() { + return new MockMultipartFile("image", "picture.png", MediaType.IMAGE_PNG_VALUE, new byte[]{1, 2, 3}); } -} \ No newline at end of file +} diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java index 535b38d..88f289a 100644 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java @@ -1,69 +1,76 @@ package com.example.copsboot.report.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; import com.example.copsboot.report.ReportService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.time.ZonedDateTime; +import java.time.Instant; +import java.util.Optional; import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; //tag::class[] -@RunWith(SpringRunner.class) @CopsbootControllerTest(ReportRestController.class) public class ReportRestControllerTest { @Autowired - private MockMvc mvc; - + private MockMvc mockMvc; @MockBean private ReportService service; + @MockBean + private UserService userService; @Test public void officerIsAbleToPostAReport() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - String dateTime = "2018-04-11T22:59:03.189+02:00"; - String description = "The suspect is wearing a black hat."; - MockMultipartFile image = createMockImage(); - when(service.createReport(eq(Users.officer().getId()), - any(ZonedDateTime.class), - eq(description), - any(MockMultipartFile.class))) - .thenReturn(new Report(new ReportId(UUID.randomUUID()), Users.officer(), ZonedDateTime.parse(dateTime), description)); - - mvc.perform(fileUpload("/api/reports") //<1> - .file(image) //<2> - .header(HEADER_AUTHORIZATION, bearer(accessToken)) - .param("dateTime", dateTime) //<3> - .param("description", description)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("reporter").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("dateTime").value(dateTime)) - .andExpect(jsonPath("description").value(description)); - } - private MockMultipartFile createMockImage() { //<4> - return new MockMultipartFile("image", - "picture.png", - "image/png", - new byte[]{1, 2, 3}); + UserId userId = new UserId(UUID.randomUUID()); + AuthServerId authServerId = new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); + User user = new User(userId, + "wim@example.com", + authServerId, + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"); + when(userService.findUserByAuthServerId(authServerId)) + .thenReturn(Optional.of(user)); + when(userService.getUserById(userId)) + .thenReturn(user); + when(service.createReport(any(CreateReportParameters.class))) + .thenReturn(new Report(new ReportId(UUID.randomUUID()), + userId, + Instant.parse("2023-04-11T22:59:03.189+02:00"), + "This is a test report description. The suspect was wearing a black hat.")); + mockMvc.perform(multipart("/api/reports") //<.> + .file(new MockMultipartFile("image", "picture.png", MediaType.IMAGE_PNG_VALUE, new byte[]{1,2,3})) //<.> + .param("dateTime", "2023-04-11T22:59:03.189+02:00") //<.> + .param("description", "This is a test report description. The suspect was wearing a black hat.") + .param("trafficIncident", "false") + .param("numberOfInvolvedCars", "0") + .with(jwt().jwt(builder -> builder.subject(authServerId.value().toString()) + .claim("email", "wim@example.com")) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("id").exists()) + .andExpect(jsonPath("reporter").value("wim@example.com")) + .andExpect(jsonPath("dateTime").value("2023-04-11T20:59:03.189Z")) + .andExpect(jsonPath("description").value("This is a test report description. The suspect was wearing a black hat.")); } } -//end::class[] \ No newline at end of file +//end::class[] diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java deleted file mode 100644 index 720f959..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.copsboot.user; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.HashSet; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> -@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) //<2> -public class UserRepositoryIntegrationTest { - @Autowired - private UserRepository repository; - @PersistenceContext - private EntityManager entityManager; - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void testSaveUser() { - Set roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); - - assertThat(repository.count()).isEqualTo(1L); - - entityManager.flush(); //<3> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM user_roles", Long.class)).isEqualTo(1L); - assertThat(jdbcTemplate.queryForObject("SELECT roles FROM user_roles", String.class)).isEqualTo("OFFICER"); - } -} diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..19c23fe 100644 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -3,14 +3,16 @@ import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,62 +21,34 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) //<2> public class UserRepositoryTest { @Autowired private UserRepository repository; + @PersistenceContext + private EntityManager entityManager; + @Autowired + private JdbcTemplate jdbcTemplate; - //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } + assertThat(repository.count()).isEqualTo(1L); - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); + entityManager.flush(); //<3> - assertThat(optional).isEmpty(); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> + assertThat(jdbcTemplate.queryForObject("SELECT email FROM copsboot_user", String.class)).isEqualTo("alex.foley@beverly-hills.com"); } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @Bean @@ -82,5 +56,4 @@ public UniqueIdGenerator generator() { return new InMemoryUniqueIdGenerator(); } } - //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/Users.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java deleted file mode 100644 index 7b94df3..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserService; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.validation.ConstraintViolation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; - -import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; -import static org.mockito.Mockito.when; - -//tag::class[] -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@ActiveProfiles(SpringProfiles.TEST) -public class CreateUserParametersValidatorTest { - - @MockBean - private UserService userService; //<2> - @Autowired - private PasswordEncoder encoder; - @Autowired - private ValidatorFactory factory; //<3> - - @Test - public void invalidIfAlreadyUserWithGivenEmail() { - - String email = "wim.deblauwe@example.com"; - when(userService.findUserByEmail(email)) - .thenReturn(Optional.of( - User.createOfficer(new UserId(UUID.randomUUID()), - email, - encoder.encode("testing1234")))); - - Validator validator = factory.getValidator(); //<4> - - CreateOfficerParameters userParameters = new CreateOfficerParameters(); - userParameters.setEmail(email); - userParameters.setPassword("my-secret-pwd-1234"); - Set> violationSet = validator.validate(userParameters); //<5> - assertThat(violationSet).hasViolationSize(2) - .hasViolationOnPath("email"); //<6> - } - - @Test - public void validIfNoUserWithGivenEmail() { - String email = "wim.deblauwe@example.com"; - when(userService.findUserByEmail(email)) - .thenReturn(Optional.empty()); - - Validator validator = factory.getValidator(); - - CreateOfficerParameters userParameters = new CreateOfficerParameters(); - userParameters.setEmail(email); - userParameters.setPassword("my-secret-pwd-1234"); - Set> violationSet = validator.validate(userParameters); - assertThat(violationSet).hasNoViolations(); - } -} -//end::class[] \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java new file mode 100644 index 0000000..d058abd --- /dev/null +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java @@ -0,0 +1,65 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.infrastructure.SpringProfiles; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; +import static org.mockito.Mockito.when; + +//tag::class[] +@SpringBootTest //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) +public class CreateUserRequestValidatorTest { + + @MockBean + private UserService userService; //<2> + @Autowired + private ValidatorFactory factory; //<3> + + @Test + public void invalidIfAlreadyUserWithGivenMobileToken() { + + String mobileToken = "abc123"; + when(userService.findUserByMobileToken(mobileToken)) + .thenReturn(Optional.of(new User(new UserId(UUID.randomUUID()), + "wim@example.com", + new AuthServerId(UUID.randomUUID()), + mobileToken))); + + Validator validator = factory.getValidator(); //<4> + + CreateUserRequest request = new CreateUserRequest(mobileToken); + Set> violationSet = validator.validate(request); //<5> + assertThat(violationSet).hasViolationSize(2) + .hasViolationOnPath("mobileToken"); //<6> + } + + @Test + public void validIfNoUserWithGivenMobileToken() { + String mobileToken = "abc123"; + when(userService.findUserByMobileToken(mobileToken)) + .thenReturn(Optional.empty()); + + Validator validator = factory.getValidator(); + + CreateUserRequest request = new CreateUserRequest(mobileToken); + Set> violationSet = validator.validate(request); + assertThat(violationSet).hasNoViolations(); + } +} +//end::class[] diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index b1c3165..805c501 100644 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,134 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index f55e6c6..a20d744 100644 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,121 +1,104 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } + // tag::emptyToken[] @Test - public void testCreateOfficerIfPasswordIsTooShort() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("errors[0].fieldName").value("password")); - - verify(service, never()).createOfficer(email, password); + void givenEmptyMobileToken_badRequestIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "" + } + """)) //<.> + .andExpect(status().isBadRequest()) //<.> + .andDo(print()); //<.> + + verify(userService, never()).createUser(any(CreateUserParameters.class)); //<.> } + // end::emptyToken[] } diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java index 77c5f4c..21556a5 100644 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java @@ -2,7 +2,8 @@ import org.assertj.core.api.AbstractAssert; -import javax.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolation; + import java.util.Set; import java.util.stream.Collectors; @@ -21,12 +22,12 @@ public ConstraintViolationSetAssert hasViolationOnPath(String path) { // check condition if (!containsViolationWithPath(actual, path)) { failWithMessage("There was no violation with path <%s>. Violation paths: <%s>", path, actual.stream() - .map(violation -> violation - .getPropertyPath() - .toString()) - .collect( - Collectors - .toList())); + .map(violation -> violation + .getPropertyPath() + .toString()) + .collect( + Collectors + .toList())); } return this; diff --git a/chapter09/01 - fileupload/src/test/resources/application-integration-test.properties b/chapter09/01 - fileupload/src/test/resources/application-integration-test.properties index 159536c..c61e563 100644 --- a/chapter09/01 - fileupload/src/test/resources/application-integration-test.properties +++ b/chapter09/01 - fileupload/src/test/resources/application-integration-test.properties @@ -1,11 +1,6 @@ -spring.datasource.url=jdbc:tc:postgresql://localhost/copsbootdb +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.username=user spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none - -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter09/01 - fileupload/src/test/resources/application-repository-test.properties b/chapter09/01 - fileupload/src/test/resources/application-repository-test.properties new file mode 100644 index 0000000..c61e563 --- /dev/null +++ b/chapter09/01 - fileupload/src/test/resources/application-repository-test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=user +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter09/01 - fileupload/src/test/resources/application-test.properties b/chapter09/01 - fileupload/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter09/01 - fileupload/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/test/resources/logback-test.xml b/chapter09/01 - fileupload/src/test/resources/logback-test.xml index bf47fec..164429c 100644 --- a/chapter09/01 - fileupload/src/test/resources/logback-test.xml +++ b/chapter09/01 - fileupload/src/test/resources/logback-test.xml @@ -5,7 +5,7 @@ - + @@ -17,14 +17,8 @@ - - - - - - - \ No newline at end of file + diff --git a/chapter09/02 - validation/.mvn/wrapper/maven-wrapper.jar b/chapter09/02 - validation/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/chapter09/02 - validation/.mvn/wrapper/maven-wrapper.properties b/chapter09/02 - validation/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter09/02 - validation/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter09/02 - validation/docker-compose.yaml b/chapter09/02 - validation/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter09/02 - validation/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter09/02 - validation/mvnw b/chapter09/02 - validation/mvnw index 5bf251c..66df285 100755 --- a/chapter09/02 - validation/mvnw +++ b/chapter09/02 - validation/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter09/02 - validation/mvnw.cmd b/chapter09/02 - validation/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter09/02 - validation/mvnw.cmd +++ b/chapter09/02 - validation/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter09/02 - validation/pom.xml b/chapter09/02 - validation/pom.xml index dc02169..e5f26ec 100644 --- a/chapter09/02 - validation/pom.xml +++ b/chapter09/02 - validation/pom.xml @@ -1,240 +1,225 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 1.5.6 + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + - - 27.1-jre + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + + + io.rest-assured + rest-assured + test + + + + + com.github.dasniko + testcontainers-keycloak + 3.0.0 + test + + - - 2.0.3.RELEASE - 1.11.2 - 3.3.0 - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - org.postgresql - postgresql - - - org.flywaydb - flyway-core - + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - - - io.rest-assured - rest-assured - ${rest-assured.version} - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter09/02 - validation/src/docs/asciidoc/Copsboot REST API Guide.adoc b/chapter09/02 - validation/src/docs/asciidoc/Copsboot REST API Guide.adoc new file mode 100644 index 0000000..b0b91ae --- /dev/null +++ b/chapter09/02 - validation/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -0,0 +1,14 @@ += Copsboot REST API Guide +:icons: font +:toc: +:toclevels: 2 + +:numbered: + +== Introduction + +The Copsboot project uses a REST API for interfacing with the server. + +This documentation covers version {project-version} of the application. + +include::_users.adoc[] diff --git a/chapter09/02 - validation/src/docs/asciidoc/_users.adoc b/chapter09/02 - validation/src/docs/asciidoc/_users.adoc new file mode 100644 index 0000000..2becf75 --- /dev/null +++ b/chapter09/02 - validation/src/docs/asciidoc/_users.adoc @@ -0,0 +1,24 @@ +//tag::initial-doc[] +== User Management + +=== User information + +The API allows to get information on the currently logged on user +via a `GET` on `/api/users/me`. If you are not a logged on user, the +following response will be returned: + +operation::own-details-unauthorized[snippets='http-request,http-response'] +//end::initial-doc[] + +If you do log on as a user, you get more information on that user: + +operation::own-details[snippets='http-request,http-response,response-fields'] + + +//tag::create-user[] +=== Create a user + +To create an new user, do a `POST` on `/api/users`: + +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter09/02 - validation/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter09/02 - validation/src/main/asciidoc/Copsboot REST API Guide.adoc deleted file mode 100644 index 255bc8e..0000000 --- a/chapter09/02 - validation/src/main/asciidoc/Copsboot REST API Guide.adoc +++ /dev/null @@ -1,14 +0,0 @@ -= Copsboot REST API Guide -:icons: font -:toc: -:toclevels: 2 - -:numbered: - -== Introduction - -The Copsboot project uses a REST API for interfacing with the server. - -This documentation covers version {project-version} of the application. - -include::_users.adoc[] \ No newline at end of file diff --git a/chapter09/02 - validation/src/main/asciidoc/_users.adoc b/chapter09/02 - validation/src/main/asciidoc/_users.adoc deleted file mode 100644 index a033db8..0000000 --- a/chapter09/02 - validation/src/main/asciidoc/_users.adoc +++ /dev/null @@ -1,24 +0,0 @@ -//tag::initial-doc[] -== User Management - -=== User information - -The API allows to get information on the currently logged on user -via a `GET` on `/api/users/me`. If you are not a logged on user, the -following response will be returned: - -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] -//end::initial-doc[] - -If you do log on as a user, you get more information on that user: - -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] - - -//tag::create-user[] -=== Create a user - -To create an new user, do a `POST` on `/api/users`: - -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java index 344a5fe..fb1cc59 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java @@ -6,6 +6,7 @@ public final class SpringProfiles { public static final String TEST = "test"; public static final String STAGING = "staging"; public static final String PROD = "prod"; + public static final String REPOSITORY_TEST = "repository-test"; public static final String INTEGRATION_TEST = "integration-test"; private SpringProfiles() { diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java index 9c92c49..8d26775 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java @@ -1,11 +1,4 @@ package com.example.copsboot.infrastructure.mvc; -import lombok.Value; - -//tag::class[] -@Value -public class FieldErrorResponse { - private String fieldName; - private String errorMessage; +public record FieldErrorResponse(String fieldName, String errorMesesage) { } -//end::class[] \ No newline at end of file diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/mvc/RestControllerExceptionHandler.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/mvc/RestControllerExceptionHandler.java index a5acde8..b9fabac 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/mvc/RestControllerExceptionHandler.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/mvc/RestControllerExceptionHandler.java @@ -2,14 +2,12 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.ui.Model; -import org.springframework.validation.BindException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.multipart.MultipartException; +import org.springframework.web.multipart.MaxUploadSizeExceededException; import java.util.Collections; import java.util.List; @@ -17,42 +15,30 @@ import java.util.stream.Collectors; //tag::class[] -@ControllerAdvice +@ControllerAdvice //<1> public class RestControllerExceptionHandler { - @ExceptionHandler - @ResponseBody - @ResponseStatus(HttpStatus.BAD_REQUEST) - public Map> handle(MethodArgumentNotValidException exception) { + @ExceptionHandler //<2> + @ResponseBody //<3> + @ResponseStatus(HttpStatus.BAD_REQUEST) //<4> + public Map> handle(MethodArgumentNotValidException exception) { //<5> return error(exception.getBindingResult() - .getFieldErrors() - .stream() - .map(fieldError -> new FieldErrorResponse(fieldError.getField(), - fieldError.getDefaultMessage())) - .collect(Collectors.toList())); + .getFieldErrors() + .stream() + .map(fieldError -> new FieldErrorResponse(fieldError.getField(), //<6> + fieldError.getDefaultMessage())) + .collect(Collectors.toList())); } - @ExceptionHandler - @ResponseBody - @ResponseStatus(HttpStatus.BAD_REQUEST) - public Map> handle(BindException exception) { - return error(exception.getBindingResult() - .getFieldErrors() - .stream() - .map(fieldError -> new FieldErrorResponse(fieldError.getField(), - fieldError.getDefaultMessage())) - .collect(Collectors.toList())); + // tag::maxUploadSizeExceeded[] + @ExceptionHandler(MaxUploadSizeExceededException.class) + public ResponseEntity> maxUploadSizeExceeded(MaxUploadSizeExceededException e) { + return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE) + .body(Map.of("code", "MAX_UPLOAD_SIZE_EXCEEDED", + "description", e.getMessage())); } + // end::maxUploadSizeExceeded[] - //tag::multipart-exception[] - @ExceptionHandler(MultipartException.class) - public ResponseEntity handleMultipartException(MultipartException e, Model model) { - model.addAttribute("exception", e); - return ResponseEntity - .badRequest() - .body(e.getMessage()); - } - //end::multipart-exception[] private Map> error(List errors) { return Collections.singletonMap("errors", errors); diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index d28668e..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/CreateReportParameters.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/CreateReportParameters.java new file mode 100644 index 0000000..64aeea6 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/CreateReportParameters.java @@ -0,0 +1,8 @@ +package com.example.copsboot.report; + +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +public record CreateReportParameters(UserId userId, Instant dateTime, String description) { +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/Report.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/Report.java index c0f9c66..b10756f 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/Report.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/Report.java @@ -1,36 +1,36 @@ package com.example.copsboot.report; -import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; import com.example.orm.jpa.AbstractEntity; import com.example.util.ArtifactForFramework; +import jakarta.persistence.Entity; + +import java.time.Instant; -import javax.persistence.Entity; -import javax.persistence.ManyToOne; -import java.time.ZonedDateTime; //tag::class[] @Entity public class Report extends AbstractEntity { - @ManyToOne - private User reporter; - private ZonedDateTime dateTime; + + private UserId reporterId; + private Instant dateTime; private String description; @ArtifactForFramework protected Report() { } - public Report(ReportId id, User reporter, ZonedDateTime dateTime, String description) { + public Report(ReportId id, UserId reporterId, Instant dateTime, String description) { super(id); - this.reporter = reporter; + this.reporterId = reporterId; this.dateTime = dateTime; this.description = description; } - public User getReporter() { - return reporter; + public UserId getReporterId() { + return reporterId; } - public ZonedDateTime getDateTime() { + public Instant getDateTime() { return dateTime; } diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/ReportService.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/ReportService.java index 4d02935..613248b 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/ReportService.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/ReportService.java @@ -1,10 +1,23 @@ package com.example.copsboot.report; -import com.example.copsboot.user.UserId; -import org.springframework.web.multipart.MultipartFile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.ZonedDateTime; -public interface ReportService { - Report createReport(UserId reporterId, ZonedDateTime dateTime, String description, MultipartFile image); +@Service +@Transactional +public class ReportService { + private final ReportRepository repository; + + public ReportService(ReportRepository repository) { + this.repository = repository; + } + + public Report createReport(CreateReportParameters parameters) { + return repository.save(new Report(repository.nextId(), + parameters.userId(), + parameters.dateTime(), + parameters.description())); + } } diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/ReportServiceImpl.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/ReportServiceImpl.java deleted file mode 100644 index 403fd0e..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/ReportServiceImpl.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserNotFoundException; -import com.example.copsboot.user.UserService; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -import java.time.ZonedDateTime; - -@Service -@Transactional -public class ReportServiceImpl implements ReportService { - private final ReportRepository repository; - private final UserService userService; - - public ReportServiceImpl(ReportRepository repository, UserService userService) { - this.repository = repository; - this.userService = userService; - } - - @Override - public Report createReport(UserId reporterId, ZonedDateTime dateTime, String description, MultipartFile image) { - return repository.save(new Report(repository.nextId(), - userService.getUser(reporterId) - .orElseThrow(() -> new UserNotFoundException(reporterId)), - dateTime, - description)); - } -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java deleted file mode 100644 index efeb69b..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.report.web; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.web.multipart.MultipartFile; - -import javax.validation.constraints.NotNull; -import java.time.ZonedDateTime; - -//tag::class[] -@Data -@AllArgsConstructor -@NoArgsConstructor -public class CreateReportParameters { - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) - private ZonedDateTime dateTime; - - @ValidReportDescription - private String description; - - @NotNull - private MultipartFile image; //<1> -} -//end::class[] \ No newline at end of file diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java new file mode 100644 index 0000000..d4b215f --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java @@ -0,0 +1,21 @@ +package com.example.copsboot.report.web; + +import com.example.copsboot.report.CreateReportParameters; +import com.example.copsboot.user.UserId; +import jakarta.validation.constraints.NotNull; +import org.springframework.web.multipart.MultipartFile; + +import java.time.Instant; + +@ValidCreateReportRequest +public record CreateReportRequest( + Instant dateTime, + @ValidReportDescription String description, + boolean trafficIncident, + int numberOfInvolvedCars, + @NotNull MultipartFile image //<.> +) { + public CreateReportParameters toParameters(UserId userId) { + return new CreateReportParameters(userId, dateTime, description); + } +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java new file mode 100644 index 0000000..fbad4ea --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java @@ -0,0 +1,21 @@ +package com.example.copsboot.report.web; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +//tag::class[] +public class CreateReportRequestValidator implements ConstraintValidator { //<1> + + @Override + public void initialize(ValidCreateReportRequest constraintAnnotation) { + } + + @Override + public boolean isValid(CreateReportRequest value, ConstraintValidatorContext context) { + boolean result = true; + if (value.trafficIncident() && value.numberOfInvolvedCars() <= 0) { //<2> + result = false; + } + return result; + } +} //end::class[] diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java index d1bff04..aa30ca4 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java @@ -1,10 +1,9 @@ package com.example.copsboot.report.web; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; -public class ReportDescriptionValidator - implements ConstraintValidator { //<1> +public class ReportDescriptionValidator implements ConstraintValidator { //<1> @Override public void initialize(ValidReportDescription constraintAnnotation) { //<2> diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportDto.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportDto.java index 0adc7f8..28e606e 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportDto.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportDto.java @@ -2,23 +2,21 @@ import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; -import lombok.Value; +import com.example.copsboot.user.UserService; -import java.time.ZonedDateTime; +import java.time.Instant; //tag::class[] -@Value -public class ReportDto { - private ReportId id; - private String reporter; - private ZonedDateTime dateTime; - private String description; +public record ReportDto(ReportId id, + String reporter, + Instant dateTime, + String description) { - public static ReportDto fromReport(Report report) { + public static ReportDto fromReport(Report report, UserService userService) { return new ReportDto(report.getId(), - report.getReporter().getEmail(), - report.getDateTime(), - report.getDescription()); + userService.getUserById(report.getReporterId()).getEmail(), + report.getDateTime(), + report.getDescription()); } } //end::class[] diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportRestController.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportRestController.java index 6de180b..12387e0 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportRestController.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportRestController.java @@ -1,36 +1,44 @@ package com.example.copsboot.report.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.report.CreateReportParameters; +import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportService; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserNotFoundException; +import com.example.copsboot.user.UserService; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.UUID; //tag::class[] @RestController @RequestMapping("/api/reports") public class ReportRestController { private final ReportService service; + private final UserService userService; - public ReportRestController(ReportService service) { + public ReportRestController(ReportService service, UserService userService) { this.service = service; + this.userService = userService; } - //tag::create-report-method-signature[] + // tag::create-report-method-signature[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public ReportDto createReport(@AuthenticationPrincipal ApplicationUserDetails userDetails, - @Valid CreateReportParameters parameters) { - //end::create-report-method-signature[] - return ReportDto.fromReport(service.createReport(userDetails.getUserId(), - parameters.getDateTime(), - parameters.getDescription(), - parameters.getImage())); + public ReportDto createReport(@AuthenticationPrincipal Jwt jwt, + @Valid CreateReportRequest request) { + // end::create-report-method-signature[] + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + User user = userService.findUserByAuthServerId(authServerId) + .orElseThrow(() -> new UserNotFoundException(authServerId)); + CreateReportParameters parameters = request.toParameters(user.getId()); + Report report = service.createReport(parameters); + return ReportDto.fromReport(report, userService); } } //end::class[] diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java new file mode 100644 index 0000000..895ce6c --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java @@ -0,0 +1,20 @@ +package com.example.copsboot.report.web; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +//tag::class[] +@Target(ElementType.TYPE) //<1> +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {CreateReportRequestValidator.class}) //<2> +public @interface ValidCreateReportRequest { + String message() default "Invalid report"; + + Class[] groups() default {}; + + Class[] payload() default {}; +}//end::class[] diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java index 41d39e9..ba8fa56 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -16,4 +16,4 @@ Class[] groups() default {}; //<5> Class[] payload() default {}; //<6> -} \ No newline at end of file +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/User.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/User.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java new file mode 100644 index 0000000..2a434e3 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class UserIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(UserId attribute) { + return attribute.getId(); + } + + @Override + public UserId convertToEntityAttribute(UUID dbData) { + return new UserId(dbData); + } +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserNotFoundException.java index 1f65f04..97d0813 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserNotFoundException.java @@ -3,9 +3,13 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> +@ResponseStatus(HttpStatus.NOT_FOUND) public class UserNotFoundException extends RuntimeException { public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); + super(String.format("Unable to find user with id %s", userId)); + } + + public UserNotFoundException(AuthServerId authServerId) { + super(String.format("Unable to find user with auth server id %s", authServerId)); } } diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..741d2e0 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,11 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); + + Optional findByMobileToken(String mobileToken); } //end::class[] diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserService.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserService.java index d5630f0..ba1d4ab 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserService.java @@ -1,11 +1,37 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } + + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } - Optional getUser(UserId userId); + public User getUserById(UserId userId) { + return repository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + } - Optional findUserByEmail(String email); + public Optional findUserByMobileToken(String mobileToken) { + return repository.findByMobileToken(mobileToken); + } + // end::createUser[] } diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 6918081..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } - - @Override - public Optional findUserByEmail(String email) { - return repository.findByEmailIgnoreCase(email); - } -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index f96ee54..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -@ValidCreateUserParameters -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java deleted file mode 100644 index 3f86d70..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; - -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; - -//tag::class[] -public class CreateUserParametersValidator implements ConstraintValidator { - - private final UserService userService; - - @Autowired - public CreateUserParametersValidator(UserService userService) { //<1> - this.userService = userService; - } - - @Override - public void initialize(ValidCreateUserParameters constraintAnnotation) { - - } - - @Override - public boolean isValid(CreateOfficerParameters userParameters, ConstraintValidatorContext context) { - - boolean result = true; - - if (userService.findUserByEmail(userParameters.getEmail()).isPresent()) { //<2> - context.buildConstraintViolationWithTemplate( - "There is already a user with the given email address.") - .addPropertyNode("email").addConstraintViolation(); //<3> - - result = false; //<4> - } - - return result; - } -} -//end::class[] diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..83c56a1 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,18 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +@ValidCreateUserRequest +public record CreateUserRequest(@NotEmpty String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + String email = jwt.getClaimAsString("email"); + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java new file mode 100644 index 0000000..bdd4aa5 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java @@ -0,0 +1,40 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.UserService; +import org.springframework.beans.factory.annotation.Autowired; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +//tag::class[] +public class CreateUserRequestValidator implements ConstraintValidator { + + private final UserService userService; + + @Autowired + public CreateUserRequestValidator(UserService userService) { //<1> + this.userService = userService; + } + + @Override + public void initialize(ValidCreateUserRequest constraintAnnotation) { + + } + + @Override + public boolean isValid(CreateUserRequest userRequest, ConstraintValidatorContext context) { + + boolean result = true; + + if (userService.findUserByMobileToken(userRequest.mobileToken()).isPresent()) { //<2> + context.buildConstraintViolationWithTemplate( + "There is already a user with the given mobile token.") + .addPropertyNode("mobileToken").addConstraintViolation(); //<3> + + result = false; //<4> + } + + return result; + } +} +//end::class[] diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/UserRestController.java index b5aa1a8..e0a6545 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,49 +1,53 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import lombok.Value; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Valid; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] + // tag::createUser[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { - User officer = service.createOfficer(parameters.getEmail(), - parameters.getPassword()); - return UserDto.fromUser(officer); + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java similarity index 70% rename from chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java rename to chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java index a7ec388..e6a975e 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java @@ -1,7 +1,7 @@ package com.example.copsboot.user.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -10,8 +10,8 @@ //tag::class[] @Target(ElementType.TYPE) // <1> @Retention(RetentionPolicy.RUNTIME) -@Constraint(validatedBy = {CreateUserParametersValidator.class}) //<2> -public @interface ValidCreateUserParameters { +@Constraint(validatedBy = {CreateUserRequestValidator.class}) //<2> +public @interface ValidCreateUserRequest { String message() default "Invalid user"; Class[] groups() default {}; diff --git a/chapter09/02 - validation/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter09/02 - validation/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter09/02 - validation/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter09/02 - validation/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter09/02 - validation/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter09/02 - validation/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter09/02 - validation/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter09/02 - validation/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter09/02 - validation/src/main/java/com/example/orm/jpa/Entity.java b/chapter09/02 - validation/src/main/java/com/example/orm/jpa/Entity.java index 96cadf0..3a45231 100644 --- a/chapter09/02 - validation/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter09/02 - validation/src/main/java/com/example/orm/jpa/Entity.java @@ -1,5 +1,6 @@ package com.example.orm.jpa; + /** * Interface for entity objects. * diff --git a/chapter09/02 - validation/src/main/java/com/example/util/ArtifactForFramework.java b/chapter09/02 - validation/src/main/java/com/example/util/ArtifactForFramework.java index 9e2ef24..5d4ec38 100644 --- a/chapter09/02 - validation/src/main/java/com/example/util/ArtifactForFramework.java +++ b/chapter09/02 - validation/src/main/java/com/example/util/ArtifactForFramework.java @@ -1,8 +1,4 @@ package com.example.util; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@Retention(value = RetentionPolicy.SOURCE) public @interface ArtifactForFramework { } diff --git a/chapter09/02 - validation/src/main/resources/application-dev.properties b/chapter09/02 - validation/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter09/02 - validation/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter09/02 - validation/src/main/resources/application-local.properties b/chapter09/02 - validation/src/main/resources/application-local.properties index 8f13f3f..7e354d5 100644 --- a/chapter09/02 - validation/src/main/resources/application-local.properties +++ b/chapter09/02 - validation/src/main/resources/application-local.properties @@ -3,13 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +#spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter09/02 - validation/src/main/resources/application.properties b/chapter09/02 - validation/src/main/resources/application.properties index 32bb668..e791e0e 100644 --- a/chapter09/02 - validation/src/main/resources/application.properties +++ b/chapter09/02 - validation/src/main/resources/application.properties @@ -1,2 +1,6 @@ +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ + spring.servlet.multipart.max-file-size=1MB -spring.servlet.multipart.max-request-size=10MB \ No newline at end of file +spring.servlet.multipart.max-request-size=10MB diff --git a/chapter09/02 - validation/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter09/02 - validation/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter09/02 - validation/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter09/02 - validation/src/main/resources/db/migration/V1.0.0.2__reports.sql b/chapter09/02 - validation/src/main/resources/db/migration/V1.0.0.2__reports.sql new file mode 100644 index 0000000..cc2e26c --- /dev/null +++ b/chapter09/02 - validation/src/main/resources/db/migration/V1.0.0.2__reports.sql @@ -0,0 +1,8 @@ +CREATE TABLE report +( + date_time TIMESTAMP(6) WITH TIME ZONE, + id uuid NOT NULL, + description VARCHAR(255), + reporter_id uuid, + PRIMARY KEY (id) +); diff --git a/chapter09/02 - validation/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter09/02 - validation/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter09/02 - validation/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter09/02 - validation/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter09/02 - validation/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter09/02 - validation/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter09/02 - validation/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter09/02 - validation/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter09/02 - validation/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..5feb390 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,16 @@ package com.example.copsboot; import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForRestAssured.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForRestAssured.java deleted file mode 100644 index 62c1957..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForRestAssured.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import io.restassured.specification.RequestSpecification; -import org.springframework.security.oauth2.client.OAuth2RestTemplate; -import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails; -import org.springframework.security.oauth2.common.OAuth2AccessToken; - -import static io.restassured.RestAssured.given; -import static java.lang.String.format; - -public class SecurityHelperForRestAssured { - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static RequestSpecification givenAuthenticatedUser(int serverPort, String username, String password) { - OAuth2RestTemplate template = new OAuth2RestTemplate(createResourceOwnerPasswordResourceDetails(serverPort, - username, - password)); - OAuth2AccessToken accessToken = template.getAccessToken(); - - return given().auth().preemptive().oauth2(accessToken.getValue()); - } - - private static ResourceOwnerPasswordResourceDetails createResourceOwnerPasswordResourceDetails(int serverPort, String username, String password) { - ResourceOwnerPasswordResourceDetails details = new ResourceOwnerPasswordResourceDetails(); - details.setAccessTokenUri(String.format("http://localhost:%s/oauth/token", serverPort)); - details.setUsername(username); - details.setPassword(password); - details.setClientId(UNIT_TEST_CLIENT_ID); - details.setClientSecret(UNIT_TEST_CLIENT_SECRET); - return details; - } -} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/Reports.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/Reports.java deleted file mode 100644 index 2302dc9..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/Reports.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.Users; - -import java.time.ZonedDateTime; -import java.util.UUID; - -public class Reports { - public static Report createRandomReport(String description) { - return new Report(new ReportId(UUID.randomUUID()), - Users.newRandomOfficer(), - ZonedDateTime.now(), - description); - } -} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java new file mode 100644 index 0000000..04744f4 --- /dev/null +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java @@ -0,0 +1,75 @@ +package com.example.copsboot.report.web; + + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; + +import java.time.Instant; +import java.util.Set; + +import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; + +public class CreateReportRequestValidatorTest { + //tag::invalid[] + @Test + public void givenTrafficIndicentButInvolvedCarsZero_invalid() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat", + true, + 0, + createImage()); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasViolationOnPath(""); + } + } + //end::invalid[] + + //tag::valid[] + @Test + public void givenTrafficIndicent_involvedCarsMustBePositive() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", + true, + 2, + createImage()); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } + } + //end::valid[] + + //tag::valid-no-cars[] + @Test + public void givenNoTrafficIndicent_involvedCarsDoesNotMatter() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", + false, + 0, + createImage()); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } + } + //end::valid-no-cars[] + + @NotNull + private static MockMultipartFile createImage() { + return new MockMultipartFile("image", "picture.png", MediaType.IMAGE_PNG_VALUE, new byte[]{1, 2, 3}); + } + +} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/KeycloakAdminClientFacade.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/KeycloakAdminClientFacade.java new file mode 100644 index 0000000..c919c86 --- /dev/null +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/KeycloakAdminClientFacade.java @@ -0,0 +1,66 @@ +package com.example.copsboot.report.web; + +import jakarta.ws.rs.core.Response; +import org.keycloak.admin.client.CreatedResponseUtil; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.*; +import org.keycloak.representations.idm.*; + +import java.util.Collections; +import java.util.List; + +public class KeycloakAdminClientFacade { + private final Keycloak keycloak; + + public KeycloakAdminClientFacade(Keycloak keycloak) { + this.keycloak = keycloak; + } + + public void createRealm(String realmName) { + RealmRepresentation realmRepresentation = new RealmRepresentation(); + realmRepresentation.setRealm(realmName); + realmRepresentation.setEnabled(true); + RealmsResource realmsResource = keycloak.realms(); + realmsResource.create(realmRepresentation); + } + + public void createRealmRole(String realmName, String roleName) { + RealmResource copsbootRealm = keycloak.realm(realmName); + RoleRepresentation roleRepresentation = new RoleRepresentation(); + roleRepresentation.setName(roleName); + copsbootRealm.roles().create(roleRepresentation); + } + + public void createUser(String realmName, String username, String password, String roleName) { + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setUsername(username); + userRepresentation.setEnabled(true); + CredentialRepresentation credentialRepresentation = new CredentialRepresentation(); + credentialRepresentation.setTemporary(false); + credentialRepresentation.setType(CredentialRepresentation.PASSWORD); + credentialRepresentation.setValue(password); + userRepresentation.setCredentials(List.of(credentialRepresentation)); + RealmResource realmResource = keycloak.realm(realmName); + UsersResource usersResource = realmResource.users(); + Response response = usersResource.create(userRepresentation); + String userId = CreatedResponseUtil.getCreatedId(response); + + UserResource userResource = usersResource.get(userId); + + userResource.resetPassword(credentialRepresentation); + RoleRepresentation roleRepresentation = realmResource.roles().get(roleName).toRepresentation(); + userResource.roles().realmLevel().add(Collections.singletonList(roleRepresentation)); + } + + public String createClient(String realmName, String clientId1) { + RealmResource realmResource = keycloak.realm(realmName); + ClientRepresentation clientRepresentation = new ClientRepresentation(); + clientRepresentation.setClientId(clientId1); + clientRepresentation.setDirectAccessGrantsEnabled(true); + Response response = realmResource.clients().create(clientRepresentation); + String clientId = CreatedResponseUtil.getCreatedId(response); + ClientResource clientResource = realmResource.clients().get(clientId); + CredentialRepresentation secret = clientResource.getSecret(); + return secret.getValue(); + } +} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java index 0715dc3..b3b6724 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java @@ -1,13 +1,15 @@ package com.example.copsboot.report.web; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.jetbrains.annotations.NotNull; import org.junit.Test; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.time.ZonedDateTime; +import java.time.Instant; import java.util.Set; import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; @@ -17,33 +19,34 @@ public class ReportDescriptionValidatorTest { //tag::invalid[] @Test public void givenEmptyString_notValid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); //<1> - Validator validator = factory.getValidator(); //<2> - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), "", createMockImage()); - Set> violationSet = validator.validate(parameters); //<3> - assertThat(violationSet).hasViolationOnPath("description"); //<4> + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { //<1> + Validator validator = factory.getValidator(); //<2> + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), "", false, 0, + createImage()); + Set> violationSet = validator.validate(parameters); //<3> + assertThat(violationSet).hasViolationOnPath("description"); //<4> + } } //end::invalid[] //tag::valid[] @Test public void givenSuspectWordPresent_valid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), - "The suspect was wearing a black hat.", - createMockImage()); - Set> violationSet = validator.validate(parameters); - assertThat(violationSet).hasNoViolations(); + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", false, 0, + createImage()); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } } //end::valid[] - private MockMultipartFile createMockImage() { - return new MockMultipartFile("image", - "picture.png", - "image/png", - new byte[]{1, 2, 3}); + @NotNull + private static MockMultipartFile createImage() { + return new MockMultipartFile("image", "picture.png", MediaType.IMAGE_PNG_VALUE, new byte[]{1, 2, 3}); } -} \ No newline at end of file +} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportRestControllerIntegrationTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportRestControllerIntegrationTest.java index f89678c..5f34aef 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportRestControllerIntegrationTest.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportRestControllerIntegrationTest.java @@ -1,58 +1,152 @@ package com.example.copsboot.report.web; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AddonsWebmvcTestConf; +import com.c4_soft.springaddons.security.oidc.starter.properties.OpenidProviderProperties; +import com.c4_soft.springaddons.security.oidc.starter.properties.SimpleAuthoritiesMappingProperties; import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.infrastructure.security.SecurityHelperForRestAssured; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; +import com.example.copsboot.user.UserRepository; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import dasniko.testcontainers.keycloak.KeycloakContainer; import io.restassured.RestAssured; import io.restassured.builder.MultiPartSpecBuilder; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import io.restassured.http.ContentType; +import net.bytebuddy.asm.Advice; +import org.junit.jupiter.api.*; +import org.keycloak.admin.client.Keycloak; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.util.Collections; + +import static io.restassured.RestAssured.given; //tag::class[] -@RunWith(SpringRunner.class) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) //<1> -@ActiveProfiles(SpringProfiles.TEST) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) //<.> +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) public class ReportRestControllerIntegrationTest { + private static final String REALM_NAME = "copsboot"; + private static final String ROLE_NAME = "OFFICER"; + private static final String INTEGRATION_TEST_CLIENT_ID = "integration-test-client"; + private static final String TEST_USER_NAME = "wim@example.com"; + private static final String TEST_USER_PASSWORD = "test1234"; + @LocalServerPort - private int serverport; //<2> + private int serverport; //<.> - @Autowired - private UserService userService; + static KeycloakContainer keycloak = new KeycloakContainer("quay.io/keycloak/keycloak:22.0.1"); //<.> + private static String clientSecret; - @Before + @BeforeAll + static void beforeAll() { + keycloak.start(); //<.> + Keycloak client = keycloak.getKeycloakAdminClient(); //<.> + + KeycloakAdminClientFacade clientFacade = new KeycloakAdminClientFacade(client); //<.> + clientFacade.createRealm(REALM_NAME); + clientFacade.createRealmRole(REALM_NAME, ROLE_NAME); + clientFacade.createUser(REALM_NAME, TEST_USER_NAME, TEST_USER_PASSWORD, ROLE_NAME); + clientSecret = clientFacade.createClient(REALM_NAME, INTEGRATION_TEST_CLIENT_ID); + } + + @AfterAll + static void afterAll() { + keycloak.stop(); //<.> + } + + @AfterEach + void afterEach(@Autowired UserRepository userRepository) { + userRepository.deleteAll(); + } + + // tag::configureProperties[] + @DynamicPropertySource + static void configureProperties(DynamicPropertyRegistry registry) { + registry.add("com.c4-soft.springaddons.oidc.ops[0].iss", () -> keycloak.getAuthServerUrl() + "/realms/" + REALM_NAME); //<.> + registry.add("com.c4-soft.springaddons.oidc.ops[0].authorities[0].path", () -> "$.realm_access.roles"); //<.> + registry.add("com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix", () -> "ROLE_"); + } + // end::configureProperties[] + + @BeforeEach public void setup() { - RestAssured.port = serverport; //<3> - RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); //<4> + RestAssured.port = serverport; //<.> + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); //<.> } + // tag::officerIsUnableToPostAReportIfFileSizeIsTooBig[] @Test public void officerIsUnableToPostAReportIfFileSizeIsTooBig() { + String token = getToken(); //<.> + + given() + .header("Authorization", "Bearer " + token) //<.> + .contentType(ContentType.JSON) + .body(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """) + .post("/api/users") //<.> + .then() + .statusCode(HttpStatus.CREATED.value()); //<.> - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> + given() + .header("Authorization", "Bearer " + token) + .multiPart(new MultiPartSpecBuilder(new byte[2_000_000]) //<.> + .fileName("picture.png") + .controlName("image") + .mimeType("image/png") + .build()) + .formParam("dateTime", "2018-04-11T22:59:03.189+02:00") + .formParam("description", "The suspect is wearing a black hat.") + .formParam("trafficIncident", "false") + .formParam("numberOfInvolvedCars", "0") + .when() + .post("/api/reports") + .then() + .statusCode(HttpStatus.PAYLOAD_TOO_LARGE.value()); //<.> + } + // end::officerIsUnableToPostAReportIfFileSizeIsTooBig[] + + // tag::getToken[] + private String getToken() { + RestTemplate restTemplate = new RestTemplate(); //<.> + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - String dateTime = "2018-04-11T22:59:03.189+02:00"; - String description = "The suspect is wearing a black hat."; + MultiValueMap map = new LinkedMultiValueMap<>(); + map.put("grant_type", Collections.singletonList("password")); //<.> + map.put("client_id", Collections.singletonList(INTEGRATION_TEST_CLIENT_ID)); + map.put("client_secret", Collections.singletonList(clientSecret)); + map.put("username", Collections.singletonList(TEST_USER_NAME)); + map.put("password", Collections.singletonList(TEST_USER_PASSWORD)); + KeycloakToken token = + restTemplate.postForObject( + keycloak.getAuthServerUrl() + "/realms/" + REALM_NAME + "/protocol/openid-connect/token", //<.> + new HttpEntity<>(map, httpHeaders), + KeycloakToken.class); + + assert token != null; + return token.accessToken(); //<.> + } - SecurityHelperForRestAssured.givenAuthenticatedUser(serverport, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD) //<6> - .when() - .multiPart("image", new MultiPartSpecBuilder(new byte[2_000_000]) //<7> - .fileName("picture.png") - .mimeType("image/png") - .build()) - .formParam("dateTime", dateTime) - .formParam("description", description) - .post("/api/reports") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()); //<8> + private record KeycloakToken(@JsonProperty("access_token") String accessToken) { //<.> } + // end::getToken[] } // end::class[] diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java index 11aa8df..88f289a 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java @@ -1,69 +1,76 @@ package com.example.copsboot.report.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; import com.example.copsboot.report.ReportService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.time.ZonedDateTime; +import java.time.Instant; +import java.util.Optional; import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; //tag::class[] -@RunWith(SpringRunner.class) @CopsbootControllerTest(ReportRestController.class) public class ReportRestControllerTest { @Autowired - private MockMvc mvc; - + private MockMvc mockMvc; @MockBean private ReportService service; + @MockBean + private UserService userService; @Test public void officerIsAbleToPostAReport() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - String dateTime = "2018-04-11T22:59:03.189+02:00"; - String description = "The suspect is wearing a black hat."; - MockMultipartFile image = createMockImage(); - when(service.createReport(eq(Users.officer().getId()), - any(ZonedDateTime.class), - eq(description), - any(MockMultipartFile.class))) - .thenReturn(new Report(new ReportId(UUID.randomUUID()), Users.officer(), ZonedDateTime.parse(dateTime), description)); - - mvc.perform(fileUpload("/api/reports") //<1> - .file(image) //<2> - .header(HEADER_AUTHORIZATION, bearer(accessToken)) - .param("dateTime", dateTime) //<3> - .param("description", description)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("reporter").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("dateTime").value(dateTime)) - .andExpect(jsonPath("description").value(description)); - } - private MockMultipartFile createMockImage() { //<4> - return new MockMultipartFile("image", - "picture.png", - "image/png", - new byte[10_000_000]); + UserId userId = new UserId(UUID.randomUUID()); + AuthServerId authServerId = new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); + User user = new User(userId, + "wim@example.com", + authServerId, + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"); + when(userService.findUserByAuthServerId(authServerId)) + .thenReturn(Optional.of(user)); + when(userService.getUserById(userId)) + .thenReturn(user); + when(service.createReport(any(CreateReportParameters.class))) + .thenReturn(new Report(new ReportId(UUID.randomUUID()), + userId, + Instant.parse("2023-04-11T22:59:03.189+02:00"), + "This is a test report description. The suspect was wearing a black hat.")); + mockMvc.perform(multipart("/api/reports") //<.> + .file(new MockMultipartFile("image", "picture.png", MediaType.IMAGE_PNG_VALUE, new byte[]{1,2,3})) //<.> + .param("dateTime", "2023-04-11T22:59:03.189+02:00") //<.> + .param("description", "This is a test report description. The suspect was wearing a black hat.") + .param("trafficIncident", "false") + .param("numberOfInvolvedCars", "0") + .with(jwt().jwt(builder -> builder.subject(authServerId.value().toString()) + .claim("email", "wim@example.com")) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("id").exists()) + .andExpect(jsonPath("reporter").value("wim@example.com")) + .andExpect(jsonPath("dateTime").value("2023-04-11T20:59:03.189Z")) + .andExpect(jsonPath("description").value("This is a test report description. The suspect was wearing a black hat.")); } } -//end::class[] \ No newline at end of file +//end::class[] diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java deleted file mode 100644 index 720f959..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.copsboot.user; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.HashSet; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> -@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) //<2> -public class UserRepositoryIntegrationTest { - @Autowired - private UserRepository repository; - @PersistenceContext - private EntityManager entityManager; - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void testSaveUser() { - Set roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); - - assertThat(repository.count()).isEqualTo(1L); - - entityManager.flush(); //<3> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM user_roles", Long.class)).isEqualTo(1L); - assertThat(jdbcTemplate.queryForObject("SELECT roles FROM user_roles", String.class)).isEqualTo("OFFICER"); - } -} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..19c23fe 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -3,14 +3,16 @@ import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,62 +21,34 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) //<2> public class UserRepositoryTest { @Autowired private UserRepository repository; + @PersistenceContext + private EntityManager entityManager; + @Autowired + private JdbcTemplate jdbcTemplate; - //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } + assertThat(repository.count()).isEqualTo(1L); - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); + entityManager.flush(); //<3> - assertThat(optional).isEmpty(); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> + assertThat(jdbcTemplate.queryForObject("SELECT email FROM copsboot_user", String.class)).isEqualTo("alex.foley@beverly-hills.com"); } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @Bean @@ -82,5 +56,4 @@ public UniqueIdGenerator generator() { return new InMemoryUniqueIdGenerator(); } } - //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/Users.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java deleted file mode 100644 index 7b94df3..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserService; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.validation.ConstraintViolation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; - -import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; -import static org.mockito.Mockito.when; - -//tag::class[] -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@ActiveProfiles(SpringProfiles.TEST) -public class CreateUserParametersValidatorTest { - - @MockBean - private UserService userService; //<2> - @Autowired - private PasswordEncoder encoder; - @Autowired - private ValidatorFactory factory; //<3> - - @Test - public void invalidIfAlreadyUserWithGivenEmail() { - - String email = "wim.deblauwe@example.com"; - when(userService.findUserByEmail(email)) - .thenReturn(Optional.of( - User.createOfficer(new UserId(UUID.randomUUID()), - email, - encoder.encode("testing1234")))); - - Validator validator = factory.getValidator(); //<4> - - CreateOfficerParameters userParameters = new CreateOfficerParameters(); - userParameters.setEmail(email); - userParameters.setPassword("my-secret-pwd-1234"); - Set> violationSet = validator.validate(userParameters); //<5> - assertThat(violationSet).hasViolationSize(2) - .hasViolationOnPath("email"); //<6> - } - - @Test - public void validIfNoUserWithGivenEmail() { - String email = "wim.deblauwe@example.com"; - when(userService.findUserByEmail(email)) - .thenReturn(Optional.empty()); - - Validator validator = factory.getValidator(); - - CreateOfficerParameters userParameters = new CreateOfficerParameters(); - userParameters.setEmail(email); - userParameters.setPassword("my-secret-pwd-1234"); - Set> violationSet = validator.validate(userParameters); - assertThat(violationSet).hasNoViolations(); - } -} -//end::class[] \ No newline at end of file diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java new file mode 100644 index 0000000..d058abd --- /dev/null +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java @@ -0,0 +1,65 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.infrastructure.SpringProfiles; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; +import static org.mockito.Mockito.when; + +//tag::class[] +@SpringBootTest //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) +public class CreateUserRequestValidatorTest { + + @MockBean + private UserService userService; //<2> + @Autowired + private ValidatorFactory factory; //<3> + + @Test + public void invalidIfAlreadyUserWithGivenMobileToken() { + + String mobileToken = "abc123"; + when(userService.findUserByMobileToken(mobileToken)) + .thenReturn(Optional.of(new User(new UserId(UUID.randomUUID()), + "wim@example.com", + new AuthServerId(UUID.randomUUID()), + mobileToken))); + + Validator validator = factory.getValidator(); //<4> + + CreateUserRequest request = new CreateUserRequest(mobileToken); + Set> violationSet = validator.validate(request); //<5> + assertThat(violationSet).hasViolationSize(2) + .hasViolationOnPath("mobileToken"); //<6> + } + + @Test + public void validIfNoUserWithGivenMobileToken() { + String mobileToken = "abc123"; + when(userService.findUserByMobileToken(mobileToken)) + .thenReturn(Optional.empty()); + + Validator validator = factory.getValidator(); + + CreateUserRequest request = new CreateUserRequest(mobileToken); + Set> violationSet = validator.validate(request); + assertThat(violationSet).hasNoViolations(); + } +} +//end::class[] diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index b1c3165..805c501 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,134 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index f55e6c6..a20d744 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,121 +1,104 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } + // tag::emptyToken[] @Test - public void testCreateOfficerIfPasswordIsTooShort() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("errors[0].fieldName").value("password")); - - verify(service, never()).createOfficer(email, password); + void givenEmptyMobileToken_badRequestIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "" + } + """)) //<.> + .andExpect(status().isBadRequest()) //<.> + .andDo(print()); //<.> + + verify(userService, never()).createUser(any(CreateUserParameters.class)); //<.> } + // end::emptyToken[] } diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java index 77c5f4c..21556a5 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java @@ -2,7 +2,8 @@ import org.assertj.core.api.AbstractAssert; -import javax.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolation; + import java.util.Set; import java.util.stream.Collectors; @@ -21,12 +22,12 @@ public ConstraintViolationSetAssert hasViolationOnPath(String path) { // check condition if (!containsViolationWithPath(actual, path)) { failWithMessage("There was no violation with path <%s>. Violation paths: <%s>", path, actual.stream() - .map(violation -> violation - .getPropertyPath() - .toString()) - .collect( - Collectors - .toList())); + .map(violation -> violation + .getPropertyPath() + .toString()) + .collect( + Collectors + .toList())); } return this; diff --git a/chapter09/02 - validation/src/test/resources/application-integration-test.properties b/chapter09/02 - validation/src/test/resources/application-integration-test.properties index 159536c..81fd275 100644 --- a/chapter09/02 - validation/src/test/resources/application-integration-test.properties +++ b/chapter09/02 - validation/src/test/resources/application-integration-test.properties @@ -1,11 +1,9 @@ -spring.datasource.url=jdbc:tc:postgresql://localhost/copsbootdb +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.username=user spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#logging.level.org.springframework.security=DEBUG +#logging.level.org.springframework=DEBUG diff --git a/chapter09/02 - validation/src/test/resources/application-repository-test.properties b/chapter09/02 - validation/src/test/resources/application-repository-test.properties new file mode 100644 index 0000000..c61e563 --- /dev/null +++ b/chapter09/02 - validation/src/test/resources/application-repository-test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=user +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter09/02 - validation/src/test/resources/application-test.properties b/chapter09/02 - validation/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter09/02 - validation/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter09/02 - validation/src/test/resources/jwt-officer.json b/chapter09/02 - validation/src/test/resources/jwt-officer.json new file mode 100644 index 0000000..cdf7177 --- /dev/null +++ b/chapter09/02 - validation/src/test/resources/jwt-officer.json @@ -0,0 +1,41 @@ +{ + "exp": 1694870234, + "iat": 1694869934, + "auth_time": 1694865932, + "jti": "7b933105-60b9-43ae-8725-a34bff521858", + "iss": "http://localhost:8180/realms/copsboot", + "aud": "account", + "sub": "eaa8b8a5-a264-48be-98de-d8b4ae2750ac", + "typ": "Bearer", + "azp": "copsboot-mobile-client", + "session_state": "2866bba7-d53f-498e-8830-4dcf0bcb865e", + "acr": "0", + "allowed-origins": [ + "https://oauth.pstmn.io" + ], + "realm_access": { + "roles": [ + "default-roles-copsboot", + "offline_access", + "OFFICER", + "uma_authorization" + ] + }, + "resource_access": { + "account": { + "roles": [ + "manage-account", + "manage-account-links", + "view-profile" + ] + } + }, + "scope": "email profile", + "sid": "2866bba7-d53f-498e-8830-4dcf0bcb865e", + "email_verified": false, + "name": "Wim Example", + "preferred_username": "wim@example.com", + "given_name": "Wim", + "family_name": "Example", + "email": "wim@example.com" +} diff --git a/chapter09/02 - validation/src/test/resources/logback-test.xml b/chapter09/02 - validation/src/test/resources/logback-test.xml index bf47fec..164429c 100644 --- a/chapter09/02 - validation/src/test/resources/logback-test.xml +++ b/chapter09/02 - validation/src/test/resources/logback-test.xml @@ -5,7 +5,7 @@ - + @@ -17,14 +17,8 @@ - - - - - - - \ No newline at end of file + From 7fdd3603121fe8ebe81234c232cb240974153b84 Mon Sep 17 00:00:00 2001 From: Wim Deblauwe Date: Tue, 2 Apr 2024 07:38:24 +0200 Subject: [PATCH 2/2] Add note to readme about the different branches --- README.adoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.adoc b/README.adoc index 3ee66f6..d479d83 100644 --- a/README.adoc +++ b/README.adoc @@ -2,3 +2,6 @@ This repository contains the source code for the InfoQ mini-book 'Practical Guide to Building an API Back End with Spring Boot'. See https://www.infoq.com/minibooks/spring-boot-building-api-backend for the free download of the book. +This branch contains the sources for the latest version of the book, targeting Spring Boot 3. + +If you want to view the sources for the previous edition of the book targeting Spring Boot 2, see the https://github.com/wimdeblauwe/spring-boot-building-api-backend/tree/release/1.x[release/1.x] branch.