diff --git a/account/src/main/kotlin/br/all/application/user/find/RetrieveUserProfilePresenter.kt b/account/src/main/kotlin/br/all/application/user/find/RetrieveUserProfilePresenter.kt new file mode 100644 index 000000000..e70cf8c96 --- /dev/null +++ b/account/src/main/kotlin/br/all/application/user/find/RetrieveUserProfilePresenter.kt @@ -0,0 +1,6 @@ +package br.all.application.user.find + +import br.all.application.user.find.RetrieveUserProfileService.ResponseModel +import br.all.domain.shared.presenter.GenericPresenter + +interface RetrieveUserProfilePresenter : GenericPresenter \ No newline at end of file diff --git a/account/src/main/kotlin/br/all/application/user/find/RetrieveUserProfileService.kt b/account/src/main/kotlin/br/all/application/user/find/RetrieveUserProfileService.kt new file mode 100644 index 000000000..2016e2c86 --- /dev/null +++ b/account/src/main/kotlin/br/all/application/user/find/RetrieveUserProfileService.kt @@ -0,0 +1,19 @@ +package br.all.application.user.find +import java.util.UUID + +interface RetrieveUserProfileService { + fun retrieveData(presenter: RetrieveUserProfilePresenter, request: RequestModel) + + data class RequestModel( + val userId: UUID + ) + + data class ResponseModel( + val userId: UUID, + val username: String, + val email: String, + val affiliation: String, + val country: String, + val authorities: Set, + ) +} \ No newline at end of file diff --git a/account/src/main/kotlin/br/all/application/user/find/RetrieveUserProfileServiceImpl.kt b/account/src/main/kotlin/br/all/application/user/find/RetrieveUserProfileServiceImpl.kt new file mode 100644 index 000000000..f382bc86a --- /dev/null +++ b/account/src/main/kotlin/br/all/application/user/find/RetrieveUserProfileServiceImpl.kt @@ -0,0 +1,38 @@ +package br.all.application.user.find + +import br.all.application.user.find.RetrieveUserProfileService.RequestModel +import br.all.application.user.find.RetrieveUserProfileService.ResponseModel +import br.all.application.user.repository.UserAccountRepository + +class RetrieveUserProfileServiceImpl( + private val userAccountRepository: UserAccountRepository +) : RetrieveUserProfileService { + override fun retrieveData( + presenter: RetrieveUserProfilePresenter, + request: RequestModel + ) { + val userProfile = userAccountRepository.loadUserProfileById(request.userId) + if (userProfile == null) { + presenter.prepareFailView(NoSuchElementException("User with id ${request.userId} doesn't exist!")) + return + } + + val userCredentials = userAccountRepository.loadCredentialsById(request.userId) + if (userCredentials == null) { + presenter.prepareFailView(NoSuchElementException("Account credentials with id ${request.userId} doesn't exist!")) + return + } + + + val profile = ResponseModel( + userId = userProfile.id, + username = userCredentials.username, + email = userProfile.email, + affiliation = userProfile.affiliation, + country = userProfile.country, + authorities = userCredentials.authorities + ) + + presenter.prepareSuccessView(profile) + } +} \ No newline at end of file diff --git a/account/src/main/kotlin/br/all/application/user/repository/UserAccountRepository.kt b/account/src/main/kotlin/br/all/application/user/repository/UserAccountRepository.kt index 52531acd7..d14aa597a 100644 --- a/account/src/main/kotlin/br/all/application/user/repository/UserAccountRepository.kt +++ b/account/src/main/kotlin/br/all/application/user/repository/UserAccountRepository.kt @@ -11,4 +11,5 @@ interface UserAccountRepository { fun existsByEmail(email: String): Boolean fun existsByUsername(username: String): Boolean fun deleteById(id: UUID) + fun loadUserProfileById(id: UUID): UserProfileDto? } \ No newline at end of file diff --git a/account/src/main/kotlin/br/all/application/user/repository/UserProfileDto.kt b/account/src/main/kotlin/br/all/application/user/repository/UserProfileDto.kt new file mode 100644 index 000000000..1060041c4 --- /dev/null +++ b/account/src/main/kotlin/br/all/application/user/repository/UserProfileDto.kt @@ -0,0 +1,10 @@ +package br.all.application.user.repository + +import java.util.UUID + +data class UserProfileDto( + val id: UUID, + val email: String, + val country: String, + val affiliation: String, +) diff --git a/account/src/main/kotlin/br/all/infrastructure/user/UserAccountMapper.kt b/account/src/main/kotlin/br/all/infrastructure/user/UserAccountMapper.kt index f48e3b375..29982ff06 100644 --- a/account/src/main/kotlin/br/all/infrastructure/user/UserAccountMapper.kt +++ b/account/src/main/kotlin/br/all/infrastructure/user/UserAccountMapper.kt @@ -2,6 +2,7 @@ package br.all.infrastructure.user import br.all.application.user.repository.AccountCredentialsDto import br.all.application.user.repository.UserAccountDto +import br.all.application.user.repository.UserProfileDto fun UserAccountDto.toUserAccountEntity(): UserAccountEntity { val credentials = AccountCredentialsEntity( @@ -18,4 +19,11 @@ fun UserAccountDto.toUserAccountEntity(): UserAccountEntity { return UserAccountEntity(id, credentials, email, country, affiliation, createdAt) } -fun AccountCredentialsEntity.toAccountCredentialsDto() = AccountCredentialsDto(id, username, password, authorities, refreshToken) \ No newline at end of file +fun AccountCredentialsEntity.toAccountCredentialsDto() = AccountCredentialsDto(id, username, password, authorities, refreshToken) + +fun UserAccountEntity.toUserProfileDto() = UserProfileDto( + id = this.id, + email = this.email, + country = this.country, + affiliation = this.affiliation +) \ No newline at end of file diff --git a/account/src/main/kotlin/br/all/infrastructure/user/UserAccountRepositoryImpl.kt b/account/src/main/kotlin/br/all/infrastructure/user/UserAccountRepositoryImpl.kt index a3b5fd2b9..8c3d02b06 100644 --- a/account/src/main/kotlin/br/all/infrastructure/user/UserAccountRepositoryImpl.kt +++ b/account/src/main/kotlin/br/all/infrastructure/user/UserAccountRepositoryImpl.kt @@ -3,6 +3,7 @@ package br.all.infrastructure.user import br.all.application.user.repository.AccountCredentialsDto import br.all.application.user.repository.UserAccountDto import br.all.application.user.repository.UserAccountRepository +import br.all.application.user.repository.UserProfileDto import org.springframework.stereotype.Repository import java.util.* @@ -22,6 +23,9 @@ class UserAccountRepositoryImpl( credentialsRepository.deleteById(id) } + override fun loadUserProfileById(id: UUID): UserProfileDto? = + userAccountRepository.findById(id).orElse(null)?.toUserProfileDto() + override fun loadCredentialsByUsername(username: String) = credentialsRepository.findByUsername(username)?.toAccountCredentialsDto() diff --git a/account/src/test/kotlin/br/all/application/user/find/RetrieveUserProfileServiceImplTest.kt b/account/src/test/kotlin/br/all/application/user/find/RetrieveUserProfileServiceImplTest.kt new file mode 100644 index 000000000..3b3ebc4ba --- /dev/null +++ b/account/src/test/kotlin/br/all/application/user/find/RetrieveUserProfileServiceImplTest.kt @@ -0,0 +1,105 @@ +package br.all.application.user.find + +import br.all.application.user.repository.UserAccountRepository +import br.all.application.user.utils.TestDataFactory +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.slot +import io.mockk.verify +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.extension.ExtendWith +import java.util.* +import kotlin.NoSuchElementException + +@Tag("UnitTest") +@Tag("ServiceTest") +@ExtendWith(MockKExtension::class) +class RetrieveUserProfileServiceImplTest { + + @MockK(relaxUnitFun = true) + private lateinit var userAccountRepository: UserAccountRepository + + @MockK(relaxUnitFun = true) + private lateinit var presenter: RetrieveUserProfilePresenter + + private lateinit var sut: RetrieveUserProfileServiceImpl + private lateinit var factory: TestDataFactory + + @BeforeEach + fun setUp() { + sut = RetrieveUserProfileServiceImpl(userAccountRepository) + factory = TestDataFactory() + } + + @Nested + @DisplayName("When retrieving a user profile") + inner class WhenRetrievingUserProfile { + + @Test + fun `should retrieve user profile and prepare success view`() { + val userProfile = factory.userProfile() + val userCredentials = factory.accountCredentials().copy(id = userProfile.id) + val request = RetrieveUserProfileService.RequestModel(userId = userProfile.id) + + every { userAccountRepository.loadUserProfileById(request.userId) } returns userProfile + every { userAccountRepository.loadCredentialsById(request.userId) } returns userCredentials + + val responseSlot = slot() + every { presenter.prepareSuccessView(capture(responseSlot)) } returns Unit + + sut.retrieveData(presenter, request) + + verify(exactly = 1) { presenter.prepareSuccessView(any()) } + verify(exactly = 0) { presenter.prepareFailView(any()) } + + val capturedResponse = responseSlot.captured + assertEquals(userProfile.id, capturedResponse.userId) + assertEquals(userCredentials.username, capturedResponse.username) + assertEquals(userProfile.email, capturedResponse.email) + assertEquals(userProfile.affiliation, capturedResponse.affiliation) + assertEquals(userProfile.country, capturedResponse.country) + assertEquals(userCredentials.authorities, capturedResponse.authorities) + } + + @Test + fun `should prepare fail view when user profile is not found`() { + val userId = UUID.randomUUID() + val request = RetrieveUserProfileService.RequestModel(userId = userId) + + every { userAccountRepository.loadUserProfileById(userId) } returns null + + val exceptionSlot = slot() + every { presenter.prepareFailView(capture(exceptionSlot)) } returns Unit + + sut.retrieveData(presenter, request) + + verify(exactly = 0) { presenter.prepareSuccessView(any()) } + verify(exactly = 1) { presenter.prepareFailView(any()) } + + val capturedException = exceptionSlot.captured + assertEquals("User with id $userId doesn't exist!", capturedException.message) + } + + @Test + fun `should prepare fail view when user credentials are not found`() { + val userProfile = factory.userProfile() + val request = RetrieveUserProfileService.RequestModel(userId = userProfile.id) + + every { userAccountRepository.loadUserProfileById(request.userId) } returns userProfile + every { userAccountRepository.loadCredentialsById(request.userId) } returns null + + val exceptionSlot = slot() + every { presenter.prepareFailView(capture(exceptionSlot)) } returns Unit + + sut.retrieveData(presenter, request) + + verify(exactly = 0) { presenter.prepareSuccessView(any()) } + verify(exactly = 1) { presenter.prepareFailView(any()) } + + val capturedException = exceptionSlot.captured + assertEquals("Account credentials with id ${request.userId} doesn't exist!", capturedException.message) + } + } +} diff --git a/account/src/test/kotlin/br/all/application/user/utils/TestDataFactory.kt b/account/src/test/kotlin/br/all/application/user/utils/TestDataFactory.kt index 0b2e69e64..4bf2c37c3 100644 --- a/account/src/test/kotlin/br/all/application/user/utils/TestDataFactory.kt +++ b/account/src/test/kotlin/br/all/application/user/utils/TestDataFactory.kt @@ -2,6 +2,7 @@ package br.all.application.user.utils import br.all.application.user.create.RegisterUserAccountService import br.all.application.user.repository.AccountCredentialsDto +import br.all.application.user.repository.UserProfileDto import io.github.serpro69.kfaker.Faker import java.util.* @@ -25,4 +26,10 @@ class TestDataFactory { refreshToken = faker.lorem.words() ) + fun userProfile() = UserProfileDto( + id = UUID.randomUUID(), + email = faker.internet.email(), + country = faker.address.countryCode(), + affiliation = faker.leagueOfLegends.rank(), + ) } \ No newline at end of file diff --git a/web/src/main/kotlin/br/all/security/config/JwtAuthenticationFilter.kt b/web/src/main/kotlin/br/all/security/config/JwtAuthenticationFilter.kt index e8988bf86..a1b181363 100644 --- a/web/src/main/kotlin/br/all/security/config/JwtAuthenticationFilter.kt +++ b/web/src/main/kotlin/br/all/security/config/JwtAuthenticationFilter.kt @@ -21,7 +21,7 @@ class JwtAuthenticationFilter( ) : OncePerRequestFilter() { private val matchersToSkip: List = listOf( - AntPathRequestMatcher("/api/v1/user"), + AntPathRequestMatcher("/api/v1/user", "POST"), AntPathRequestMatcher("/api/v1/auth/"), AntPathRequestMatcher("/api/v1/auth/logout/"), AntPathRequestMatcher("/webjars/**"), diff --git a/web/src/main/kotlin/br/all/security/config/SecurityConfiguration.kt b/web/src/main/kotlin/br/all/security/config/SecurityConfiguration.kt index 9f373a709..06d34bc97 100644 --- a/web/src/main/kotlin/br/all/security/config/SecurityConfiguration.kt +++ b/web/src/main/kotlin/br/all/security/config/SecurityConfiguration.kt @@ -44,6 +44,7 @@ class SecurityConfiguration( "/webjars/**" ).permitAll() .requestMatchers(HttpMethod.POST, "/api/v1/user").permitAll() + .requestMatchers(HttpMethod.GET, "/api/v1/user").authenticated() .anyRequest().fullyAuthenticated() } diff --git a/web/src/main/kotlin/br/all/user/controller/UserAccountConfiguration.kt b/web/src/main/kotlin/br/all/user/controller/UserAccountConfiguration.kt index b5113266f..44d088719 100644 --- a/web/src/main/kotlin/br/all/user/controller/UserAccountConfiguration.kt +++ b/web/src/main/kotlin/br/all/user/controller/UserAccountConfiguration.kt @@ -1,6 +1,7 @@ package br.all.user.controller import br.all.application.user.create.RegisterUserAccountServiceImpl +import br.all.application.user.find.RetrieveUserProfileServiceImpl import br.all.application.user.repository.UserAccountRepository import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -10,4 +11,7 @@ class UserAccountConfiguration { @Bean fun registerUser(repository: UserAccountRepository) = RegisterUserAccountServiceImpl(repository) + + @Bean + fun retrieveUserProfile(repository: UserAccountRepository) = RetrieveUserProfileServiceImpl(repository) } \ No newline at end of file diff --git a/web/src/main/kotlin/br/all/user/controller/UserAccountController.kt b/web/src/main/kotlin/br/all/user/controller/UserAccountController.kt index e5973fa04..3919eaef8 100644 --- a/web/src/main/kotlin/br/all/user/controller/UserAccountController.kt +++ b/web/src/main/kotlin/br/all/user/controller/UserAccountController.kt @@ -3,7 +3,10 @@ package br.all.user.controller import br.all.application.user.CredentialsService import br.all.application.user.create.RegisterUserAccountService import br.all.application.user.create.RegisterUserAccountService.RequestModel -import br.all.user.presenter.RestfullRegisterUserAccountPresenter +import br.all.application.user.find.RetrieveUserProfileService +import br.all.security.service.AuthenticationInfoService +import br.all.user.presenter.RestfulRegisterUserAccountPresenter +import br.all.user.presenter.RestfulRetrieveUserProfilePresenter import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema @@ -12,6 +15,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping @@ -21,7 +25,9 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping("api/v1/user") class UserAccountController( private val registerUserAccountService: RegisterUserAccountService, - private val encoder: PasswordEncoder + private val encoder: PasswordEncoder, + private val retrieveUserProfileService: RetrieveUserProfileService, + private val authenticationInfoService: AuthenticationInfoService ) { @PostMapping @@ -49,9 +55,46 @@ class UserAccountController( ] ) fun registerUser(@RequestBody request: RequestModel): ResponseEntity<*> { - val presenter = RestfullRegisterUserAccountPresenter() + val presenter = RestfulRegisterUserAccountPresenter() val encodedPasswordRequest = request.copy(password = encoder.encode(request.password)) registerUserAccountService.register(presenter, encodedPasswordRequest) return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) } + + @GetMapping("/profile") + @Operation(summary = "Retrieve public information of a user") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "Success retrieving user profile", + content = [Content( + mediaType = "application/json", + schema = Schema(implementation = RetrieveUserProfileService.ResponseModel::class) + )] + ), + ApiResponse( + responseCode = "401", + description = "Fail retrieving user profile - unauthenticated collaborator", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "403", + description = "Fail retrieving user profile - unauthorized collaborator", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "404", + description = "Fail retrieving user profile - nonexistent user", + content = [Content(schema = Schema(hidden = true))] + ), + ]) + fun retrieveUserPublicData(): ResponseEntity<*> { + val presenter = RestfulRetrieveUserProfilePresenter() + val userId = authenticationInfoService.getAuthenticatedUserId() + val request = RetrieveUserProfileService.RequestModel(userId) + + retrieveUserProfileService.retrieveData(presenter, request) + return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) + } } \ No newline at end of file diff --git a/web/src/main/kotlin/br/all/user/presenter/RestfullRegisterUserAccountPresenter.kt b/web/src/main/kotlin/br/all/user/presenter/RestfulRegisterUserAccountPresenter.kt similarity index 93% rename from web/src/main/kotlin/br/all/user/presenter/RestfullRegisterUserAccountPresenter.kt rename to web/src/main/kotlin/br/all/user/presenter/RestfulRegisterUserAccountPresenter.kt index b42879b01..03f83317a 100644 --- a/web/src/main/kotlin/br/all/user/presenter/RestfullRegisterUserAccountPresenter.kt +++ b/web/src/main/kotlin/br/all/user/presenter/RestfulRegisterUserAccountPresenter.kt @@ -9,7 +9,7 @@ import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity.status import java.util.* -class RestfullRegisterUserAccountPresenter : RegisterUserAccountPresenter { +class RestfulRegisterUserAccountPresenter : RegisterUserAccountPresenter { var responseEntity: ResponseEntity<*>? = null diff --git a/web/src/main/kotlin/br/all/user/presenter/RestfulRetrieveUserProfilePresenter.kt b/web/src/main/kotlin/br/all/user/presenter/RestfulRetrieveUserProfilePresenter.kt new file mode 100644 index 000000000..9e3fc3156 --- /dev/null +++ b/web/src/main/kotlin/br/all/user/presenter/RestfulRetrieveUserProfilePresenter.kt @@ -0,0 +1,39 @@ +package br.all.user.presenter + +import br.all.application.user.find.RetrieveUserProfileService.ResponseModel +import br.all.application.user.find.RetrieveUserProfilePresenter +import br.all.shared.error.createErrorResponseFrom +import org.springframework.hateoas.RepresentationModel +import org.springframework.http.ResponseEntity +import org.springframework.http.ResponseEntity.ok +import java.util.* + +class RestfulRetrieveUserProfilePresenter : RetrieveUserProfilePresenter { + + var responseEntity: ResponseEntity<*>? = null + + override fun prepareSuccessView(response: ResponseModel) { + val restfulResponse = ViewModel( + response.userId, + response.username, + response.email, + response.affiliation, + response.country, + response.authorities + ) + responseEntity = ok(restfulResponse) + } + + override fun prepareFailView(throwable: Throwable) = run { responseEntity = createErrorResponseFrom(throwable) } + + override fun isDone() = responseEntity != null + + private data class ViewModel( + val userId: UUID, + val username: String, + val email: String, + val affiliation: String, + val country: String, + val authorities: Set + ) : RepresentationModel() +} \ No newline at end of file