diff --git a/src/test/kotlin/org/prebid/cache/functional/GeneralCacheSpec.kt b/src/test/kotlin/org/prebid/cache/functional/GeneralCacheSpec.kt index 11f3338..9a99f0b 100644 --- a/src/test/kotlin/org/prebid/cache/functional/GeneralCacheSpec.kt +++ b/src/test/kotlin/org/prebid/cache/functional/GeneralCacheSpec.kt @@ -17,7 +17,6 @@ import org.prebid.cache.functional.util.getRandomLong import org.prebid.cache.functional.util.getRandomString import org.prebid.cache.functional.util.getRandomUuid import org.springframework.http.HttpStatus.BAD_REQUEST -import org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR import org.springframework.http.HttpStatus.UNSUPPORTED_MEDIA_TYPE class GeneralCacheSpec : ShouldSpec({ diff --git a/src/test/kotlin/org/prebid/cache/functional/ModuleStorageSpec.kt b/src/test/kotlin/org/prebid/cache/functional/ModuleStorageSpec.kt new file mode 100644 index 0000000..57ca7d3 --- /dev/null +++ b/src/test/kotlin/org/prebid/cache/functional/ModuleStorageSpec.kt @@ -0,0 +1,279 @@ +package org.prebid.cache.functional + +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.core.spec.style.ShouldSpec +import io.kotest.matchers.nulls.beNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.beEmpty +import io.kotest.matchers.string.shouldContain +import java.util.Locale +import org.prebid.cache.functional.BaseSpec.Companion.prebidCacheConfig +import org.prebid.cache.functional.model.request.PayloadTransfer +import org.prebid.cache.functional.service.ApiException +import org.prebid.cache.functional.service.PrebidCacheApi +import org.prebid.cache.functional.util.getRandomString +import org.springframework.http.HttpStatus.BAD_REQUEST +import org.springframework.http.HttpStatus.NOT_FOUND +import org.springframework.http.HttpStatus.UNAUTHORIZED + +class ModuleStorageSpec : ShouldSpec({ + + lateinit var apiKey: String + lateinit var applicationName: String + lateinit var cacheApi: PrebidCacheApi + + beforeSpec { + apiKey = getRandomString() + applicationName = getRandomString().lowercase(Locale.getDefault()) + val config = prebidCacheConfig.getBaseModuleStorageConfig(applicationName, apiKey) + cacheApi = BaseSpec.getPrebidCacheApi(config) + } + + should("return the same text transfer value which was saved to module-storage") { + //given: default text payload with application + val payloadKey = getRandomString() + val payloadTransfer = PayloadTransfer.getDefaultTextPayloadTransfer().apply { + key = payloadKey + application = applicationName + } + + // when: POST module-storage endpoint is called + cacheApi.postModuleStorageCache(payloadTransfer, apiKey) + + // then: recorded payload should contain the same type and value + val savedPayload = cacheApi.getModuleStorageCache(payloadKey, applicationName, apiKey) + savedPayload.type shouldBe payloadTransfer.type + savedPayload.value shouldBe payloadTransfer.value + + // and: shouldn't contain information about application + savedPayload.application?.should(beNull()) + } + + should("return the same xml transfer value which was saved to module-storage") { + //given: default xml payload with application + val payloadKey = getRandomString() + val payloadTransfer = PayloadTransfer.getDefaultXmlPayloadTransfer().apply { + key = payloadKey + application = applicationName + } + + // when: POST module-storage endpoint is called + cacheApi.postModuleStorageCache(payloadTransfer, apiKey) + + // then: recorded payload should contain the same type and value + val savedPayload = cacheApi.getModuleStorageCache(payloadKey, applicationName, apiKey) + savedPayload.type shouldBe payloadTransfer.type + savedPayload.value shouldBe payloadTransfer.value + + // and: shouldn't contain information about application + savedPayload.application?.should(beNull()) + } + + should("return the same json transfer value which was saved to module-storage") { + //given: default json payload with application + val payloadKey = getRandomString() + val payloadTransfer = PayloadTransfer.getDefaultJsonPayloadTransfer().apply { + key = payloadKey + application = applicationName + } + + // when: POST module-storage endpoint is called + cacheApi.postModuleStorageCache(payloadTransfer, apiKey) + + // then: recorded payload should contain the same type and value + val savedPayload = cacheApi.getModuleStorageCache(payloadKey, applicationName, apiKey) + savedPayload.type shouldBe payloadTransfer.type + savedPayload.value shouldBe payloadTransfer.value + + // and: shouldn't contain information about application + savedPayload.application?.should(beNull()) + } + + should("throw an exception when post request have nonexistent PBC application name") { + //given: default text payload with nonexistent application name + val payloadKey = getRandomString() + val randomApplication = getRandomString() + val payloadTransfer = PayloadTransfer.getDefaultTextPayloadTransfer().apply { + key = payloadKey + application = randomApplication + } + + // when: POST module-storage endpoint is called + val exception = shouldThrowExactly { + cacheApi.postModuleStorageCache(payloadTransfer, apiKey) } + + // then: Not found exception is thrown + assertSoftly { + exception.statusCode shouldBe NOT_FOUND.value() + exception.responseBody shouldContain "\"path\":\"/module-storage\"" + exception.responseBody shouldContain "\"message\":\"Invalid application: ${randomApplication}\"" + } + } + + should("throw an exception when post request have null application name") { + //given: default text payload with null application name + val payloadKey = getRandomString() + val payloadTransfer = PayloadTransfer.getDefaultTextPayloadTransfer().apply { + key = payloadKey + application = null + } + + // when: POST module-storage endpoint is called + val exception = shouldThrowExactly { cacheApi.postModuleStorageCache(payloadTransfer, apiKey) } + + // then: Bad request exception is thrown + assertSoftly { + exception.statusCode shouldBe BAD_REQUEST.value() + exception.responseBody shouldContain "\"path\":\"/module-storage\"" + exception.responseBody shouldContain "application must not be empty" + } + } + + should("throw an exception when post request have empty application name") { + //given: default text payload with empty application name + val payloadKey = getRandomString() + val payloadTransfer = PayloadTransfer.getDefaultTextPayloadTransfer().apply { + key = payloadKey + application = "" + } + + // when: POST module-storage endpoint is called + val exception = shouldThrowExactly { cacheApi.postModuleStorageCache(payloadTransfer, apiKey) } + + // then: Bad request exception is thrown + assertSoftly { + exception.statusCode shouldBe BAD_REQUEST.value() + exception.responseBody shouldContain "\"path\":\"/module-storage\"" + exception.responseBody shouldContain "application must not be empty" + } + } + + should("throw an exception when post request have null key name") { + //given: default text payload with empty payloadKey + val payloadTransfer = PayloadTransfer.getDefaultTextPayloadTransfer().apply { + key = null + application = applicationName + } + + // when: POST module-storage endpoint is called + val exception = shouldThrowExactly { cacheApi.postModuleStorageCache(payloadTransfer, apiKey) } + + // then: Bad request exception is thrown + assertSoftly { + exception.statusCode shouldBe BAD_REQUEST.value() + exception.responseBody shouldContain "\"path\":\"/module-storage\"" + exception.responseBody shouldContain "key must not be empty" + } + } + + should("throw an exception when post request have empty key name") { + //given: default text payload with empty payloadKey + val payloadTransfer = PayloadTransfer.getDefaultTextPayloadTransfer().apply { + key = "" + application = applicationName + } + + // when: POST module-storage endpoint is called + val exception = shouldThrowExactly { cacheApi.postModuleStorageCache(payloadTransfer, apiKey) } + + // then: Bad request exception is thrown + assertSoftly { + exception.statusCode shouldBe BAD_REQUEST.value() + exception.responseBody shouldContain "\"path\":\"/module-storage\"" + exception.responseBody shouldContain "key must not be empty" + } + } + + should("throw an exception when post request have invalid PBC apiKey") { + //given: default text payload with application + val payloadKey = getRandomString() + val payloadTransfer = PayloadTransfer.getDefaultTextPayloadTransfer().apply { + key = payloadKey + application = applicationName + } + + // when: POST module-storage endpoint is called + val exception = + shouldThrowExactly { cacheApi.postModuleStorageCache(payloadTransfer, getRandomString()) } + + // then: Not found exception is thrown + assertSoftly { + exception.statusCode shouldBe UNAUTHORIZED.value() + exception.responseBody should beEmpty() + } + } + + should("throw an exception when get request contain invalid payload key") { + //given: default text payload with application + val payloadTransfer = PayloadTransfer.getDefaultTextPayloadTransfer().apply { + key = getRandomString() + application = applicationName + } + + // and: POST module-storage endpoint is called + cacheApi.postModuleStorageCache(payloadTransfer, apiKey) + + // when: GET module-storage endpoint is called with invalid data + val exception = shouldThrowExactly { + cacheApi.getModuleStorageCache(getRandomString(), applicationName, apiKey) + } + + // then: Not found exception is thrown + assertSoftly { + exception.statusCode shouldBe NOT_FOUND.value() + exception.responseBody shouldContain "\"path\":\"/module-storage\"" + exception.responseBody shouldContain "Invalid application or key" + } + } + + should("throw an exception when get request contain invalid application name") { + //given: default text payload with application + val payloadKey = getRandomString() + val payloadTransfer = PayloadTransfer.getDefaultTextPayloadTransfer().apply { + key = payloadKey + application = applicationName + } + + // and: POST module-storage endpoint is called + cacheApi.postModuleStorageCache(payloadTransfer, apiKey) + + //and: random application name + val randomApplication = getRandomString() + + // when: GET module-storage endpoint is called with invalid data + val exception = shouldThrowExactly { + cacheApi.getModuleStorageCache(payloadKey, randomApplication, apiKey) + } + + // then: Not found exception is thrown + assertSoftly { + exception.statusCode shouldBe NOT_FOUND.value() + exception.responseBody shouldContain "\"path\":\"/module-storage\"" + exception.responseBody shouldContain "\"message\":\"Invalid application: ${randomApplication}\"" + } + } + + should("throw an exception when get request contain invalid apiKey") { + //given: default text payload with application + val payloadKey = getRandomString() + val payloadTransfer = PayloadTransfer.getDefaultTextPayloadTransfer().apply { + key = payloadKey + application = applicationName + } + + // and: POST module-storage endpoint is called + cacheApi.postModuleStorageCache(payloadTransfer, apiKey) + + // when: GET module-storage endpoint is called with invalid data + val exception = shouldThrowExactly { + cacheApi.getModuleStorageCache(payloadKey, applicationName, getRandomString()) + } + + // then: Not found exception is thrown + assertSoftly { + exception.statusCode shouldBe UNAUTHORIZED.value() + exception.responseBody should beEmpty() + } + } +}) diff --git a/src/test/kotlin/org/prebid/cache/functional/model/request/MediaType.kt b/src/test/kotlin/org/prebid/cache/functional/model/request/MediaType.kt index 4afba48..1aee478 100644 --- a/src/test/kotlin/org/prebid/cache/functional/model/request/MediaType.kt +++ b/src/test/kotlin/org/prebid/cache/functional/model/request/MediaType.kt @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonValue enum class MediaType { - JSON, XML, UNSUPPORTED; + JSON, XML, TEXT, UNSUPPORTED; @JsonValue fun getValue(): String = name.lowercase() diff --git a/src/test/kotlin/org/prebid/cache/functional/model/request/PayloadTransfer.kt b/src/test/kotlin/org/prebid/cache/functional/model/request/PayloadTransfer.kt index 259614c..2c71d3d 100644 --- a/src/test/kotlin/org/prebid/cache/functional/model/request/PayloadTransfer.kt +++ b/src/test/kotlin/org/prebid/cache/functional/model/request/PayloadTransfer.kt @@ -4,7 +4,9 @@ import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL import org.prebid.cache.functional.mapper.objectMapper import org.prebid.cache.functional.model.request.MediaType.JSON +import org.prebid.cache.functional.model.request.MediaType.TEXT import org.prebid.cache.functional.model.request.MediaType.XML +import org.prebid.cache.functional.util.getRandomString @JsonInclude(NON_NULL) data class PayloadTransfer( @@ -14,6 +16,7 @@ data class PayloadTransfer( var expiry: Long? = null, var ttlseconds: Long? = null, var prefix: String? = null, + var application: String? = null, //Fields not affected on PBC var timestamp: Long? = null, @@ -23,18 +26,18 @@ data class PayloadTransfer( ) { companion object { + private const val DEFAULT_EXPIRY = 300L + + private fun getDefaultPayloadTransfer(type: MediaType, value: String): PayloadTransfer = + PayloadTransfer(type = type, value = value, expiry = DEFAULT_EXPIRY, ttlseconds = 3000L) + fun getDefaultJsonPayloadTransfer(): PayloadTransfer = - PayloadTransfer( - type = JSON, - value = objectMapper.writeValueAsString(TransferValue.getDefaultJsonValue()), - expiry = 300 - ) + getDefaultPayloadTransfer(JSON, objectMapper.writeValueAsString(TransferValue.getDefaultJsonValue())) fun getDefaultXmlPayloadTransfer(): PayloadTransfer = - PayloadTransfer( - type = XML, - value = objectMapper.writeValueAsString(TransferValue.getDefaultXmlValue()), - expiry = 300 - ) + getDefaultPayloadTransfer(XML, objectMapper.writeValueAsString(TransferValue.getDefaultXmlValue())) + + fun getDefaultTextPayloadTransfer(): PayloadTransfer = + getDefaultPayloadTransfer(TEXT, getRandomString()) } } diff --git a/src/test/kotlin/org/prebid/cache/functional/model/request/RequestObject.kt b/src/test/kotlin/org/prebid/cache/functional/model/request/RequestObject.kt index 2d08fb8..b027194 100644 --- a/src/test/kotlin/org/prebid/cache/functional/model/request/RequestObject.kt +++ b/src/test/kotlin/org/prebid/cache/functional/model/request/RequestObject.kt @@ -11,5 +11,8 @@ data class RequestObject(var puts: List) { fun getDefaultXmlRequestObject(): RequestObject = RequestObject(puts = listOf(PayloadTransfer.getDefaultXmlPayloadTransfer())) + + fun getDefaultTextRequestObject(): RequestObject = + RequestObject(puts = listOf(PayloadTransfer.getDefaultTextPayloadTransfer())) } } diff --git a/src/test/kotlin/org/prebid/cache/functional/service/PrebidCacheApi.kt b/src/test/kotlin/org/prebid/cache/functional/service/PrebidCacheApi.kt index b164ffe..67c21f0 100644 --- a/src/test/kotlin/org/prebid/cache/functional/service/PrebidCacheApi.kt +++ b/src/test/kotlin/org/prebid/cache/functional/service/PrebidCacheApi.kt @@ -16,12 +16,38 @@ import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.bodyAsText import io.ktor.http.ContentType.Application.Json import io.ktor.http.HttpHeaders.ContentType +import io.ktor.http.HttpStatusCode import io.ktor.serialization.jackson.jackson +import org.prebid.cache.functional.model.request.PayloadTransfer import org.prebid.cache.functional.model.request.RequestObject import org.prebid.cache.functional.model.response.ResponseObject class PrebidCacheApi(prebidCacheHost: String, prebidCachePort: Int) { + suspend fun getCache(uuid: String?, proxyCacheHost: String? = null): HttpResponse = + get(CACHE_ENDPOINT, mapOf(UUID_QUERY_PARAMETER to uuid, PROXY_CACHE_HOST_QUERY_PARAMETER to proxyCacheHost)) + + suspend fun postCache(requestObject: RequestObject, secondaryCache: String? = null): ResponseObject = + post(CACHE_ENDPOINT, requestObject, mapOf(SECONDARY_CACHE_QUERY_PARAMETER to secondaryCache)).body() + + suspend fun getModuleStorageCache( + payloadTransferKey: String?, + application: String?, + apiKey: String? + ): PayloadTransfer = + get( + MODULE_STORAGE_ENDPOINT, + mapOf(KEY_PARAMETER to payloadTransferKey, APPLICATION_PARAMETER to application), + mapOf(API_KEY_PARAMETER to apiKey) + ).body() + + suspend fun postModuleStorageCache(requestObject: PayloadTransfer, apiKey: String? = null): Boolean = + post( + MODULE_STORAGE_ENDPOINT, + requestObject, + headers = mapOf(API_KEY_PARAMETER to apiKey) + ).status == HttpStatusCode.NoContent + private val client = HttpClient(Apache) { expectSuccess = true defaultRequest { @@ -41,22 +67,44 @@ class PrebidCacheApi(prebidCacheHost: String, prebidCachePort: Int) { } } - suspend fun getCache(uuid: String?, proxyCacheHost: String? = null): HttpResponse = - client.get(CACHE_ENDPOINT) { - if (uuid != null) parameter(UUID_QUERY_PARAMETER, uuid) - if (proxyCacheHost != null) parameter(PROXY_CACHE_HOST_QUERY_PARAMETER, proxyCacheHost) + private suspend fun get( + endpoint: String, + parameters: Map = emptyMap(), + headers: Map = emptyMap() + ): HttpResponse = client.get(endpoint) { + parameters.forEach { (key, value) -> + value?.let { parameter(key, value) } } + headers.forEach { (key, value) -> + value?.let { header(key, value) } + } + } - suspend fun postCache(requestObject: RequestObject, secondaryCache: String? = null): ResponseObject = - client.post(CACHE_ENDPOINT) { - if (secondaryCache != null) parameter(SECONDARY_CACHE_QUERY_PARAMETER, secondaryCache) + private suspend fun post( + endpoint: String, + requestObject: Any, + parameters: Map = emptyMap(), + headers: Map = emptyMap() + ): HttpResponse = + client.post(endpoint) { setBody(requestObject) - }.body() + parameters.forEach { (key, value) -> + value?.let { parameter(key, value) } + } + headers.forEach { (key, value) -> + value?.let { header(key, value) } + } + } companion object { private const val CACHE_ENDPOINT = "/cache" private const val UUID_QUERY_PARAMETER = "uuid" private const val PROXY_CACHE_HOST_QUERY_PARAMETER = "ch" private const val SECONDARY_CACHE_QUERY_PARAMETER = "secondaryCache" + + private const val MODULE_STORAGE_ENDPOINT = "/module-storage" + private const val API_KEY_PARAMETER = "x-pbc-api-key" + private const val KEY_PARAMETER = "k" + private const val APPLICATION_PARAMETER = "a" } } diff --git a/src/test/kotlin/org/prebid/cache/functional/testcontainers/PrebidCacheContainerConfig.kt b/src/test/kotlin/org/prebid/cache/functional/testcontainers/PrebidCacheContainerConfig.kt index 8ad125e..9fb389f 100644 --- a/src/test/kotlin/org/prebid/cache/functional/testcontainers/PrebidCacheContainerConfig.kt +++ b/src/test/kotlin/org/prebid/cache/functional/testcontainers/PrebidCacheContainerConfig.kt @@ -13,6 +13,9 @@ class PrebidCacheContainerConfig(private val redisHost: String, private val aero fun getBaseAerospikeConfig(allowExternalUuid: String, aerospikeNamespace: String = NAMESPACE): Map = getBaseConfig(allowExternalUuid) + getAerospikeConfig(aerospikeNamespace) + fun getBaseModuleStorageConfig(applicationName: String, apiKey: String): Map = + getBaseConfig("true") + getModuleStorageRedisConfig(apiKey, applicationName) + getRedisConfig() + fun getCacheExpiryConfig(minExpiry: String = "15", maxExpiry: String = "28800"): Map = mapOf( "cache.min.expiry" to minExpiry, @@ -57,11 +60,22 @@ class PrebidCacheContainerConfig(private val redisHost: String, private val aero "spring.aerospike.namespace" to aerospikeNamespace ) + private fun getModuleStorageRedisConfig( + apiKey: String, + applicationName: String, + timeoutMs: Long = 9999L + ): Map = + mapOf( + "api.api-key" to apiKey, + "module.storage.redis.${applicationName}.port" to RedisContainer.PORT.toString(), + "module.storage.redis.${applicationName}.host" to redisHost, + "module.storage.redis.${applicationName}.timeout" to timeoutMs.toString(), + ) + private fun getBaseConfig(allowExternalUuid: String): Map = getCachePrefixConfig() + getCacheExpiryConfig() + getAllowExternalUuidConfig(allowExternalUuid) + getCacheTimeoutConfig("2500") - private fun getCachePrefixConfig(): Map = mapOf("cache.prefix" to "prebid_") private fun getAllowExternalUuidConfig(allowExternalUuid: String): Map =