diff --git a/service/app/src/test/kotlin/io/miragon/blueprint/adapter/inbound/rest/SelectAlternativeControllerTest.kt b/service/app/src/test/kotlin/io/miragon/blueprint/adapter/inbound/rest/SelectAlternativeControllerTest.kt index 6d950a8..27b1365 100644 --- a/service/app/src/test/kotlin/io/miragon/blueprint/adapter/inbound/rest/SelectAlternativeControllerTest.kt +++ b/service/app/src/test/kotlin/io/miragon/blueprint/adapter/inbound/rest/SelectAlternativeControllerTest.kt @@ -53,4 +53,29 @@ class SelectAlternativeControllerTest { } confirmVerified(useCase) } + + @Test + fun `client reports that no alternative was found`() { + + // given: a decision body with no alternative and no bike details + val pathVar = "123e4567-e89b-12d3-a456-426614174000" + val body = mapOf("alternativeFound" to false) + every { useCase.selectAlternative(any()) } just Runs + val operation = + post("/api/bike-leasing/{applicationId}/clarify-alternative", pathVar) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(body)) + + // when: the request is performed + val response = mockMvc.perform(operation).andReturn() + + // then: the use case is invoked with a command carrying no alternative bike and the response is 202 + assertThat(response.response.status).isEqualTo(202) + verify { + useCase.selectAlternative( + SelectAlternativeUseCase.Command(ApplicationId.of(pathVar), alternativeFound = false, bikeId = null, bikeModel = null), + ) + } + confirmVerified(useCase) + } } diff --git a/service/app/src/test/kotlin/io/miragon/blueprint/adapter/outbound/db/LeasingApplicationEntityMapperTest.kt b/service/app/src/test/kotlin/io/miragon/blueprint/adapter/outbound/db/LeasingApplicationEntityMapperTest.kt new file mode 100644 index 0000000..993236e --- /dev/null +++ b/service/app/src/test/kotlin/io/miragon/blueprint/adapter/outbound/db/LeasingApplicationEntityMapperTest.kt @@ -0,0 +1,76 @@ +package io.miragon.blueprint.adapter.outbound.db + +import io.miragon.blueprint.domain.bike.OrderId +import io.miragon.blueprint.domain.leasing.ContractId +import io.miragon.blueprint.domain.leasing.LeasingStatus +import io.miragon.blueprint.domain.leasing.testLeasingApplication +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class LeasingApplicationEntityMapperTest { + + @Test + fun `toEntity copies every field including the nullable order and contract ids`() { + // given: a fully populated application carrying an order and a contract + val application = testLeasingApplication( + status = LeasingStatus.ACTIVE, + orderId = OrderId("ORDER-1"), + contractId = ContractId("CONTRACT-1"), + ) + + // when: it is mapped to its persistence entity + val entity = LeasingApplicationEntityMapper.toEntity(application) + + // then: each field is carried over unchanged + assertThat(entity.applicationId).isEqualTo(application.id.value) + assertThat(entity.customerName).isEqualTo(application.customerName.value) + assertThat(entity.email).isEqualTo(application.email.value) + assertThat(entity.age).isEqualTo(application.age) + assertThat(entity.monthlyNetIncome).isEqualTo(application.monthlyNetIncome) + assertThat(entity.bikeId).isEqualTo(application.bikeId.value) + assertThat(entity.status).isEqualTo(application.status) + assertThat(entity.createdAt).isEqualTo(application.createdAt) + assertThat(entity.orderId).isEqualTo("ORDER-1") + assertThat(entity.contractId).isEqualTo("CONTRACT-1") + } + + @Test + fun `toDomain reconstructs every field including the nullable order and contract ids`() { + // given: an entity carrying an order and a contract + val entity = LeasingApplicationEntityMapper.toEntity( + testLeasingApplication( + status = LeasingStatus.ACTIVE, + orderId = OrderId("ORDER-1"), + contractId = ContractId("CONTRACT-1"), + ), + ) + + // when: it is mapped back to the domain + val application = LeasingApplicationEntityMapper.toDomain(entity) + + // then: each field is reconstructed unchanged + assertThat(application.id.value).isEqualTo(entity.applicationId) + assertThat(application.customerName.value).isEqualTo(entity.customerName) + assertThat(application.email.value).isEqualTo(entity.email) + assertThat(application.age).isEqualTo(entity.age) + assertThat(application.monthlyNetIncome).isEqualTo(entity.monthlyNetIncome) + assertThat(application.bikeId.value).isEqualTo(entity.bikeId) + assertThat(application.status).isEqualTo(entity.status) + assertThat(application.createdAt).isEqualTo(entity.createdAt) + assertThat(application.orderId).isEqualTo(OrderId("ORDER-1")) + assertThat(application.contractId).isEqualTo(ContractId("CONTRACT-1")) + } + + @Test + fun `toDomain keeps the order and contract ids null when the entity has none`() { + // given: an entity without an order or a contract + val entity = LeasingApplicationEntityMapper.toEntity(testLeasingApplication()) + + // when: it is mapped back to the domain + val application = LeasingApplicationEntityMapper.toDomain(entity) + + // then: both optional references stay null + assertThat(application.orderId).isNull() + assertThat(application.contractId).isNull() + } +} diff --git a/service/app/src/test/kotlin/io/miragon/blueprint/application/service/CancelContractServiceTest.kt b/service/app/src/test/kotlin/io/miragon/blueprint/application/service/CancelContractServiceTest.kt index 0ec9185..f5fe814 100644 --- a/service/app/src/test/kotlin/io/miragon/blueprint/application/service/CancelContractServiceTest.kt +++ b/service/app/src/test/kotlin/io/miragon/blueprint/application/service/CancelContractServiceTest.kt @@ -2,6 +2,7 @@ package io.miragon.blueprint.application.service import io.miragon.blueprint.application.port.outbound.ContractPort import io.miragon.blueprint.application.port.outbound.LeasingApplicationRepository +import io.miragon.blueprint.domain.leasing.ApplicationId import io.miragon.blueprint.domain.leasing.ContractId import io.miragon.blueprint.domain.leasing.testLeasingApplication import io.mockk.Runs @@ -10,6 +11,7 @@ import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.verify +import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test class CancelContractServiceTest { @@ -34,4 +36,32 @@ class CancelContractServiceTest { verify { contract.revokeContract(ContractId("CONTRACT-1")) } confirmVerified(repository, contract) } + + @Test + fun `cancelContract fails when the application is unknown`() { + + // given: an id the repository cannot resolve + val id = ApplicationId.of("123e4567-e89b-12d3-a456-426614174000") + every { repository.findById(id) } returns null + + // when/then: cancellation fails and no contract is touched + assertThatThrownBy { underTest.cancelContract(id) } + .isInstanceOf(IllegalStateException::class.java) + verify { repository.findById(id) } + confirmVerified(repository, contract) + } + + @Test + fun `cancelContract fails when no contract was issued for the application`() { + + // given: an application that never received a contract + val application = testLeasingApplication(contractId = null) + every { repository.findById(application.id) } returns application + + // when/then: cancellation fails and no contract is touched + assertThatThrownBy { underTest.cancelContract(application.id) } + .isInstanceOf(IllegalStateException::class.java) + verify { repository.findById(application.id) } + confirmVerified(repository, contract) + } } diff --git a/service/app/src/test/kotlin/io/miragon/blueprint/domain/bike/BikeIdTest.kt b/service/app/src/test/kotlin/io/miragon/blueprint/domain/bike/BikeIdTest.kt new file mode 100644 index 0000000..4d3b3bb --- /dev/null +++ b/service/app/src/test/kotlin/io/miragon/blueprint/domain/bike/BikeIdTest.kt @@ -0,0 +1,23 @@ +package io.miragon.blueprint.domain.bike + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test + +class BikeIdTest { + + @Test + fun `exposes the wrapped bike id`() { + // given/when: a bike id is created from a non-blank value + val bikeId = BikeId("BIKE-900") + // then: the raw value is exposed unchanged + assertThat(bikeId.value).isEqualTo("BIKE-900") + } + + @Test + fun `rejects a blank bike id`() { + // when/then: a blank value is refused + assertThatThrownBy { BikeId(" ") } + .isInstanceOf(IllegalArgumentException::class.java) + } +} diff --git a/service/app/src/test/kotlin/io/miragon/blueprint/domain/bike/OrderIdTest.kt b/service/app/src/test/kotlin/io/miragon/blueprint/domain/bike/OrderIdTest.kt new file mode 100644 index 0000000..0fceb24 --- /dev/null +++ b/service/app/src/test/kotlin/io/miragon/blueprint/domain/bike/OrderIdTest.kt @@ -0,0 +1,23 @@ +package io.miragon.blueprint.domain.bike + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test + +class OrderIdTest { + + @Test + fun `exposes the wrapped order id`() { + // given/when: an order id is created from a non-blank value + val orderId = OrderId("ORDER-1") + // then: the raw value is exposed unchanged + assertThat(orderId.value).isEqualTo("ORDER-1") + } + + @Test + fun `rejects a blank order id`() { + // when/then: a blank value is refused + assertThatThrownBy { OrderId(" ") } + .isInstanceOf(IllegalArgumentException::class.java) + } +} diff --git a/service/app/src/test/kotlin/io/miragon/blueprint/domain/leasing/ApplicationInvalidExceptionTest.kt b/service/app/src/test/kotlin/io/miragon/blueprint/domain/leasing/ApplicationInvalidExceptionTest.kt new file mode 100644 index 0000000..593b35c --- /dev/null +++ b/service/app/src/test/kotlin/io/miragon/blueprint/domain/leasing/ApplicationInvalidExceptionTest.kt @@ -0,0 +1,21 @@ +package io.miragon.blueprint.domain.leasing + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.util.UUID + +class ApplicationInvalidExceptionTest { + + @Test + fun `carries the reason and a message naming the application`() { + // given: an application id and a rejection reason + val id = ApplicationId(UUID.fromString("123e4567-e89b-12d3-a456-426614174000")) + // when: the exception is raised + val exception = ApplicationInvalidException(id, "no income") + // then: reason and composed message are exposed + assertThat(exception.reason).isEqualTo("no income") + assertThat(exception.applicationId).isEqualTo(id) + assertThat(exception.message) + .isEqualTo("Application 123e4567-e89b-12d3-a456-426614174000 is invalid: no income") + } +} diff --git a/service/app/src/test/kotlin/io/miragon/blueprint/domain/leasing/ContractIdTest.kt b/service/app/src/test/kotlin/io/miragon/blueprint/domain/leasing/ContractIdTest.kt new file mode 100644 index 0000000..b231252 --- /dev/null +++ b/service/app/src/test/kotlin/io/miragon/blueprint/domain/leasing/ContractIdTest.kt @@ -0,0 +1,23 @@ +package io.miragon.blueprint.domain.leasing + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test + +class ContractIdTest { + + @Test + fun `exposes the wrapped contract id`() { + // given/when: a contract id is created from a non-blank value + val contractId = ContractId("CONTRACT-1") + // then: the raw value is exposed unchanged + assertThat(contractId.value).isEqualTo("CONTRACT-1") + } + + @Test + fun `rejects a blank contract id`() { + // when/then: a blank value is refused + assertThatThrownBy { ContractId(" ") } + .isInstanceOf(IllegalArgumentException::class.java) + } +} diff --git a/service/app/src/test/kotlin/io/miragon/blueprint/domain/leasing/CustomerNameTest.kt b/service/app/src/test/kotlin/io/miragon/blueprint/domain/leasing/CustomerNameTest.kt new file mode 100644 index 0000000..2d88c30 --- /dev/null +++ b/service/app/src/test/kotlin/io/miragon/blueprint/domain/leasing/CustomerNameTest.kt @@ -0,0 +1,30 @@ +package io.miragon.blueprint.domain.leasing + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test + +class CustomerNameTest { + + @Test + fun `exposes the wrapped name`() { + // given/when: a customer name is created from a non-blank value + val name = CustomerName("John Doe") + // then: the raw value is exposed unchanged + assertThat(name.value).isEqualTo("John Doe") + } + + @Test + fun `rejects an empty name`() { + // when/then: an empty value is refused + assertThatThrownBy { CustomerName("") } + .isInstanceOf(IllegalArgumentException::class.java) + } + + @Test + fun `rejects a blank name`() { + // when/then: a whitespace-only value is refused + assertThatThrownBy { CustomerName(" ") } + .isInstanceOf(IllegalArgumentException::class.java) + } +} diff --git a/service/app/src/test/kotlin/io/miragon/blueprint/domain/leasing/EmailTest.kt b/service/app/src/test/kotlin/io/miragon/blueprint/domain/leasing/EmailTest.kt new file mode 100644 index 0000000..ab9b088 --- /dev/null +++ b/service/app/src/test/kotlin/io/miragon/blueprint/domain/leasing/EmailTest.kt @@ -0,0 +1,30 @@ +package io.miragon.blueprint.domain.leasing + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test + +class EmailTest { + + @Test + fun `exposes a valid email address`() { + // given/when: an email is created from a well-formed address + val email = Email("john.doe@test.com") + // then: the raw value is exposed unchanged + assertThat(email.value).isEqualTo("john.doe@test.com") + } + + @Test + fun `rejects a malformed address`() { + // when/then: a value that is not an email is refused + assertThatThrownBy { Email("not-an-email") } + .isInstanceOf(IllegalArgumentException::class.java) + } + + @Test + fun `rejects an empty address`() { + // when/then: an empty value is refused + assertThatThrownBy { Email("") } + .isInstanceOf(IllegalArgumentException::class.java) + } +} diff --git a/service/app/src/test/kotlin/io/miragon/blueprint/domain/leasing/LeasingApplicationTest.kt b/service/app/src/test/kotlin/io/miragon/blueprint/domain/leasing/LeasingApplicationTest.kt index f4a107c..b6c7b20 100644 --- a/service/app/src/test/kotlin/io/miragon/blueprint/domain/leasing/LeasingApplicationTest.kt +++ b/service/app/src/test/kotlin/io/miragon/blueprint/domain/leasing/LeasingApplicationTest.kt @@ -8,6 +8,15 @@ import org.junit.jupiter.api.Test class LeasingApplicationTest { + @Test + fun `exposes the applicant's age and monthly net income`() { + // given: an application built with a specific age and income + val application = testLeasingApplication(age = 40, monthlyNetIncome = 4200.0) + // then: both fields are exposed unchanged + assertThat(application.age).isEqualTo(40) + assertThat(application.monthlyNetIncome).isEqualTo(4200.0) + } + @Test fun `documentOrder attaches the order id and moves to ORDERED`() { // given: a received application