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/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/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..49e93e3 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 @@ -35,6 +40,43 @@ class OrganisationRepository { return createOrganisation(organisation, id) } + @Transactional + fun addAddress(orgId: UUID, orgAddress: OrganisationAddressRequest): UUID { + validateAddress(orgId, orgAddress) + return createAddress(orgId, orgAddress) + } + + private fun createAddress(orgId: UUID, 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, orgId) + 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) + 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 +89,36 @@ class OrganisationRepository { return (reply != null) && (reply > 0) } + 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) + }, + orgId + ) + + if (!orgExists!!) + throw OrganisationWithIdDoesNotExist(orgId) + + 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 +219,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..6040bcc --- /dev/null +++ b/src/main/kotlin/io/billie/organisations/data/OrganisationWithIdDoesNotExist.kt @@ -0,0 +1,6 @@ +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..76e93e0 100644 --- a/src/main/kotlin/io/billie/organisations/resource/OrganisationResource.kt +++ b/src/main/kotlin/io/billie/organisations/resource/OrganisationResource.kt @@ -1,19 +1,29 @@ 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.* +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.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.* +import java.util.UUID import javax.validation.Valid +import javax.validation.constraints.Size @RestController @@ -46,4 +56,31 @@ class OrganisationResource(val service: OrganisationService) { } } + @PostMapping(path = arrayOf("/{org_id}/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( + @Size(min = 36, max = 36) @PathVariable("org_id") orgId: UUID, + @Valid @RequestBody orgAddress: OrganisationAddressRequest + ): Entity { + try { + val id = service.addAddressToOrg(orgId, orgAddress) + return Entity(id) + } catch (e: OrganisationWithIdDoesNotExist) { + throw ResponseStatusException(BAD_REQUEST, e.message) + } catch (e: CityWithIdDoesNotExist) { + 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..3b7071f 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(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 new file mode 100644 index 0000000..80f5283 --- /dev/null +++ b/src/main/kotlin/io/billie/organisations/viewmodel/OrganisationAddressRequest.kt @@ -0,0 +1,17 @@ +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("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/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 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..78867ab --- /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) 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/CanReadLocationsTest.kt b/src/test/kotlin/io/billie/functional/CanReadLocationsTest.kt index 91782d7..0a59e81 100644 --- a/src/test/kotlin/io/billie/functional/CanReadLocationsTest.kt +++ b/src/test/kotlin/io/billie/functional/CanReadLocationsTest.kt @@ -1,29 +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.web.server.LocalServerPort +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT 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 - 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 2d57630..ee04a36 100644 --- a/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt +++ b/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt @@ -1,41 +1,46 @@ 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.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 -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT -import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT +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.* - +import java.util.stream.Stream @AutoConfigureMockMvc -@SpringBootTest(webEnvironment = DEFINED_PORT) +@SpringBootTest(webEnvironment = RANDOM_PORT) +@ExtendWith(ClearDatabaseBeforeEach::class) class CanStoreAndReadOrganisationTest { - - @LocalServerPort - private val port = 8080 - @Autowired private lateinit var mockMvc: MockMvc @@ -128,7 +133,137 @@ class CanStoreAndReadOrganisationTest { assertDataMatches(contactDetails, bbcContactFixture(contactDetailsId)) } - fun assertDataMatches(reply: Map, assertions: Map) { + @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) + + // when + mockMvc.perform( + post("/organisations/${orgResponse.id}/addresses").contentType(APPLICATION_JSON).content(payload) + ) + .andExpect(status().isBadRequest) + } + + // address + @ParameterizedTest + @MethodSource("validOrgAddressRequestPayloads") + 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) + val city: Map = cityFromDatabase(countryCode = "DE", cityName = "Berlin") + + // when + payload["city_id"] = city["id"] + val result = mockMvc.perform( + MockMvcRequestBuilders.post("/organisations/${orgResponse.id}/addresses") + .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 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) { for (key in assertions.keys) { assertThat(reply[key], equalTo(assertions[key])) } @@ -140,7 +275,146 @@ 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) + 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 { + return Stream.of( + Arguments.of( + "missing city id", + """ + { + "pin_code": "10405", + "street_name": "Metzer Strasse", + "plot_number": "45", + "floor": "3" + } + """.trimIndent() + ), + Arguments.of( + "missing pin code", + """ + { + "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", + "street_name": "Metzer Strasse", + "plot_number": "45", + "floor": "3" + } + """.trimIndent() + ), + Arguments.of( + "missing street name", + """ + { + "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", + "pin_code": "10405", + "plot_number": "45", + "floor": "3" + } + """.trimIndent() + ), + Arguments.of( + "missing plot number", + """ + { + "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", + "pin_code": "10405", + "street_name": "Metzer Strasse", + "floor": "3" + } + """.trimIndent() + ), + Arguments.of( + "null org id", + """ + { + "city_id": "b63d0116-8b10-447d-91f6-92d3b518940a", + "pin_code": "10405", + "street_name": "Metzer Strasse", + "plot_number": "45", + "floor": "3" + } + """.trimIndent() + ), + Arguments.of( + "non existing org id", + """ + { + "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", + mutableMapOf( + "pin_code" to "10405", + "street_name" to "Metzer Strasse", + "plot_number" to "45", + "floor" to "3", + "apartment_number" to "13" + ) + ), + Arguments.of( + "missing floor", + mutableMapOf( + "pin_code" to "10405", + "street_name" to "Metzer Strasse", + "plot_number" to "45", + "apartment_number" to "13" + ) + ), + Arguments.of( + "missing apartment number", + mutableMapOf( + "pin_code" to "10405", + "street_name" to "Metzer Strasse", + "plot_number" to "45", + "floor" to "3", + ) + ), + Arguments.of( + "int plot, floor and apartmentNumber", + mutableMapOf( + "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", + mutableMapOf( + "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 9954801..fd7961b 100644 --- a/src/test/kotlin/io/billie/functional/data/Fixtures.kt +++ b/src/test/kotlin/io/billie/functional/data/Fixtures.kt @@ -2,7 +2,7 @@ package io.billie.functional.data import java.text.SimpleDateFormat import java.util.* -import kotlin.collections.HashMap + object Fixtures { @@ -147,6 +147,24 @@ object Fixtures { return data } - - + // addresses + 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 + ) } 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..78e435f --- /dev/null +++ b/src/test/kotlin/io/billie/functional/utils/ClearDatabaseBeforeEach.kt @@ -0,0 +1,27 @@ +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() + appContext.getBean(OrganisationRepository::class.java).truncateOrganisations() + } +} + +private fun OrganisationRepository.truncateAddresses() { + jdbcTemplate.execute( + "TRUNCATE TABLE organisations_schema.addresses" + ) +} + +private fun OrganisationRepository.truncateOrganisations() { + jdbcTemplate.execute( + "TRUNCATE TABLE organisations_schema.organisations CASCADE" + ) +}