From 778384e3be2ca07675d338abf9364a1ea7073af8 Mon Sep 17 00:00:00 2001 From: Pratik Gaur Date: Thu, 15 Jun 2023 21:16:29 +0200 Subject: [PATCH 1/8] running integration tests on random ports --- .../kotlin/io/billie/functional/CanReadLocationsTest.kt | 7 ++----- .../billie/functional/CanStoreAndReadOrganisationTest.kt | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/test/kotlin/io/billie/functional/CanReadLocationsTest.kt b/src/test/kotlin/io/billie/functional/CanReadLocationsTest.kt index 91782d7..c84bc5d 100644 --- a/src/test/kotlin/io/billie/functional/CanReadLocationsTest.kt +++ b/src/test/kotlin/io/billie/functional/CanReadLocationsTest.kt @@ -1,24 +1,21 @@ package io.billie.functional import io.billie.functional.matcher.IsUUID.isUuid -import org.hamcrest.Description -import org.hamcrest.TypeSafeMatcher import org.junit.jupiter.api.Test 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.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT import org.springframework.boot.test.web.server.LocalServerPort import org.springframework.http.MediaType.APPLICATION_JSON import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status -import java.util.* @AutoConfigureMockMvc -@SpringBootTest(webEnvironment = DEFINED_PORT) +@SpringBootTest(webEnvironment = RANDOM_PORT) class CanReadLocationsTest { @LocalServerPort diff --git a/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt b/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt index 2d57630..7d66d2d 100644 --- a/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt +++ b/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt @@ -18,7 +18,7 @@ import org.junit.jupiter.api.Test 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.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT import org.springframework.boot.test.web.server.LocalServerPort import org.springframework.http.MediaType.APPLICATION_JSON import org.springframework.jdbc.core.JdbcTemplate @@ -30,7 +30,7 @@ import java.util.* @AutoConfigureMockMvc -@SpringBootTest(webEnvironment = DEFINED_PORT) +@SpringBootTest(webEnvironment = RANDOM_PORT) class CanStoreAndReadOrganisationTest { @LocalServerPort From d71e47ae807d1285cb167f79fe6163607872611b Mon Sep 17 00:00:00 2001 From: pratikgaur Date: Fri, 23 Jun 2023 02:22:10 +0200 Subject: [PATCH 2/8] Feature 1: Adding an address to an organisation: Happy case --- .../data/CityWithIdDoesNotExist.kt | 6 ++ .../data/OrganisationRepository.kt | 68 ++++++++++++++++++- .../data/OrganisationWithIdDoesNotExist.kt | 5 ++ .../resource/OrganisationResource.kt | 35 ++++++++-- .../service/OrganisationService.kt | 4 ++ .../viewmodel/OrganisationAddressRequest.kt | 18 +++++ .../db/migration/V9__addresses_table.sql | 13 ++++ .../billie/functional/CanReadLocationsTest.kt | 5 -- .../CanStoreAndReadOrganisationTest.kt | 31 +++++++-- .../io/billie/functional/data/Fixtures.kt | 27 +++++++- 10 files changed, 195 insertions(+), 17 deletions(-) create mode 100644 src/main/kotlin/io/billie/organisations/data/CityWithIdDoesNotExist.kt create mode 100644 src/main/kotlin/io/billie/organisations/data/OrganisationWithIdDoesNotExist.kt create mode 100644 src/main/kotlin/io/billie/organisations/viewmodel/OrganisationAddressRequest.kt create mode 100644 src/main/resources/db/migration/V9__addresses_table.sql diff --git a/src/main/kotlin/io/billie/organisations/data/CityWithIdDoesNotExist.kt b/src/main/kotlin/io/billie/organisations/data/CityWithIdDoesNotExist.kt new file mode 100644 index 0000000..8b9e9c7 --- /dev/null +++ b/src/main/kotlin/io/billie/organisations/data/CityWithIdDoesNotExist.kt @@ -0,0 +1,6 @@ +package io.billie.organisations.data + +import java.util.* + + +class CityWithIdDoesNotExist(val cityId: UUID) : RuntimeException() diff --git a/src/main/kotlin/io/billie/organisations/data/OrganisationRepository.kt b/src/main/kotlin/io/billie/organisations/data/OrganisationRepository.kt index 8c0026b..ab93000 100644 --- a/src/main/kotlin/io/billie/organisations/data/OrganisationRepository.kt +++ b/src/main/kotlin/io/billie/organisations/data/OrganisationRepository.kt @@ -35,6 +35,43 @@ class OrganisationRepository { return createOrganisation(organisation, id) } + @Transactional + fun addAddress(orgAddress: OrganisationAddressRequest): UUID { + validateAddress(orgAddress) + return createAddress(orgAddress) + } + + private fun createAddress(orgAddress: OrganisationAddressRequest): UUID { + val keyHolder: KeyHolder = GeneratedKeyHolder() + jdbcTemplate.update( + { connection -> + val ps = connection.prepareStatement( + """ + INSERT INTO organisations_schema.addresses ( + organisation_id, + city_id, + pin_code, + street_name, + plot_number, + floor, + apartment_number + ) VALUES (?, ?, ?, ?, ?, ?, ?) + """.trimIndent(), + arrayOf("id") + ) + ps.setObject(1, orgAddress.organisationId) + ps.setObject(2, orgAddress.cityId) + ps.setString(3, orgAddress.pinCode) + ps.setString(4, orgAddress.streetName) + ps.setString(5, orgAddress.plotNumber) + ps.setString(6, orgAddress.floor) + ps.setString(7, orgAddress.apartmentNumber.toString()) + ps + }, keyHolder + ) + return keyHolder.getKeyAs(UUID::class.java)!! + } + private fun valuesValid(organisation: OrganisationRequest): Boolean { val reply: Int? = jdbcTemplate.query( "select count(country_code) from organisations_schema.countries c WHERE c.country_code = ?", @@ -47,6 +84,36 @@ class OrganisationRepository { return (reply != null) && (reply > 0) } + private fun validateAddress(orgAddress: OrganisationAddressRequest): Boolean { + val orgExists: Boolean? = jdbcTemplate.query( + "select exists(select 1 from organisations_schema.organisations o WHERE o.id = ?)", + ResultSetExtractor { + it.next() + it.getBoolean(1) + }, + orgAddress.organisationId + ) + + if (!orgExists!!) + throw OrganisationWithIdDoesNotExist(orgAddress.organisationId) + + val cityExists: Boolean? = jdbcTemplate.query( + "select exists(select 1 from organisations_schema.cities c WHERE c.id = ?)", + ResultSetExtractor { + it.next() + it.getBoolean(1) + }, + orgAddress.cityId + ) + + if (!cityExists!!) + throw CityWithIdDoesNotExist(orgAddress.cityId) + + // We can also check if both city and organisation are in same country if that's what is required + + return true + } + private fun createOrganisation(org: OrganisationRequest, contactDetailsId: UUID): UUID { val keyHolder: KeyHolder = GeneratedKeyHolder() jdbcTemplate.update( @@ -147,5 +214,4 @@ class OrganisationRepository { it.getString("country_code") ) } - } diff --git a/src/main/kotlin/io/billie/organisations/data/OrganisationWithIdDoesNotExist.kt b/src/main/kotlin/io/billie/organisations/data/OrganisationWithIdDoesNotExist.kt new file mode 100644 index 0000000..d722240 --- /dev/null +++ b/src/main/kotlin/io/billie/organisations/data/OrganisationWithIdDoesNotExist.kt @@ -0,0 +1,5 @@ +package io.billie.organisations.data + +import java.util.* + +class OrganisationWithIdDoesNotExist(val organisationId: UUID) : RuntimeException() diff --git a/src/main/kotlin/io/billie/organisations/resource/OrganisationResource.kt b/src/main/kotlin/io/billie/organisations/resource/OrganisationResource.kt index b108a1f..4a7c8bf 100644 --- a/src/main/kotlin/io/billie/organisations/resource/OrganisationResource.kt +++ b/src/main/kotlin/io/billie/organisations/resource/OrganisationResource.kt @@ -2,17 +2,22 @@ package io.billie.organisations.resource import io.billie.organisations.data.UnableToFindCountry import io.billie.organisations.service.OrganisationService -import io.billie.organisations.viewmodel.* +import io.billie.organisations.viewmodel.Entity +import io.billie.organisations.viewmodel.OrganisationAddressRequest +import io.billie.organisations.viewmodel.OrganisationRequest +import io.billie.organisations.viewmodel.OrganisationResponse import io.swagger.v3.oas.annotations.media.ArraySchema import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses -import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus.BAD_REQUEST -import org.springframework.web.bind.annotation.* +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 +import org.springframework.web.bind.annotation.RestController import org.springframework.web.server.ResponseStatusException -import java.util.* import javax.validation.Valid @@ -46,4 +51,26 @@ class OrganisationResource(val service: OrganisationService) { } } + @PostMapping(path = arrayOf("/addresses")) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "Accepted the address for organisation", + content = [ + (Content( + mediaType = "application/json", + array = (ArraySchema(schema = Schema(implementation = Entity::class))) + ))] + ), + ApiResponse(responseCode = "400", description = "Bad request", content = [Content()])] + ) + fun postAddress(@Valid @RequestBody orgAddress: OrganisationAddressRequest): Entity { + try { + val id = service.addAddressToOrg(orgAddress) + return Entity(id) + } catch (e: UnableToFindCountry) { + throw ResponseStatusException(BAD_REQUEST, e.message) + } + } } diff --git a/src/main/kotlin/io/billie/organisations/service/OrganisationService.kt b/src/main/kotlin/io/billie/organisations/service/OrganisationService.kt index d029521..b4d433b 100644 --- a/src/main/kotlin/io/billie/organisations/service/OrganisationService.kt +++ b/src/main/kotlin/io/billie/organisations/service/OrganisationService.kt @@ -1,6 +1,7 @@ package io.billie.organisations.service import io.billie.organisations.data.OrganisationRepository +import io.billie.organisations.viewmodel.OrganisationAddressRequest import io.billie.organisations.viewmodel.OrganisationRequest import io.billie.organisations.viewmodel.OrganisationResponse import org.springframework.stereotype.Service @@ -15,4 +16,7 @@ class OrganisationService(val db: OrganisationRepository) { return db.create(organisation) } + fun addAddressToOrg(orgAddress: OrganisationAddressRequest): UUID { + return db.addAddress(orgAddress) + } } diff --git a/src/main/kotlin/io/billie/organisations/viewmodel/OrganisationAddressRequest.kt b/src/main/kotlin/io/billie/organisations/viewmodel/OrganisationAddressRequest.kt new file mode 100644 index 0000000..eaefec2 --- /dev/null +++ b/src/main/kotlin/io/billie/organisations/viewmodel/OrganisationAddressRequest.kt @@ -0,0 +1,18 @@ +package io.billie.organisations.viewmodel + +import com.fasterxml.jackson.annotation.JsonProperty +import org.springframework.data.relational.core.mapping.Table +import java.util.* +import javax.validation.constraints.NotBlank +import javax.validation.constraints.Size + +@Table("ADDRESSES") +data class OrganisationAddressRequest( + @Size(min = 36, max = 36) @JsonProperty("organisation_id") val organisationId: UUID, + @Size(min = 36, max = 36) @JsonProperty("city_id") val cityId: UUID, + @field:NotBlank @JsonProperty("pin_code") val pinCode: String, + @field:NotBlank @JsonProperty("street_name") val streetName: String, + @field:NotBlank @JsonProperty("plot_number") val plotNumber: String, + val floor: String? = null, + @JsonProperty("apartment_number") val apartmentNumber: String? = null, +) diff --git a/src/main/resources/db/migration/V9__addresses_table.sql b/src/main/resources/db/migration/V9__addresses_table.sql new file mode 100644 index 0000000..da0e322 --- /dev/null +++ b/src/main/resources/db/migration/V9__addresses_table.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS organisations_schema.addresses +( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + organisation_id UUID NOT NULL, + city_id UUID NOT NULL, + pin_code VARCHAR(10) NOT NULL, + street_name VARCHAR(100) NOT NULL, + plot_number VARCHAR(5) NOT NULL, + floor VARCHAR(3) NULL, + apartment_number VARCHAR(4) NULL, + FOREIGN KEY (organisation_id) REFERENCES organisations_schema.organisations(id) ON DELETE CASCADE, + FOREIGN KEY (city_id) REFERENCES organisations_schema.cities(id) +); diff --git a/src/test/kotlin/io/billie/functional/CanReadLocationsTest.kt b/src/test/kotlin/io/billie/functional/CanReadLocationsTest.kt index c84bc5d..0a59e81 100644 --- a/src/test/kotlin/io/billie/functional/CanReadLocationsTest.kt +++ b/src/test/kotlin/io/billie/functional/CanReadLocationsTest.kt @@ -6,7 +6,6 @@ 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.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT -import org.springframework.boot.test.web.server.LocalServerPort import org.springframework.http.MediaType.APPLICATION_JSON import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get @@ -17,10 +16,6 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @AutoConfigureMockMvc @SpringBootTest(webEnvironment = RANDOM_PORT) class CanReadLocationsTest { - - @LocalServerPort - private val port = 8080 - @Autowired private lateinit var mockMvc: MockMvc diff --git a/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt b/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt index 7d66d2d..ed0e59b 100644 --- a/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt +++ b/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt @@ -1,16 +1,17 @@ package io.billie.functional import com.fasterxml.jackson.databind.ObjectMapper +import io.billie.functional.data.Fixtures import io.billie.functional.data.Fixtures.bbcContactFixture import io.billie.functional.data.Fixtures.bbcFixture import io.billie.functional.data.Fixtures.orgRequestJson import io.billie.functional.data.Fixtures.orgRequestJsonCountryCodeBlank import io.billie.functional.data.Fixtures.orgRequestJsonCountryCodeIncorrect -import io.billie.functional.data.Fixtures.orgRequestJsonNoName import io.billie.functional.data.Fixtures.orgRequestJsonNameBlank import io.billie.functional.data.Fixtures.orgRequestJsonNoContactDetails import io.billie.functional.data.Fixtures.orgRequestJsonNoCountryCode import io.billie.functional.data.Fixtures.orgRequestJsonNoLegalEntityType +import io.billie.functional.data.Fixtures.orgRequestJsonNoName import io.billie.organisations.viewmodel.Entity import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.core.IsEqual.equalTo @@ -19,12 +20,14 @@ 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.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT -import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.http.MediaType import org.springframework.http.MediaType.APPLICATION_JSON import org.springframework.jdbc.core.JdbcTemplate import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import java.util.* @@ -32,10 +35,6 @@ import java.util.* @AutoConfigureMockMvc @SpringBootTest(webEnvironment = RANDOM_PORT) class CanStoreAndReadOrganisationTest { - - @LocalServerPort - private val port = 8080 - @Autowired private lateinit var mockMvc: MockMvc @@ -128,6 +127,23 @@ class CanStoreAndReadOrganisationTest { assertDataMatches(contactDetails, bbcContactFixture(contactDetailsId)) } + // address + @Test + fun canStoreOrgAddress() { + val result = mockMvc.perform( + MockMvcRequestBuilders.post("/organisations/addresses") + .contentType(MediaType.APPLICATION_JSON).content(Fixtures.addressRequestJson()) + ) + .andExpect(MockMvcResultMatchers.status().isOk) + .andReturn() + + val response = mapper.readValue(result.response.contentAsString, Entity::class.java) + + val org: Map = addressFromDatabase(response.id) + assertDataMatches(org, Fixtures.addressFixture(response.id)) + } + + fun assertDataMatches(reply: Map, assertions: Map) { for (key in assertions.keys) { assertThat(reply[key], equalTo(assertions[key])) @@ -140,6 +156,9 @@ class CanStoreAndReadOrganisationTest { private fun orgFromDatabase(id: UUID): MutableMap = queryEntityFromDatabase("select * from organisations_schema.organisations where id = ?", id) + private fun addressFromDatabase(id: UUID): MutableMap = + queryEntityFromDatabase("select * from organisations_schema.addresses where id = ?", id) + private fun contactDetailsFromDatabase(id: UUID): MutableMap = queryEntityFromDatabase("select * from organisations_schema.contact_details where id = ?", id) diff --git a/src/test/kotlin/io/billie/functional/data/Fixtures.kt b/src/test/kotlin/io/billie/functional/data/Fixtures.kt index 9954801..e2ccea2 100644 --- a/src/test/kotlin/io/billie/functional/data/Fixtures.kt +++ b/src/test/kotlin/io/billie/functional/data/Fixtures.kt @@ -2,7 +2,6 @@ package io.billie.functional.data import java.text.SimpleDateFormat import java.util.* -import kotlin.collections.HashMap object Fixtures { @@ -147,6 +146,32 @@ object Fixtures { return data } + // addresses + fun addressRequestJson(): String { + return """ + { + "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", + "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", + "pin_code": "10405", + "street_name": "Metzer Strasse", + "plot_number": "45", + "floor": "3" + } + """.trimIndent() + } + + fun addressFixture(id: UUID): Map { + val data = HashMap() + data["id"] = id + data["organisation_id"] = UUID.fromString("fa55c095-c771-4901-bb87-1624ac7c1eeb") + data["city_id"] = UUID.fromString("b63d0116-8b10-447d-91f6-92d3b518940a") + data["pin_code"] = "10405" + data["street_name"] = "Metzer Strasse" + data["plot_number"] = "45" + data["floor"] = "3" + + return data + } } From 613c7022f868908521848d0458a67ac1f3de6eb4 Mon Sep 17 00:00:00 2001 From: pratikgaur Date: Sat, 24 Jun 2023 00:01:30 +0200 Subject: [PATCH 3/8] Feature 1: Adding an address to an organisation: Adding valid invalid tests --- .../data/OrganisationRepository.kt | 9 +- .../db/migration/V9__addresses_table.sql | 4 +- .../CanStoreAndReadOrganisationTest.kt | 180 +++++++++++++++++- .../io/billie/functional/data/Fixtures.kt | 18 +- .../utils/ClearDatabaseBeforeEach.kt | 20 ++ 5 files changed, 207 insertions(+), 24 deletions(-) create mode 100644 src/test/kotlin/io/billie/functional/utils/ClearDatabaseBeforeEach.kt diff --git a/src/main/kotlin/io/billie/organisations/data/OrganisationRepository.kt b/src/main/kotlin/io/billie/organisations/data/OrganisationRepository.kt index ab93000..5fb8b29 100644 --- a/src/main/kotlin/io/billie/organisations/data/OrganisationRepository.kt +++ b/src/main/kotlin/io/billie/organisations/data/OrganisationRepository.kt @@ -1,7 +1,12 @@ package io.billie.organisations.data import io.billie.countries.model.CountryResponse -import io.billie.organisations.viewmodel.* +import io.billie.organisations.viewmodel.ContactDetails +import io.billie.organisations.viewmodel.ContactDetailsRequest +import io.billie.organisations.viewmodel.LegalEntityType +import io.billie.organisations.viewmodel.OrganisationAddressRequest +import io.billie.organisations.viewmodel.OrganisationRequest +import io.billie.organisations.viewmodel.OrganisationResponse import org.springframework.beans.factory.annotation.Autowired import org.springframework.jdbc.core.JdbcTemplate import org.springframework.jdbc.core.ResultSetExtractor @@ -65,7 +70,7 @@ class OrganisationRepository { ps.setString(4, orgAddress.streetName) ps.setString(5, orgAddress.plotNumber) ps.setString(6, orgAddress.floor) - ps.setString(7, orgAddress.apartmentNumber.toString()) + ps.setString(7, orgAddress.apartmentNumber) ps }, keyHolder ) diff --git a/src/main/resources/db/migration/V9__addresses_table.sql b/src/main/resources/db/migration/V9__addresses_table.sql index da0e322..78867ab 100644 --- a/src/main/resources/db/migration/V9__addresses_table.sql +++ b/src/main/resources/db/migration/V9__addresses_table.sql @@ -6,8 +6,8 @@ CREATE TABLE IF NOT EXISTS organisations_schema.addresses pin_code VARCHAR(10) NOT NULL, street_name VARCHAR(100) NOT NULL, plot_number VARCHAR(5) NOT NULL, - floor VARCHAR(3) NULL, - apartment_number VARCHAR(4) NULL, + floor VARCHAR(3) DEFAULT NULL, + apartment_number VARCHAR(4) DEFAULT NULL, FOREIGN KEY (organisation_id) REFERENCES organisations_schema.organisations(id) ON DELETE CASCADE, FOREIGN KEY (city_id) REFERENCES organisations_schema.cities(id) ); diff --git a/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt b/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt index ed0e59b..db3e6a6 100644 --- a/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt +++ b/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt @@ -12,10 +12,15 @@ import io.billie.functional.data.Fixtures.orgRequestJsonNoContactDetails import io.billie.functional.data.Fixtures.orgRequestJsonNoCountryCode import io.billie.functional.data.Fixtures.orgRequestJsonNoLegalEntityType import io.billie.functional.data.Fixtures.orgRequestJsonNoName +import io.billie.functional.utils.ClearDatabaseBeforeEach import io.billie.organisations.viewmodel.Entity import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.core.IsEqual.equalTo import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest @@ -30,10 +35,12 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post import org.springframework.test.web.servlet.result.MockMvcResultMatchers import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import java.util.* +import java.util.stream.Stream @AutoConfigureMockMvc @SpringBootTest(webEnvironment = RANDOM_PORT) +@ExtendWith(ClearDatabaseBeforeEach::class) class CanStoreAndReadOrganisationTest { @Autowired private lateinit var mockMvc: MockMvc @@ -128,11 +135,12 @@ class CanStoreAndReadOrganisationTest { } // address - @Test - fun canStoreOrgAddress() { + @ParameterizedTest + @MethodSource("validOrgAddressRequestPayloads") + fun canStoreOrgAddress(caseIdentifier: String, payload: String) { val result = mockMvc.perform( MockMvcRequestBuilders.post("/organisations/addresses") - .contentType(MediaType.APPLICATION_JSON).content(Fixtures.addressRequestJson()) + .contentType(MediaType.APPLICATION_JSON).content(payload) ) .andExpect(MockMvcResultMatchers.status().isOk) .andReturn() @@ -140,7 +148,16 @@ class CanStoreAndReadOrganisationTest { val response = mapper.readValue(result.response.contentAsString, Entity::class.java) val org: Map = addressFromDatabase(response.id) - assertDataMatches(org, Fixtures.addressFixture(response.id)) + assertDataMatches(org, Fixtures.orgAddressFixture(response.id)) + } + + @ParameterizedTest + @MethodSource("invalidOrgAddressRequestPayloads") + fun cannotStoreOrgAddressForInvalidPayload(caseIdentifier: String, payload: String) { + mockMvc.perform( + post("/organisations/addresses").contentType(APPLICATION_JSON).content(payload) + ) + .andExpect(status().isBadRequest) } @@ -162,4 +179,159 @@ class CanStoreAndReadOrganisationTest { private fun contactDetailsFromDatabase(id: UUID): MutableMap = queryEntityFromDatabase("select * from organisations_schema.contact_details where id = ?", id) + companion object { + @JvmStatic + fun invalidOrgAddressRequestPayloads(): Stream { + return Stream.of( + Arguments.of( + "missing org id", + """ + { + "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", + "pin_code": "10405", + "street_name": "Metzer Strasse", + "plot_number": "45", + "floor": "3" + } + """.trimIndent() + ), + Arguments.of( + "missing city id", + """ + { + "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", + "pin_code": "10405", + "street_name": "Metzer Strasse", + "plot_number": "45", + "floor": "3" + } + """.trimIndent() + ), + Arguments.of( + "missing pin code", + """ + { + "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", + "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", + "street_name": "Metzer Strasse", + "plot_number": "45", + "floor": "3" + } + """.trimIndent() + ), + Arguments.of( + "missing street name", + """ + { + "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", + "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", + "pin_code": "10405", + "plot_number": "45", + "floor": "3" + } + """.trimIndent() + ), + Arguments.of( + "missing plot number", + """ + { + "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", + "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", + "pin_code": "10405", + "street_name": "Metzer Strasse", + "floor": "3" + } + """.trimIndent() + ), + Arguments.of( + "null org id", + """ + { + "organisation_id": null, + "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", + "pin_code": "10405", + "street_name": "Metzer Strasse", + "plot_number": "45", + "floor": "3" + } + """.trimIndent() + ), + ) + } + + @JvmStatic + fun validOrgAddressRequestPayloads(): Stream { + return Stream.of( + Arguments.of( + "full payload", + """ + { + "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", + "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", + "pin_code": "10405", + "street_name": "Metzer Strasse", + "plot_number": "45", + "floor": "3", + "apartment_number": "13" + } + """.trimIndent() + ), + Arguments.of( + "missing floor", + """ + { + "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", + "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", + "pin_code": "10405", + "street_name": "Metzer Strasse", + "plot_number": "45", + "apartment_number": "13" + } + """.trimIndent() + ), + Arguments.of( + "missing apartment number", + """ + { + "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", + "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", + "pin_code": "10405", + "street_name": "Metzer Strasse", + "plot_number": "45", + "floor": "3" + } + """.trimIndent() + ), + Arguments.of( + "int plot, floor and apartmentNumber", + """ + { + "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", + "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", + "pin_code": "10405", + "street_name": "Metzer Strasse", + "plot_number": 45, + "floor": 3, + "apartment_number": 13 + } + """.trimIndent() + ), + Arguments.of( + "null floor and apartmentNumber", + """ + { + "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", + "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", + "pin_code": "10405", + "street_name": "Metzer Strasse", + "plot_number": "45", + "floor": null, + "apartment_number": null + } + """.trimIndent() + ), + ) + } + } + } diff --git a/src/test/kotlin/io/billie/functional/data/Fixtures.kt b/src/test/kotlin/io/billie/functional/data/Fixtures.kt index e2ccea2..0f6e5c5 100644 --- a/src/test/kotlin/io/billie/functional/data/Fixtures.kt +++ b/src/test/kotlin/io/billie/functional/data/Fixtures.kt @@ -3,6 +3,7 @@ package io.billie.functional.data import java.text.SimpleDateFormat import java.util.* + object Fixtures { fun orgRequestJsonNameBlank(): String { @@ -147,21 +148,7 @@ object Fixtures { } // addresses - - fun addressRequestJson(): String { - return """ - { - "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", - "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", - "pin_code": "10405", - "street_name": "Metzer Strasse", - "plot_number": "45", - "floor": "3" - } - """.trimIndent() - } - - fun addressFixture(id: UUID): Map { + fun orgAddressFixture(id: UUID): Map { val data = HashMap() data["id"] = id @@ -170,7 +157,6 @@ object Fixtures { data["pin_code"] = "10405" data["street_name"] = "Metzer Strasse" data["plot_number"] = "45" - data["floor"] = "3" return data } diff --git a/src/test/kotlin/io/billie/functional/utils/ClearDatabaseBeforeEach.kt b/src/test/kotlin/io/billie/functional/utils/ClearDatabaseBeforeEach.kt new file mode 100644 index 0000000..2cf92ac --- /dev/null +++ b/src/test/kotlin/io/billie/functional/utils/ClearDatabaseBeforeEach.kt @@ -0,0 +1,20 @@ +package io.billie.functional.utils + +import io.billie.organisations.data.OrganisationRepository +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.springframework.test.context.junit.jupiter.SpringExtension + +class ClearDatabaseBeforeEach: BeforeEachCallback { + override fun beforeEach(context: ExtensionContext) { + val appContext = SpringExtension.getApplicationContext(context) + + appContext.getBean(OrganisationRepository::class.java).truncateAddresses() + } +} + +private fun OrganisationRepository.truncateAddresses() { + jdbcTemplate.execute( + "TRUNCATE TABLE organisations_schema.addresses" + ) +} From 46d8b84f45da98a61205eb1d43dcf0104d9ca9e0 Mon Sep 17 00:00:00 2001 From: pratikgaur Date: Sat, 24 Jun 2023 08:28:17 +0200 Subject: [PATCH 4/8] running flyway migration on application startup in an effort to dockerize the service --- build.gradle.kts | 5 +++-- docker-compose.yml | 8 ++++++-- src/main/resources/application.properties | 2 ++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 209c614..bd098e6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ -import java.io.File + +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.io.FileInputStream import java.util.* -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.flywaydb.flyway") version "9.3.1" @@ -38,6 +38,7 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("org.flywaydb:flyway-core:6.4.1") runtimeOnly("org.postgresql:postgresql") testImplementation("org.springframework.boot:spring-boot-starter-test") diff --git a/docker-compose.yml b/docker-compose.yml index 4756a1a..bca3d75 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: command: - "--providers.docker=true" - "--providers.docker.exposedByDefault=false" - - "--entryPoints.web.address=:80" + - "--entryPoints.web.address=:8080" - "--entryPoints.websecure.address=:443" - "--certificatesResolvers.organisationsresolver.acme.email=team@organisations.manager" - "--certificatesResolvers.organisationsresolver.acme.storage=acme.json" @@ -36,7 +36,11 @@ services: env_file: - database.env - service.env + environment: + - DATABASE_URL=jdbc:postgresql://host.docker.internal:5432/ + ports: + - "8080:8080" labels: - "traefik.enable=true" - "traefik.http.routers.organisations.rule=PathPrefix(`/swagger-ui`) || PathPrefix(`/organisations`) || PathPrefix(`/countries`)" - - "traefik.http.services.organisations.loadbalancer.server.port=80" + - "traefik.http.services.organisations.loadbalancer.server.port=8080" diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 09b6bce..6535ce2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -5,3 +5,5 @@ spring.datasource.username=${POSTGRES_USER} spring.datasource.password=${POSTGRES_PASSWORD} spring.datasource.schema=${DATABASE_SCHEMA} spring.datasource.initialization-mode=always +spring.flyway.enabled=true +spring.flyway.locations=classpath:db/migration \ No newline at end of file From 2f209822ca38c618e3a9b42c7600b4c1200fe72a Mon Sep 17 00:00:00 2001 From: pratikgaur Date: Sat, 24 Jun 2023 09:28:24 +0200 Subject: [PATCH 5/8] creating organisation before address storage in tests --- .../data/OrganisationWithIdDoesNotExist.kt | 1 + .../resource/OrganisationResource.kt | 6 +- .../CanStoreAndReadOrganisationTest.kt | 120 ++++++++++-------- .../io/billie/functional/data/Fixtures.kt | 6 +- .../utils/ClearDatabaseBeforeEach.kt | 7 + 5 files changed, 81 insertions(+), 59 deletions(-) diff --git a/src/main/kotlin/io/billie/organisations/data/OrganisationWithIdDoesNotExist.kt b/src/main/kotlin/io/billie/organisations/data/OrganisationWithIdDoesNotExist.kt index d722240..6040bcc 100644 --- a/src/main/kotlin/io/billie/organisations/data/OrganisationWithIdDoesNotExist.kt +++ b/src/main/kotlin/io/billie/organisations/data/OrganisationWithIdDoesNotExist.kt @@ -2,4 +2,5 @@ package io.billie.organisations.data import java.util.* + class OrganisationWithIdDoesNotExist(val organisationId: UUID) : RuntimeException() diff --git a/src/main/kotlin/io/billie/organisations/resource/OrganisationResource.kt b/src/main/kotlin/io/billie/organisations/resource/OrganisationResource.kt index 4a7c8bf..4c844a7 100644 --- a/src/main/kotlin/io/billie/organisations/resource/OrganisationResource.kt +++ b/src/main/kotlin/io/billie/organisations/resource/OrganisationResource.kt @@ -1,5 +1,7 @@ package io.billie.organisations.resource +import io.billie.organisations.data.CityWithIdDoesNotExist +import io.billie.organisations.data.OrganisationWithIdDoesNotExist import io.billie.organisations.data.UnableToFindCountry import io.billie.organisations.service.OrganisationService import io.billie.organisations.viewmodel.Entity @@ -69,7 +71,9 @@ class OrganisationResource(val service: OrganisationService) { try { val id = service.addAddressToOrg(orgAddress) return Entity(id) - } catch (e: UnableToFindCountry) { + } catch (e: OrganisationWithIdDoesNotExist) { + throw ResponseStatusException(BAD_REQUEST, e.message) + } catch (e: CityWithIdDoesNotExist) { throw ResponseStatusException(BAD_REQUEST, e.message) } } diff --git a/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt b/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt index db3e6a6..e489437 100644 --- a/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt +++ b/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt @@ -137,18 +137,30 @@ class CanStoreAndReadOrganisationTest { // address @ParameterizedTest @MethodSource("validOrgAddressRequestPayloads") - fun canStoreOrgAddress(caseIdentifier: String, payload: String) { + fun canStoreOrgAddress(caseIdentifier: String, payload: MutableMap) { + // given + val addOrgResult = mockMvc.perform( + post("/organisations").contentType(APPLICATION_JSON).content(orgRequestJson()) + ) + .andExpect(status().isOk) + .andReturn() + + val orgResponse = mapper.readValue(addOrgResult.response.contentAsString, Entity::class.java) + + // when + payload["organisation_id"] = orgResponse.id val result = mockMvc.perform( MockMvcRequestBuilders.post("/organisations/addresses") - .contentType(MediaType.APPLICATION_JSON).content(payload) + .contentType(MediaType.APPLICATION_JSON).content(mapper.writeValueAsString(payload)) ) .andExpect(MockMvcResultMatchers.status().isOk) .andReturn() val response = mapper.readValue(result.response.contentAsString, Entity::class.java) + // then val org: Map = addressFromDatabase(response.id) - assertDataMatches(org, Fixtures.orgAddressFixture(response.id)) + assertDataMatches(org, Fixtures.orgAddressFixture(response.id, orgResponse.id)) } @ParameterizedTest @@ -256,79 +268,77 @@ class CanStoreAndReadOrganisationTest { } """.trimIndent() ), - ) - } - - @JvmStatic - fun validOrgAddressRequestPayloads(): Stream { - return Stream.of( Arguments.of( - "full payload", + "non existing org id", """ { - "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", + "organisation_id": "NON EXISTENT", "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", "pin_code": "10405", "street_name": "Metzer Strasse", "plot_number": "45", - "floor": "3", - "apartment_number": "13" + "floor": "3" } """.trimIndent() ), + ) + } + + @JvmStatic + fun validOrgAddressRequestPayloads(): Stream { + return Stream.of( + Arguments.of( + "full payload", + mutableMapOf( + "city_id" to "8cf6507c-5fcc-4b24-81df-eae67dc7a9f6", + "pin_code" to "10405", + "street_name" to "Metzer Strasse", + "plot_number" to "45", + "floor" to "3", + "apartment_number" to "13" + ) + ), Arguments.of( "missing floor", - """ - { - "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", - "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", - "pin_code": "10405", - "street_name": "Metzer Strasse", - "plot_number": "45", - "apartment_number": "13" - } - """.trimIndent() + mutableMapOf( + "city_id" to "8cf6507c-5fcc-4b24-81df-eae67dc7a9f6", + "pin_code" to "10405", + "street_name" to "Metzer Strasse", + "plot_number" to "45", + "apartment_number" to "13" + ) ), Arguments.of( "missing apartment number", - """ - { - "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", - "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", - "pin_code": "10405", - "street_name": "Metzer Strasse", - "plot_number": "45", - "floor": "3" - } - """.trimIndent() + mutableMapOf( + "city_id" to "8cf6507c-5fcc-4b24-81df-eae67dc7a9f6", + "pin_code" to "10405", + "street_name" to "Metzer Strasse", + "plot_number" to "45", + "floor" to "3", + ) ), Arguments.of( "int plot, floor and apartmentNumber", - """ - { - "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", - "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", - "pin_code": "10405", - "street_name": "Metzer Strasse", - "plot_number": 45, - "floor": 3, - "apartment_number": 13 - } - """.trimIndent() + mutableMapOf( + "city_id" to "8cf6507c-5fcc-4b24-81df-eae67dc7a9f6", + "pin_code" to "10405", + "street_name" to "Metzer Strasse", + "plot_number" to 45, + "floor" to 3, + "apartment_number" to 13 + ) ), Arguments.of( "null floor and apartmentNumber", - """ - { - "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", - "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", - "pin_code": "10405", - "street_name": "Metzer Strasse", - "plot_number": "45", - "floor": null, - "apartment_number": null - } - """.trimIndent() + mutableMapOf( + "city_id" to "8cf6507c-5fcc-4b24-81df-eae67dc7a9f6", + "pin_code" to "10405", + "street_name" to "Metzer Strasse", + "plot_number" to "45", + "floor" to null, + "apartment_number" to null + ) ), ) } diff --git a/src/test/kotlin/io/billie/functional/data/Fixtures.kt b/src/test/kotlin/io/billie/functional/data/Fixtures.kt index 0f6e5c5..6ab630f 100644 --- a/src/test/kotlin/io/billie/functional/data/Fixtures.kt +++ b/src/test/kotlin/io/billie/functional/data/Fixtures.kt @@ -148,12 +148,12 @@ object Fixtures { } // addresses - fun orgAddressFixture(id: UUID): Map { + fun orgAddressFixture(id: UUID, orgId: UUID): Map { val data = HashMap() data["id"] = id - data["organisation_id"] = UUID.fromString("fa55c095-c771-4901-bb87-1624ac7c1eeb") - data["city_id"] = UUID.fromString("b63d0116-8b10-447d-91f6-92d3b518940a") + data["organisation_id"] = orgId + data["city_id"] = UUID.fromString("8cf6507c-5fcc-4b24-81df-eae67dc7a9f6") // berlin city id data["pin_code"] = "10405" data["street_name"] = "Metzer Strasse" data["plot_number"] = "45" diff --git a/src/test/kotlin/io/billie/functional/utils/ClearDatabaseBeforeEach.kt b/src/test/kotlin/io/billie/functional/utils/ClearDatabaseBeforeEach.kt index 2cf92ac..78e435f 100644 --- a/src/test/kotlin/io/billie/functional/utils/ClearDatabaseBeforeEach.kt +++ b/src/test/kotlin/io/billie/functional/utils/ClearDatabaseBeforeEach.kt @@ -10,6 +10,7 @@ class ClearDatabaseBeforeEach: BeforeEachCallback { val appContext = SpringExtension.getApplicationContext(context) appContext.getBean(OrganisationRepository::class.java).truncateAddresses() + appContext.getBean(OrganisationRepository::class.java).truncateOrganisations() } } @@ -18,3 +19,9 @@ private fun OrganisationRepository.truncateAddresses() { "TRUNCATE TABLE organisations_schema.addresses" ) } + +private fun OrganisationRepository.truncateOrganisations() { + jdbcTemplate.execute( + "TRUNCATE TABLE organisations_schema.organisations CASCADE" + ) +} From 4cc302e5615e7eb4bbce787a2ebb789404339f8a Mon Sep 17 00:00:00 2001 From: Pratik Gaur Date: Mon, 26 Jun 2023 10:20:23 +0200 Subject: [PATCH 6/8] fixing tests for autogenerated cityId --- .../CanStoreAndReadOrganisationTest.kt | 16 +++++++++------- .../kotlin/io/billie/functional/data/Fixtures.kt | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt b/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt index e489437..270116f 100644 --- a/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt +++ b/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt @@ -144,11 +144,12 @@ class CanStoreAndReadOrganisationTest { ) .andExpect(status().isOk) .andReturn() - val orgResponse = mapper.readValue(addOrgResult.response.contentAsString, Entity::class.java) + val city: Map = cityFromDatabase(countryCode = "DE", cityName = "Berlin") // when payload["organisation_id"] = orgResponse.id + payload["city_id"] = city["id"] val result = mockMvc.perform( MockMvcRequestBuilders.post("/organisations/addresses") .contentType(MediaType.APPLICATION_JSON).content(mapper.writeValueAsString(payload)) @@ -160,7 +161,7 @@ class CanStoreAndReadOrganisationTest { // then val org: Map = addressFromDatabase(response.id) - assertDataMatches(org, Fixtures.orgAddressFixture(response.id, orgResponse.id)) + assertDataMatches(org, Fixtures.orgAddressFixture(response.id, orgResponse.id, city["id"] as UUID)) } @ParameterizedTest @@ -191,6 +192,12 @@ class CanStoreAndReadOrganisationTest { private fun contactDetailsFromDatabase(id: UUID): MutableMap = queryEntityFromDatabase("select * from organisations_schema.contact_details where id = ?", id) + private fun cityFromDatabase(countryCode: String, cityName: String): MutableMap { + return template.queryForMap( + "SELECT * from organisations_schema.cities where country_code = '$countryCode' and name = '$cityName' LIMIT 1" + ) + } + companion object { @JvmStatic fun invalidOrgAddressRequestPayloads(): Stream { @@ -290,7 +297,6 @@ class CanStoreAndReadOrganisationTest { Arguments.of( "full payload", mutableMapOf( - "city_id" to "8cf6507c-5fcc-4b24-81df-eae67dc7a9f6", "pin_code" to "10405", "street_name" to "Metzer Strasse", "plot_number" to "45", @@ -301,7 +307,6 @@ class CanStoreAndReadOrganisationTest { Arguments.of( "missing floor", mutableMapOf( - "city_id" to "8cf6507c-5fcc-4b24-81df-eae67dc7a9f6", "pin_code" to "10405", "street_name" to "Metzer Strasse", "plot_number" to "45", @@ -311,7 +316,6 @@ class CanStoreAndReadOrganisationTest { Arguments.of( "missing apartment number", mutableMapOf( - "city_id" to "8cf6507c-5fcc-4b24-81df-eae67dc7a9f6", "pin_code" to "10405", "street_name" to "Metzer Strasse", "plot_number" to "45", @@ -321,7 +325,6 @@ class CanStoreAndReadOrganisationTest { Arguments.of( "int plot, floor and apartmentNumber", mutableMapOf( - "city_id" to "8cf6507c-5fcc-4b24-81df-eae67dc7a9f6", "pin_code" to "10405", "street_name" to "Metzer Strasse", "plot_number" to 45, @@ -332,7 +335,6 @@ class CanStoreAndReadOrganisationTest { Arguments.of( "null floor and apartmentNumber", mutableMapOf( - "city_id" to "8cf6507c-5fcc-4b24-81df-eae67dc7a9f6", "pin_code" to "10405", "street_name" to "Metzer Strasse", "plot_number" to "45", diff --git a/src/test/kotlin/io/billie/functional/data/Fixtures.kt b/src/test/kotlin/io/billie/functional/data/Fixtures.kt index 6ab630f..2e8c4ff 100644 --- a/src/test/kotlin/io/billie/functional/data/Fixtures.kt +++ b/src/test/kotlin/io/billie/functional/data/Fixtures.kt @@ -148,12 +148,12 @@ object Fixtures { } // addresses - fun orgAddressFixture(id: UUID, orgId: UUID): Map { + fun orgAddressFixture(id: UUID, orgId: UUID, cityId: UUID): Map { val data = HashMap() data["id"] = id data["organisation_id"] = orgId - data["city_id"] = UUID.fromString("8cf6507c-5fcc-4b24-81df-eae67dc7a9f6") // berlin city id + data["city_id"] = cityId data["pin_code"] = "10405" data["street_name"] = "Metzer Strasse" data["plot_number"] = "45" From 0ba9d650a956ee558880364f0445cf610dfc37e7 Mon Sep 17 00:00:00 2001 From: Pratik Gaur Date: Mon, 26 Jun 2023 13:05:05 +0200 Subject: [PATCH 7/8] orgId in path param for posting address --- .../data/OrganisationRepository.kt | 16 +++--- .../resource/OrganisationResource.kt | 12 +++-- .../service/OrganisationService.kt | 4 +- .../viewmodel/OrganisationAddressRequest.kt | 1 - .../CanStoreAndReadOrganisationTest.kt | 52 ++++++++----------- 5 files changed, 41 insertions(+), 44 deletions(-) diff --git a/src/main/kotlin/io/billie/organisations/data/OrganisationRepository.kt b/src/main/kotlin/io/billie/organisations/data/OrganisationRepository.kt index 5fb8b29..49e93e3 100644 --- a/src/main/kotlin/io/billie/organisations/data/OrganisationRepository.kt +++ b/src/main/kotlin/io/billie/organisations/data/OrganisationRepository.kt @@ -41,12 +41,12 @@ class OrganisationRepository { } @Transactional - fun addAddress(orgAddress: OrganisationAddressRequest): UUID { - validateAddress(orgAddress) - return createAddress(orgAddress) + fun addAddress(orgId: UUID, orgAddress: OrganisationAddressRequest): UUID { + validateAddress(orgId, orgAddress) + return createAddress(orgId, orgAddress) } - private fun createAddress(orgAddress: OrganisationAddressRequest): UUID { + private fun createAddress(orgId: UUID, orgAddress: OrganisationAddressRequest): UUID { val keyHolder: KeyHolder = GeneratedKeyHolder() jdbcTemplate.update( { connection -> @@ -64,7 +64,7 @@ class OrganisationRepository { """.trimIndent(), arrayOf("id") ) - ps.setObject(1, orgAddress.organisationId) + ps.setObject(1, orgId) ps.setObject(2, orgAddress.cityId) ps.setString(3, orgAddress.pinCode) ps.setString(4, orgAddress.streetName) @@ -89,18 +89,18 @@ class OrganisationRepository { return (reply != null) && (reply > 0) } - private fun validateAddress(orgAddress: OrganisationAddressRequest): Boolean { + private fun validateAddress(orgId: UUID, orgAddress: OrganisationAddressRequest): Boolean { val orgExists: Boolean? = jdbcTemplate.query( "select exists(select 1 from organisations_schema.organisations o WHERE o.id = ?)", ResultSetExtractor { it.next() it.getBoolean(1) }, - orgAddress.organisationId + orgId ) if (!orgExists!!) - throw OrganisationWithIdDoesNotExist(orgAddress.organisationId) + throw OrganisationWithIdDoesNotExist(orgId) val cityExists: Boolean? = jdbcTemplate.query( "select exists(select 1 from organisations_schema.cities c WHERE c.id = ?)", diff --git a/src/main/kotlin/io/billie/organisations/resource/OrganisationResource.kt b/src/main/kotlin/io/billie/organisations/resource/OrganisationResource.kt index 4c844a7..76e93e0 100644 --- a/src/main/kotlin/io/billie/organisations/resource/OrganisationResource.kt +++ b/src/main/kotlin/io/billie/organisations/resource/OrganisationResource.kt @@ -15,12 +15,15 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import org.springframework.http.HttpStatus.BAD_REQUEST import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.server.ResponseStatusException +import java.util.UUID import javax.validation.Valid +import javax.validation.constraints.Size @RestController @@ -53,7 +56,7 @@ class OrganisationResource(val service: OrganisationService) { } } - @PostMapping(path = arrayOf("/addresses")) + @PostMapping(path = arrayOf("/{org_id}/addresses")) @ApiResponses( value = [ ApiResponse( @@ -67,9 +70,12 @@ class OrganisationResource(val service: OrganisationService) { ), ApiResponse(responseCode = "400", description = "Bad request", content = [Content()])] ) - fun postAddress(@Valid @RequestBody orgAddress: OrganisationAddressRequest): Entity { + fun postAddress( + @Size(min = 36, max = 36) @PathVariable("org_id") orgId: UUID, + @Valid @RequestBody orgAddress: OrganisationAddressRequest + ): Entity { try { - val id = service.addAddressToOrg(orgAddress) + val id = service.addAddressToOrg(orgId, orgAddress) return Entity(id) } catch (e: OrganisationWithIdDoesNotExist) { throw ResponseStatusException(BAD_REQUEST, e.message) diff --git a/src/main/kotlin/io/billie/organisations/service/OrganisationService.kt b/src/main/kotlin/io/billie/organisations/service/OrganisationService.kt index b4d433b..3b7071f 100644 --- a/src/main/kotlin/io/billie/organisations/service/OrganisationService.kt +++ b/src/main/kotlin/io/billie/organisations/service/OrganisationService.kt @@ -16,7 +16,7 @@ class OrganisationService(val db: OrganisationRepository) { return db.create(organisation) } - fun addAddressToOrg(orgAddress: OrganisationAddressRequest): UUID { - return db.addAddress(orgAddress) + fun addAddressToOrg(orgId: UUID, orgAddress: OrganisationAddressRequest): UUID { + return db.addAddress(orgId, orgAddress) } } diff --git a/src/main/kotlin/io/billie/organisations/viewmodel/OrganisationAddressRequest.kt b/src/main/kotlin/io/billie/organisations/viewmodel/OrganisationAddressRequest.kt index eaefec2..80f5283 100644 --- a/src/main/kotlin/io/billie/organisations/viewmodel/OrganisationAddressRequest.kt +++ b/src/main/kotlin/io/billie/organisations/viewmodel/OrganisationAddressRequest.kt @@ -8,7 +8,6 @@ import javax.validation.constraints.Size @Table("ADDRESSES") data class OrganisationAddressRequest( - @Size(min = 36, max = 36) @JsonProperty("organisation_id") val organisationId: UUID, @Size(min = 36, max = 36) @JsonProperty("city_id") val cityId: UUID, @field:NotBlank @JsonProperty("pin_code") val pinCode: String, @field:NotBlank @JsonProperty("street_name") val streetName: String, diff --git a/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt b/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt index 270116f..7e845c1 100644 --- a/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt +++ b/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt @@ -134,6 +134,27 @@ class CanStoreAndReadOrganisationTest { assertDataMatches(contactDetails, bbcContactFixture(contactDetailsId)) } + @ParameterizedTest + @MethodSource("invalidOrgAddressRequestPayloads") + fun cannotStoreOrgAddressForInvalidPayload(caseIdentifier: String, payload: String) { + // given + val addOrgResult = mockMvc.perform( + post("/organisations").contentType(APPLICATION_JSON).content(orgRequestJson()) + ) + .andExpect(status().isOk) + .andReturn() + val orgResponse = mapper.readValue(addOrgResult.response.contentAsString, Entity::class.java) + val city: Map = cityFromDatabase(countryCode = "DE", cityName = "Berlin") + + // when + val response = mockMvc.perform( + post("/organisations/${orgResponse.id}/addresses").contentType(APPLICATION_JSON).content(payload) + ) + .andExpect(status().isBadRequest) + + var a = 4 + } + // address @ParameterizedTest @MethodSource("validOrgAddressRequestPayloads") @@ -148,10 +169,9 @@ class CanStoreAndReadOrganisationTest { val city: Map = cityFromDatabase(countryCode = "DE", cityName = "Berlin") // when - payload["organisation_id"] = orgResponse.id payload["city_id"] = city["id"] val result = mockMvc.perform( - MockMvcRequestBuilders.post("/organisations/addresses") + MockMvcRequestBuilders.post("/organisations/${orgResponse.id}/addresses") .contentType(MediaType.APPLICATION_JSON).content(mapper.writeValueAsString(payload)) ) .andExpect(MockMvcResultMatchers.status().isOk) @@ -164,16 +184,6 @@ class CanStoreAndReadOrganisationTest { assertDataMatches(org, Fixtures.orgAddressFixture(response.id, orgResponse.id, city["id"] as UUID)) } - @ParameterizedTest - @MethodSource("invalidOrgAddressRequestPayloads") - fun cannotStoreOrgAddressForInvalidPayload(caseIdentifier: String, payload: String) { - mockMvc.perform( - post("/organisations/addresses").contentType(APPLICATION_JSON).content(payload) - ) - .andExpect(status().isBadRequest) - } - - fun assertDataMatches(reply: Map, assertions: Map) { for (key in assertions.keys) { assertThat(reply[key], equalTo(assertions[key])) @@ -202,23 +212,10 @@ class CanStoreAndReadOrganisationTest { @JvmStatic fun invalidOrgAddressRequestPayloads(): Stream { return Stream.of( - Arguments.of( - "missing org id", - """ - { - "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", - "pin_code": "10405", - "street_name": "Metzer Strasse", - "plot_number": "45", - "floor": "3" - } - """.trimIndent() - ), Arguments.of( "missing city id", """ { - "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", "pin_code": "10405", "street_name": "Metzer Strasse", "plot_number": "45", @@ -230,7 +227,6 @@ class CanStoreAndReadOrganisationTest { "missing pin code", """ { - "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", "street_name": "Metzer Strasse", "plot_number": "45", @@ -242,7 +238,6 @@ class CanStoreAndReadOrganisationTest { "missing street name", """ { - "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", "pin_code": "10405", "plot_number": "45", @@ -254,7 +249,6 @@ class CanStoreAndReadOrganisationTest { "missing plot number", """ { - "organisation_id": "fa55c095-c771-4901-bb87-1624ac7c1eeb", "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", "pin_code": "10405", "street_name": "Metzer Strasse", @@ -266,7 +260,6 @@ class CanStoreAndReadOrganisationTest { "null org id", """ { - "organisation_id": null, "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", "pin_code": "10405", "street_name": "Metzer Strasse", @@ -279,7 +272,6 @@ class CanStoreAndReadOrganisationTest { "non existing org id", """ { - "organisation_id": "NON EXISTENT", "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", "pin_code": "10405", "street_name": "Metzer Strasse", From a0abf63df5f30b96e6ba25cf8ae0da0d7b7f6432 Mon Sep 17 00:00:00 2001 From: Pratik Gaur Date: Mon, 26 Jun 2023 14:22:38 +0200 Subject: [PATCH 8/8] test adding multiple address to same org --- README.md | 11 +++ .../CanStoreAndReadOrganisationTest.kt | 95 +++++++++++++++++-- .../io/billie/functional/data/Fixtures.kt | 31 +++--- 3 files changed, 117 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index d4b06b3..d80e64e 100644 --- a/README.md +++ b/README.md @@ -54,3 +54,14 @@ docs at -> http://localhost:8080/swagger-ui/index.html Work has been started but not done yet to containerise the kotlin service. The service runs in the host right now. Feel free to fix that if it makes your life easier + + +### From candidate: + +Implemented features: +1. Add an address to an organisation + +Additional improvements: +1. Integration tests now run on random ports +2. Flyway migration runs on application start +3. Now, it is possible to run service inside docker on local diff --git a/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt b/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt index 7e845c1..ee04a36 100644 --- a/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt +++ b/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt @@ -37,7 +37,6 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import java.util.* import java.util.stream.Stream - @AutoConfigureMockMvc @SpringBootTest(webEnvironment = RANDOM_PORT) @ExtendWith(ClearDatabaseBeforeEach::class) @@ -144,15 +143,12 @@ class CanStoreAndReadOrganisationTest { .andExpect(status().isOk) .andReturn() val orgResponse = mapper.readValue(addOrgResult.response.contentAsString, Entity::class.java) - val city: Map = cityFromDatabase(countryCode = "DE", cityName = "Berlin") // when - val response = mockMvc.perform( + mockMvc.perform( post("/organisations/${orgResponse.id}/addresses").contentType(APPLICATION_JSON).content(payload) ) .andExpect(status().isBadRequest) - - var a = 4 } // address @@ -180,11 +176,94 @@ class CanStoreAndReadOrganisationTest { val response = mapper.readValue(result.response.contentAsString, Entity::class.java) // then - val org: Map = addressFromDatabase(response.id) - assertDataMatches(org, Fixtures.orgAddressFixture(response.id, orgResponse.id, city["id"] as UUID)) + val orgAddress: Map = addressFromDatabase(response.id) + assertDataMatches( + orgAddress, + Fixtures.orgAddressFixture( + id = response.id, + orgId = orgResponse.id, + cityId = city["id"] as UUID, + pinCode = payload["pin_code"] as String, + streetName = payload["street_name"] as String, + plotNumber = payload["plot_number"].toString(), + floor = payload["floor"]?.toString(), + apartmentNumber = payload["apartment_number"]?.toString(), + ) + ) + } + + @Test + fun canStoreMultipleAddressForSameOrg() { + // given + val addOrgResult = mockMvc.perform( + post("/organisations").contentType(APPLICATION_JSON).content(orgRequestJson()) + ) + .andExpect(status().isOk) + .andReturn() + val orgResponse = mapper.readValue(addOrgResult.response.contentAsString, Entity::class.java) + val city1Id = cityFromDatabase(countryCode = "DE", cityName = "Berlin")["id"] + + val payload1 = """ + { + "city_id": "$city1Id", + "pin_code": "10405", + "street_name": "Metzer Strasse", + "plot_number": "45", + "floor": "3", + "apartment_number": "10" + } + """.trimIndent() + val result1 = mockMvc.perform( + MockMvcRequestBuilders.post("/organisations/${orgResponse.id}/addresses") + .contentType(MediaType.APPLICATION_JSON).content(payload1) + ) + .andExpect(MockMvcResultMatchers.status().isOk) + .andReturn() + + val response1 = mapper.readValue(result1.response.contentAsString, Entity::class.java) + + val orgAddress1: Map = addressFromDatabase(response1.id) + assertDataMatches(orgAddress1, Fixtures.orgAddressFixture(response1.id, orgResponse.id, city1Id as UUID)) + + // when + val city2Id = cityFromDatabase(countryCode = "IN", cityName = "Delhi")["id"] + val payload2 = """ + { + "city_id": "$city2Id", + "pin_code": "110034", + "street_name": "Railway Road", + "plot_number": "45", + "floor": "3", + "apartment_number": "4" + } + """.trimIndent() + val result2 = mockMvc.perform( + MockMvcRequestBuilders.post("/organisations/${orgResponse.id}/addresses") + .contentType(MediaType.APPLICATION_JSON).content(payload2) + ) + .andExpect(MockMvcResultMatchers.status().isOk) + .andReturn() + + val response2 = mapper.readValue(result2.response.contentAsString, Entity::class.java) + + // then + val orgAddress2: Map = addressFromDatabase(response2.id) + assertDataMatches( + orgAddress2, + Fixtures.orgAddressFixture( + id = response2.id, + orgId = orgResponse.id, + cityId = city2Id as UUID, + pinCode = "110034", + streetName = "Railway Road", + floor = "3", + apartmentNumber = "4" + + ) + ) } - fun assertDataMatches(reply: Map, assertions: Map) { + fun assertDataMatches(reply: Map, assertions: Map) { for (key in assertions.keys) { assertThat(reply[key], equalTo(assertions[key])) } diff --git a/src/test/kotlin/io/billie/functional/data/Fixtures.kt b/src/test/kotlin/io/billie/functional/data/Fixtures.kt index 2e8c4ff..fd7961b 100644 --- a/src/test/kotlin/io/billie/functional/data/Fixtures.kt +++ b/src/test/kotlin/io/billie/functional/data/Fixtures.kt @@ -148,16 +148,23 @@ object Fixtures { } // addresses - fun orgAddressFixture(id: UUID, orgId: UUID, cityId: UUID): Map { - val data = HashMap() - - data["id"] = id - data["organisation_id"] = orgId - data["city_id"] = cityId - data["pin_code"] = "10405" - data["street_name"] = "Metzer Strasse" - data["plot_number"] = "45" - - return data - } + fun orgAddressFixture( + id: UUID, + orgId: UUID, + cityId: UUID, + pinCode: String = "10405", + streetName: String = "Metzer Strasse", + plotNumber: String = "45", + floor: String? = "3", + apartmentNumber: String? = "10" + ) = mapOf( + "id" to id, + "organisation_id" to orgId, + "city_id" to cityId, + "pin_code" to pinCode, + "street_name" to streetName, + "plot_number" to plotNumber, + "floor" to floor, + "apartment_number" to apartmentNumber + ) }