diff --git a/backend/src/test/kotlin/app/ehrenamtskarte/backend/helper/TestAdministrators.kt b/backend/src/test/kotlin/app/ehrenamtskarte/backend/helper/TestAdministrators.kt index 6d729e150..26e1dd2f4 100644 --- a/backend/src/test/kotlin/app/ehrenamtskarte/backend/helper/TestAdministrators.kt +++ b/backend/src/test/kotlin/app/ehrenamtskarte/backend/helper/TestAdministrators.kt @@ -23,6 +23,16 @@ enum class TestAdministrators( project = "bayern.ehrenamtskarte.app", email = "project-admin@bayern.ehrenamtskarte.app", role = Role.PROJECT_ADMIN + ), + NUERNBERG_PROJECT_STORE_MANAGER( + project = "nuernberg.sozialpass.app", + email = "project-store-manager@nuernberg.sozialpass.app", + role = Role.PROJECT_STORE_MANAGER + ), + NUERNBERG_PROJECT_ADMIN( + project = "nuernberg.sozialpass.app", + email = "project-admin@nuernberg.sozialpass.app", + role = Role.PROJECT_ADMIN ); fun getJwtToken(): String { diff --git a/backend/src/test/kotlin/app/ehrenamtskarte/backend/helper/TestData.kt b/backend/src/test/kotlin/app/ehrenamtskarte/backend/helper/TestData.kt index 077321acd..993b2fe5f 100644 --- a/backend/src/test/kotlin/app/ehrenamtskarte/backend/helper/TestData.kt +++ b/backend/src/test/kotlin/app/ehrenamtskarte/backend/helper/TestData.kt @@ -3,9 +3,16 @@ package app.ehrenamtskarte.backend.helper import app.ehrenamtskarte.backend.cards.database.CardEntity import app.ehrenamtskarte.backend.cards.database.Cards import app.ehrenamtskarte.backend.cards.database.CodeType +import app.ehrenamtskarte.backend.stores.database.AcceptingStoreEntity +import app.ehrenamtskarte.backend.stores.database.AcceptingStores +import app.ehrenamtskarte.backend.stores.database.Addresses +import app.ehrenamtskarte.backend.stores.database.Contacts +import app.ehrenamtskarte.backend.stores.database.PhysicalStores import app.ehrenamtskarte.backend.userdata.database.UserEntitlements import app.ehrenamtskarte.backend.userdata.database.UserEntitlementsEntity +import net.postgis.jdbc.geometry.Point import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.transactions.transaction import java.time.Instant import java.time.LocalDate @@ -16,6 +23,49 @@ import kotlin.random.Random */ object TestData { + fun createAcceptingStore( + name: String = "Test store", + description: String? = "100% Ermäßigung\n\n100% discount", + street: String = "Teststr. 10", + postalCode: String = "90408", + location: String = "Nürnberg", + coordinates: Point = Point(), + email: String? = "info@test.de", + website: String? = "https://www.test.de", + telephone: String? = "0911/123456", + projectId: Int = 2, + categoryId: Int = 17 + ): AcceptingStoreEntity { + return transaction { + val addressId = Addresses.insertAndGetId { + it[Addresses.street] = street + it[Addresses.postalCode] = postalCode + it[Addresses.location] = location + it[Addresses.countryCode] = "de" + } + val contactId = Contacts.insertAndGetId { + it[Contacts.email] = email + it[Contacts.telephone] = telephone + it[Contacts.website] = website + } + val acceptingStoreRow = AcceptingStores.insert { + it[AcceptingStores.name] = name + it[AcceptingStores.description] = description + it[AcceptingStores.contactId] = contactId + it[AcceptingStores.categoryId] = categoryId + it[AcceptingStores.regionId] = null + it[AcceptingStores.projectId] = projectId + }.resultedValues!!.first() + val acceptingStoreEntity = AcceptingStoreEntity.wrapRow(acceptingStoreRow) + PhysicalStores.insert { + it[PhysicalStores.storeId] = acceptingStoreEntity.id.value + it[PhysicalStores.addressId] = addressId + it[PhysicalStores.coordinates] = coordinates + } + acceptingStoreEntity + } + } + fun createUserEntitlements( userHash: String, startDate: LocalDate = LocalDate.now().minusDays(1L), diff --git a/backend/src/test/kotlin/app/ehrenamtskarte/backend/stores/ImportAcceptingStoresTest.kt b/backend/src/test/kotlin/app/ehrenamtskarte/backend/stores/ImportAcceptingStoresTest.kt new file mode 100644 index 000000000..345b72e09 --- /dev/null +++ b/backend/src/test/kotlin/app/ehrenamtskarte/backend/stores/ImportAcceptingStoresTest.kt @@ -0,0 +1,383 @@ +package app.ehrenamtskarte.backend.stores + +import app.ehrenamtskarte.backend.GraphqlApiTest +import app.ehrenamtskarte.backend.auth.database.Administrators +import app.ehrenamtskarte.backend.helper.TestAdministrators +import app.ehrenamtskarte.backend.helper.TestData +import app.ehrenamtskarte.backend.stores.database.AcceptingStoreEntity +import app.ehrenamtskarte.backend.stores.database.AcceptingStores +import app.ehrenamtskarte.backend.stores.database.AddressEntity +import app.ehrenamtskarte.backend.stores.database.Addresses +import app.ehrenamtskarte.backend.stores.database.ContactEntity +import app.ehrenamtskarte.backend.stores.database.Contacts +import app.ehrenamtskarte.backend.stores.database.PhysicalStoreEntity +import app.ehrenamtskarte.backend.stores.database.PhysicalStores +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import io.javalin.testtools.JavalinTest +import org.jetbrains.exposed.sql.deleteAll +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.fail + +internal class ImportAcceptingStoresTest : GraphqlApiTest() { + + private val projectStoreManager = TestAdministrators.NUERNBERG_PROJECT_STORE_MANAGER + private val projectAdmin = TestAdministrators.NUERNBERG_PROJECT_ADMIN + + @AfterEach + fun cleanUp() { + transaction { + PhysicalStores.deleteAll() + Addresses.deleteAll() + AcceptingStores.deleteAll() + Contacts.deleteAll() + Administrators.deleteAll() + } + } + + @Test + fun `POST returns an error when project does not exist`() = JavalinTest.test(app) { _, client -> + val mutation = createMutation( + project = "non-existent.ehrenamtskarte.app", + stores = emptyList() + ) + val response = post(client, mutation, projectStoreManager.getJwtToken()) + + assertEquals(404, response.code) + } + + @Test + fun `POST returns an error when the auth token is missing`() = JavalinTest.test(app) { _, client -> + val mutation = createMutation(stores = emptyList()) + val response = post(client, mutation) + + assertEquals(401, response.code) + } + + @Test + fun `POST returns an error when the user is not allowed to import stores`() = JavalinTest.test(app) { _, client -> + val mutation = createMutation(stores = emptyList()) + val response = post(client, mutation, projectAdmin.getJwtToken()) + + assertEquals(403, response.code) + } + + @Test + fun `POST returns a successful response if the list of accepting stores is empty`() = JavalinTest.test(app) { _, client -> + val mutation = createMutation(stores = emptyList()) + val response = post(client, mutation, projectStoreManager.getJwtToken()) + + assertEquals(200, response.code) + + val responseBody = response.body?.string() ?: fail("Response body is null") + val jsonResponse = jacksonObjectMapper().readTree(responseBody) + + jsonResponse.apply { + assertEquals(0, findValue("storesCreated").intValue()) + assertEquals(0, findValue("storesDeleted").intValue()) + assertEquals(0, findValue("storesUntouched").intValue()) + } + } + + @Test + fun `POST returns a successful response if one accepting store with all fields has been created`() = JavalinTest.test(app) { _, client -> + val csvStore = createAcceptingStoreInput( + name = "Test store", + street = "Teststr.", + houseNumber = "10", + postalCode = "90408", + location = "Nürnberg", + latitude = "0", + longitude = "0", + telephone = "0911/123456", + email = "info@test.de", + homepage = "https://www.test.de/kontakt/", + discountDE = "20% Ermäßigung", + discountEN = "20% discount", + categoryId = "17" + ) + val mutation = createMutation(stores = listOf(csvStore)) + val response = post(client, mutation, projectStoreManager.getJwtToken()) + + assertEquals(200, response.code) + + val responseBody = response.body?.string() ?: fail("Response body is null") + val jsonResponse = jacksonObjectMapper().readTree(responseBody) + + jsonResponse.apply { + assertEquals(1, findValue("storesCreated").intValue()) + assertEquals(0, findValue("storesDeleted").intValue()) + assertEquals(0, findValue("storesUntouched").intValue()) + } + + transaction { + val acceptanceStore = AcceptingStoreEntity.all().single() + + assertEquals("Test store", acceptanceStore.name) + assertEquals("20% Ermäßigung\n\n20% discount", acceptanceStore.description) + assertEquals(17, acceptanceStore.categoryId.value) + assertEquals(2, acceptanceStore.projectId.value) + assertNull(acceptanceStore.regionId) + assertNotNull(acceptanceStore.createdDate) + + val contact = ContactEntity.all().single() + + assertEquals(acceptanceStore.contactId, contact.id) + assertEquals("info@test.de", contact.email) + assertEquals("0911/123456", contact.telephone) + assertEquals("https://www.test.de/kontakt/", contact.website) + + val address = AddressEntity.all().single() + + assertEquals("Teststr. 10", address.street) + assertEquals("90408", address.postalCode) + assertEquals("Nürnberg", address.location) + assertEquals("de", address.countryCode) + + val physicalStore = PhysicalStoreEntity.all().single() + + assertEquals("(0 0)", physicalStore.coordinates.value) + assertEquals(address.id, physicalStore.addressId) + assertEquals(acceptanceStore.id, physicalStore.storeId) + } + } + + @Test + fun `POST returns a successful response if one accepting store with only mandatory fields has been created`() = JavalinTest.test(app) { _, client -> + val csvStore = createAcceptingStoreInput( + name = "Test store", + street = "Teststr.", + houseNumber = "10", + postalCode = "90408", + location = "Nürnberg", + latitude = "0", + longitude = "0", + telephone = "", + email = "", + homepage = "", + discountDE = "", + discountEN = "", + categoryId = "17" + ) + val mutation = createMutation(stores = listOf(csvStore)) + val response = post(client, mutation, projectStoreManager.getJwtToken()) + + assertEquals(200, response.code) + + val responseBody = response.body?.string() ?: fail("Response body is null") + val jsonResponse = jacksonObjectMapper().readTree(responseBody) + + jsonResponse.apply { + assertEquals(1, findValue("storesCreated").intValue()) + assertEquals(0, findValue("storesDeleted").intValue()) + assertEquals(0, findValue("storesUntouched").intValue()) + } + + transaction { + val acceptanceStore = AcceptingStoreEntity.all().single() + + assertEquals("Test store", acceptanceStore.name) + assertNull(acceptanceStore.description) + assertEquals(17, acceptanceStore.categoryId.value) + assertEquals(2, acceptanceStore.projectId.value) + assertNull(acceptanceStore.regionId) + assertNotNull(acceptanceStore.createdDate) + + val contact = ContactEntity.all().single() + + assertEquals(acceptanceStore.contactId, contact.id) + assertNull(contact.email) + assertNull(contact.telephone) + assertNull(contact.website) + + val address = AddressEntity.all().single() + + assertEquals("Teststr. 10", address.street) + assertEquals("90408", address.postalCode) + assertEquals("Nürnberg", address.location) + assertEquals("de", address.countryCode) + + val physicalStore = PhysicalStoreEntity.all().single() + + assertEquals("(0 0)", physicalStore.coordinates.value) + assertEquals(address.id, physicalStore.addressId) + assertEquals(acceptanceStore.id, physicalStore.storeId) + } + } + + @Test + fun `POST returns a successful response if two duplicate acceptance stores are submitted`() = JavalinTest.test(app) { _, client -> + val csvStore = createAcceptingStoreInput( + name = "Test store", + street = "Teststr.", + houseNumber = "10", + postalCode = "90408", + location = "Nürnberg", + latitude = "0", + longitude = "0", + telephone = "0911/123456", + email = "info@test.de", + homepage = "https://www.test.de/kontakt/", + discountDE = "20% Ermäßigung", + discountEN = "20% discount", + categoryId = "17" + ) + val mutation = createMutation(stores = listOf(csvStore, csvStore)) + val response = post(client, mutation, projectStoreManager.getJwtToken()) + + assertEquals(200, response.code) + + val responseBody = response.body?.string() ?: fail("Response body is null") + val jsonResponse = jacksonObjectMapper().readTree(responseBody) + + jsonResponse.apply { + assertEquals(1, findValue("storesCreated").intValue()) + assertEquals(0, findValue("storesDeleted").intValue()) + assertEquals(1, findValue("storesUntouched").intValue()) + } + + transaction { + assertEquals(1, AcceptingStores.selectAll().count()) + assertEquals(1, Contacts.selectAll().count()) + assertEquals(1, Addresses.selectAll().count()) + assertEquals(1, PhysicalStores.selectAll().count()) + } + } + + @Test + fun `POST returns a successful response if one store has been created and another one has been deleted`() = JavalinTest.test(app) { _, client -> + val oldStore = TestData.createAcceptingStore() + val newStore = createAcceptingStoreInput( + name = "Test store 2", + street = "Teststr.", + houseNumber = "10", + postalCode = "90408", + location = "Nürnberg", + latitude = "0", + longitude = "0", + telephone = "0911/123456", + email = "info@test.de", + homepage = "https://www.test.de/kontakt/", + discountDE = "20% Ermäßigung", + discountEN = "20% discount", + categoryId = "17" + ) + val mutation = createMutation(stores = listOf(newStore)) + val response = post(client, mutation, projectStoreManager.getJwtToken()) + + assertEquals(200, response.code) + + val responseBody = response.body?.string() ?: fail("Response body is null") + val jsonResponse = jacksonObjectMapper().readTree(responseBody) + + jsonResponse.apply { + assertEquals(1, findValue("storesCreated").intValue()) + assertEquals(1, findValue("storesDeleted").intValue()) + assertEquals(0, findValue("storesUntouched").intValue()) + } + + transaction { + AcceptingStores.selectAll().single().let { + assertEquals("Test store 2", it[AcceptingStores.name]) + } + assertEquals(1, Contacts.selectAll().count()) + assertEquals(1, Addresses.selectAll().count()) + assertEquals(1, PhysicalStores.selectAll().count()) + } + } + + @Test + fun `POST returns a successful response if nothing has changed`() = JavalinTest.test(app) { _, client -> + val oldStore = TestData.createAcceptingStore() + val newStore = createAcceptingStoreInput( + name = oldStore.name, + street = "Teststr.", + houseNumber = "10", + postalCode = "90408", + location = "Nürnberg", + latitude = "0", + longitude = "0", + telephone = "0911/123456", + email = "info@test.de", + homepage = "https://www.test.de", + discountDE = "100% Ermäßigung", + discountEN = "100% discount", + categoryId = "17" + ) + val mutation = createMutation(stores = listOf(newStore)) + val response = post(client, mutation, projectStoreManager.getJwtToken()) + + assertEquals(200, response.code) + + val responseBody = response.body?.string() ?: fail("Response body is null") + val jsonResponse = jacksonObjectMapper().readTree(responseBody) + + jsonResponse.apply { + assertEquals(0, findValue("storesCreated").intValue()) + assertEquals(0, findValue("storesDeleted").intValue()) + assertEquals(1, findValue("storesUntouched").intValue()) + } + + transaction { + assertEquals(1, AcceptingStores.selectAll().count()) + assertEquals(1, Contacts.selectAll().count()) + assertEquals(1, Addresses.selectAll().count()) + assertEquals(1, PhysicalStores.selectAll().count()) + } + } + + private fun createMutation(project: String = "nuernberg.sozialpass.app", dryRun: Boolean = false, stores: List): String { + return """ + mutation ImportAcceptingStores { + importAcceptingStores( + project: "$project" + dryRun: $dryRun + stores: $stores + ) { + storesCreated + storesDeleted + storesUntouched + } + } + """.trimIndent() + } + + private fun createAcceptingStoreInput( + categoryId: String, + discountDE: String, + discountEN: String, + email: String, + homepage: String, + houseNumber: String, + latitude: String, + location: String, + longitude: String, + name: String, + postalCode: String, + street: String, + telephone: String + ): String { + return """ + { + categoryId: "$categoryId", + discountDE: "$discountDE", + discountEN: "$discountEN", + email: "$email" + homepage: "$homepage" + houseNumber: "$houseNumber" + latitude: "$latitude" + location: "$location" + longitude: "$longitude" + name: "$name" + postalCode: "$postalCode" + street: "$street" + telephone: "$telephone" + } + """.trimIndent() + } +}