Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ interface UserAccountRepository {
fun deleteById(id: UUID)
fun loadUserProfileById(id: UUID): UserProfileDto?
fun loadFullUserAccountById(id: UUID): UserAccountDto?
fun updatePassword(id: UUID, newHashedPassword: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package br.all.application.user.update

import br.all.application.user.update.ChangeAccountPasswordService.ResponseModel
import br.all.domain.shared.presenter.GenericPresenter

interface ChangeAccountPasswordPresenter : GenericPresenter<ResponseModel>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package br.all.application.user.update

import io.swagger.v3.oas.annotations.media.Schema
import java.util.UUID

interface ChangeAccountPasswordService {
fun changePassword(presenter: ChangeAccountPasswordPresenter, request: RequestModel)

data class RequestModel(
val userId: UUID,
val oldPassword: String,
val newPassword: String,
val confirmPassword: String
)

@Schema(name = "ChangeAccountPasswordServiceResponseModel", description = "Response model for Change Account Password Service")
data class ResponseModel(
val userId: UUID
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package br.all.application.user.update

import br.all.application.user.repository.UserAccountRepository
import br.all.domain.shared.service.PasswordEncoderPort

class ChangeAccountPasswordServiceImpl(
private val repository: UserAccountRepository,
private val encoder: PasswordEncoderPort
) : ChangeAccountPasswordService {
override fun changePassword(
presenter: ChangeAccountPasswordPresenter,
request: ChangeAccountPasswordService.RequestModel
) {
val userCredentials = repository.loadCredentialsById(request.userId)
if (userCredentials == null) {
presenter.prepareFailView(NoSuchElementException("User with id ${request.userId} doesn't exist!"))
return
}

if (!encoder.matches(request.oldPassword, userCredentials.password)) {
presenter.prepareFailView(IllegalArgumentException("Invalid old password provided!"))
return
}

if (request.newPassword != request.confirmPassword) {
presenter.prepareFailView(IllegalArgumentException("Confirm password does not match new password!"))
return
}

if (encoder.matches(request.newPassword, userCredentials.password)) {
presenter.prepareFailView(IllegalArgumentException("New password cannot be the same as the old password!"))
return
}

val newHashedPassword = encoder.encode(request.newPassword)

try {
repository.updatePassword(request.userId, newHashedPassword)
presenter.prepareSuccessView(ChangeAccountPasswordService.ResponseModel(
request.userId,
))
} catch (e: Exception) {
presenter.prepareFailView(e)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ class UserAccountRepositoryImpl(
)
}

override fun updatePassword(id: UUID, newHashedPassword: String) {
val credentials = credentialsRepository.findById(id).orElse(null)
?: throw NoSuchElementException("Cannot update password. Credentials for user with id $id not found!")

credentials.password = newHashedPassword
credentialsRepository.save(credentials)
}

override fun loadCredentialsByUsername(username: String) =
credentialsRepository.findByUsername(username)?.toAccountCredentialsDto()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package br.all.application.user.update

import br.all.application.user.repository.UserAccountRepository
import br.all.application.user.utils.TestDataFactory
import br.all.domain.shared.service.PasswordEncoderPort
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.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertIs

@Tag("UnitTest")
@Tag("ServiceTest")
@ExtendWith(MockKExtension::class)
class ChangeAccountPasswordServiceImplTest {

@MockK(relaxUnitFun = true)
private lateinit var repository: UserAccountRepository

@MockK
private lateinit var encoder: PasswordEncoderPort

@MockK(relaxUnitFun = true)
private lateinit var presenter: ChangeAccountPasswordPresenter

private lateinit var sut: ChangeAccountPasswordServiceImpl
private lateinit var factory: TestDataFactory

@BeforeEach
fun setup() {
sut = ChangeAccountPasswordServiceImpl(repository, encoder)
factory = TestDataFactory()
}

@Nested
@DisplayName("When changing a user's password")
inner class WhenChangingPassword {

@Test
fun `should update password when all inputs are valid`() {
val userCredentials = factory.accountCredentials().copy(password = "oldHashedPassword")
val request = ChangeAccountPasswordService.RequestModel(
userId = userCredentials.id,
oldPassword = "plainOldPassword",
newPassword = "plainNewPassword",
confirmPassword = "plainNewPassword"
)
val newHashedPassword = "newHashedPassword"

every { repository.loadCredentialsById(userCredentials.id) } returns userCredentials
every { encoder.matches(request.oldPassword, userCredentials.password) } returns true
every { encoder.matches(request.newPassword, userCredentials.password) } returns false
every { encoder.encode(request.newPassword) } returns newHashedPassword

val userIdSlot = slot<UUID>()
val passwordSlot = slot<String>()
every { repository.updatePassword(capture(userIdSlot), capture(passwordSlot)) } returns Unit

val responseSlot = slot<ChangeAccountPasswordService.ResponseModel>()
every { presenter.prepareSuccessView(capture(responseSlot)) } returns Unit

sut.changePassword(presenter, request)

verify(exactly = 1) { presenter.prepareSuccessView(any()) }
verify(exactly = 0) { presenter.prepareFailView(any()) }
verify(exactly = 1) { repository.updatePassword(userCredentials.id, newHashedPassword) }

assertEquals(userCredentials.id, userIdSlot.captured)
assertEquals(newHashedPassword, passwordSlot.captured)
assertEquals(userCredentials.id, responseSlot.captured.userId)
}

@Test
fun `should prepare fail view when user does not exist`() {
val nonExistentUserId = UUID.randomUUID()
val request = ChangeAccountPasswordService.RequestModel(
userId = nonExistentUserId,
oldPassword = "any", newPassword = "any", confirmPassword = "any"
)

every { repository.loadCredentialsById(nonExistentUserId) } returns null

val exceptionSlot = slot<Exception>()
every { presenter.prepareFailView(capture(exceptionSlot)) } returns Unit

sut.changePassword(presenter, request)

verify(exactly = 1) { presenter.prepareFailView(any()) }
verify(exactly = 0) { presenter.prepareSuccessView(any()) }
verify(exactly = 0) { repository.updatePassword(any(), any()) }

assertIs<NoSuchElementException>(exceptionSlot.captured)
assertEquals("User with id $nonExistentUserId doesn't exist!", exceptionSlot.captured.message)
}

@Test
fun `should prepare fail view when old password is incorrect`() {
val userCredentials = factory.accountCredentials().copy(password = "oldHashedPassword")
val request = ChangeAccountPasswordService.RequestModel(
userId = userCredentials.id,
oldPassword = "wrongOldPassword",
newPassword = "plainNewPassword",
confirmPassword = "plainNewPassword"
)

every { repository.loadCredentialsById(userCredentials.id) } returns userCredentials
every { encoder.matches(request.oldPassword, userCredentials.password) } returns false

val exceptionSlot = slot<Exception>()
every { presenter.prepareFailView(capture(exceptionSlot)) } returns Unit

sut.changePassword(presenter, request)

verify(exactly = 1) { presenter.prepareFailView(any()) }
verify(exactly = 0) { presenter.prepareSuccessView(any()) }

assertIs<IllegalArgumentException>(exceptionSlot.captured)
assertEquals("Invalid old password provided!", exceptionSlot.captured.message)
}

@Test
fun `should prepare fail view when new password and confirmation do not match`() {
val userCredentials = factory.accountCredentials().copy(password = "oldHashedPassword")
val request = ChangeAccountPasswordService.RequestModel(
userId = userCredentials.id,
oldPassword = "plainOldPassword",
newPassword = "plainNewPassword",
confirmPassword = "doesNotMatch"
)

every { repository.loadCredentialsById(userCredentials.id) } returns userCredentials
every { encoder.matches(request.oldPassword, userCredentials.password) } returns true

val exceptionSlot = slot<Exception>()
every { presenter.prepareFailView(capture(exceptionSlot)) } returns Unit

sut.changePassword(presenter, request)

verify(exactly = 1) { presenter.prepareFailView(any()) }
verify(exactly = 0) { presenter.prepareSuccessView(any()) }

assertIs<IllegalArgumentException>(exceptionSlot.captured)
assertEquals("Confirm password does not match new password!", exceptionSlot.captured.message)
}

@Test
fun `should prepare fail view when new password is the same as the old one`() {
val userCredentials = factory.accountCredentials().copy(password = "oldHashedPassword")
val request = ChangeAccountPasswordService.RequestModel(
userId = userCredentials.id,
oldPassword = "plainOldPassword",
newPassword = "plainOldPassword",
confirmPassword = "plainOldPassword"
)

every { repository.loadCredentialsById(userCredentials.id) } returns userCredentials
every { encoder.matches(request.oldPassword, userCredentials.password) } returns true
every { encoder.matches(request.newPassword, userCredentials.password) } returns true

val exceptionSlot = slot<Exception>()
every { presenter.prepareFailView(capture(exceptionSlot)) } returns Unit

sut.changePassword(presenter, request)

verify(exactly = 1) { presenter.prepareFailView(any()) }
verify(exactly = 0) { presenter.prepareSuccessView(any()) }

assertIs<IllegalArgumentException>(exceptionSlot.captured)
assertEquals("New password cannot be the same as the old password!", exceptionSlot.captured.message)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package br.all.domain.shared.service

interface PasswordEncoderPort {
fun encode(rawPassword: String): String
fun matches(rawPassword: String, encodedPassword: String): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package br.all.security.service

import br.all.domain.shared.service.PasswordEncoderPort
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Component

@Component
class PasswordEncoderAdapter(
private val encoder: PasswordEncoder
) : PasswordEncoderPort {
override fun encode(rawPassword: String): String {
return encoder.encode(rawPassword)
}

override fun matches(rawPassword: String, encodedPassword: String): Boolean {
return encoder.matches(rawPassword, encodedPassword)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ 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 br.all.application.user.update.ChangeAccountPasswordServiceImpl
import br.all.application.user.update.PatchUserProfileServiceImpl
import br.all.domain.shared.service.PasswordEncoderPort
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

Expand All @@ -18,4 +20,7 @@ class UserAccountConfiguration {

@Bean
fun patchUserProfile(repository: UserAccountRepository) = PatchUserProfileServiceImpl(repository)

@Bean
fun changeAccountPassword(repository: UserAccountRepository, encoder: PasswordEncoderPort) = ChangeAccountPasswordServiceImpl(repository, encoder)
}
Loading