From 53336b3accc8b65deca31208e3d92ca3c3884167 Mon Sep 17 00:00:00 2001
From: osulzhenko <osulzhenko@magnite.com>
Date: Wed, 19 Feb 2025 14:28:13 +0200
Subject: [PATCH] Add functional tests for external-uuid-security

---
 .../functional/AuthenticationCacheSpec.kt     | 189 +++++++++++++++++-
 .../PrebidCacheContainerConfig.kt             |   8 +-
 2 files changed, 191 insertions(+), 6 deletions(-)

diff --git a/src/test/kotlin/org/prebid/cache/functional/AuthenticationCacheSpec.kt b/src/test/kotlin/org/prebid/cache/functional/AuthenticationCacheSpec.kt
index 2149be3..e54c5ab 100644
--- a/src/test/kotlin/org/prebid/cache/functional/AuthenticationCacheSpec.kt
+++ b/src/test/kotlin/org/prebid/cache/functional/AuthenticationCacheSpec.kt
@@ -5,6 +5,7 @@ import io.kotest.assertions.throwables.shouldThrowExactly
 import io.kotest.core.spec.style.ShouldSpec
 import io.kotest.matchers.shouldBe
 import io.kotest.matchers.string.beEmpty
+import io.kotest.matchers.string.shouldContain
 import io.ktor.client.statement.bodyAsText
 import io.ktor.http.contentType
 import org.prebid.cache.functional.BaseSpec.Companion.prebidCacheConfig
@@ -13,6 +14,7 @@ import org.prebid.cache.functional.model.request.RequestObject
 import org.prebid.cache.functional.model.request.TransferValue
 import org.prebid.cache.functional.service.ApiException
 import org.prebid.cache.functional.util.getRandomString
+import org.prebid.cache.functional.util.getRandomUuid
 import org.springframework.http.HttpStatus.UNAUTHORIZED
 
 class AuthenticationCacheSpec : ShouldSpec({
@@ -102,7 +104,7 @@ class AuthenticationCacheSpec : ShouldSpec({
         // when: POST cache endpoint is called
         val exception = shouldThrowExactly<ApiException> { prebidCacheApi.postCache(requestObject, apiKey = null) }
 
-        // then: Bad Request exception is thrown
+        // then: Unauthorized exception is thrown
         assertSoftly {
             exception.statusCode shouldBe UNAUTHORIZED.value()
             exception.responseBody should beEmpty()
@@ -126,7 +128,7 @@ class AuthenticationCacheSpec : ShouldSpec({
         val exception =
             shouldThrowExactly<ApiException> { prebidCacheApi.postCache(requestObject, apiKey = "") }
 
-        // then: Bad Request exception is thrown
+        // then: Unauthorized exception is thrown
         assertSoftly {
             exception.statusCode shouldBe UNAUTHORIZED.value()
             exception.responseBody should beEmpty()
@@ -150,7 +152,7 @@ class AuthenticationCacheSpec : ShouldSpec({
         val exception =
             shouldThrowExactly<ApiException> { prebidCacheApi.postCache(requestObject, apiKey = getRandomString()) }
 
-        // then: Bad Request exception is thrown
+        // then: Unauthorized exception is thrown
         assertSoftly {
             exception.statusCode shouldBe UNAUTHORIZED.value()
             exception.responseBody should beEmpty()
@@ -175,10 +177,189 @@ class AuthenticationCacheSpec : ShouldSpec({
         val exception =
             shouldThrowExactly<ApiException> { prebidCacheApi.postCache(requestObject, apiKey = prebidApiKey.uppercase()) }
 
-        // then: Bad Request exception is thrown
+        // then: Unauthorized exception is thrown
         assertSoftly {
             exception.statusCode shouldBe UNAUTHORIZED.value()
             exception.responseBody should beEmpty()
         }
     }
+
+    should("should save JSON transfer value with proper api-key in header when cache-write-secured and external-uuid-secured are enabled") {
+        // given: Prebid Cache with external-uuid-secured=true property
+        val prebidApiKey = getRandomString()
+        val prebidCacheApi = BaseSpec.getPrebidCacheApi(
+            prebidCacheConfig.getBaseRedisConfig(
+                allowExternalUuid = true,
+                cacheWriteSecured = true,
+                externalUuidSecured = true,
+                apiKey = prebidApiKey
+            )
+        )
+
+        // and: Request object with JSON transfer value
+        val requestObject = RequestObject.getDefaultJsonRequestObject()
+        val requestTransferValue = objectMapper.readValue(requestObject.puts[0].value, TransferValue::class.java)
+
+        // when: POST cache endpoint is called
+        val postResponse = prebidCacheApi.postCache(requestObject, apiKey = prebidApiKey)
+
+        // when: GET cache endpoint is called
+        val getCacheResponse = BaseSpec.getPrebidCacheApi().getCache(postResponse.responses[0].uuid)
+
+        // then: response content type is the same as request object type
+        getCacheResponse.contentType()?.contentType shouldBe "application"
+        getCacheResponse.contentType()?.contentSubtype shouldBe requestObject.puts[0].type.getValue()
+
+        // and: transfer value is returned
+        val responseTransferValue = objectMapper.readValue(getCacheResponse.bodyAsText(), TransferValue::class.java)
+
+        assertSoftly {
+            responseTransferValue.adm shouldBe requestTransferValue.adm
+            responseTransferValue.width shouldBe requestTransferValue.width
+            responseTransferValue.height shouldBe requestTransferValue.height
+        }
+    }
+
+    should("should save JSON transfer value with proper api-key in header when cache-write-secured disabled and external-uuid-secured enabled") {
+        // given: Prebid Cache with external-uuid-secured=true property
+        val prebidApiKey = getRandomString()
+        val prebidCacheApi = BaseSpec.getPrebidCacheApi(
+            prebidCacheConfig.getBaseRedisConfig(
+                allowExternalUuid = true,
+                cacheWriteSecured = false,
+                externalUuidSecured = true,
+                apiKey = prebidApiKey
+            )
+        )
+
+        // and: Request object with JSON transfer value
+        val requestObject = RequestObject.getDefaultJsonRequestObject()
+        val requestTransferValue = objectMapper.readValue(requestObject.puts[0].value, TransferValue::class.java)
+
+        // when: POST cache endpoint is called
+        val postResponse = prebidCacheApi.postCache(requestObject, apiKey = prebidApiKey)
+
+        // when: GET cache endpoint is called
+        val getCacheResponse = BaseSpec.getPrebidCacheApi().getCache(postResponse.responses[0].uuid)
+
+        // then: response content type is the same as request object type
+        getCacheResponse.contentType()?.contentType shouldBe "application"
+        getCacheResponse.contentType()?.contentSubtype shouldBe requestObject.puts[0].type.getValue()
+
+        // and: transfer value is returned
+        val responseTransferValue = objectMapper.readValue(getCacheResponse.bodyAsText(), TransferValue::class.java)
+
+        assertSoftly {
+            responseTransferValue.adm shouldBe requestTransferValue.adm
+            responseTransferValue.width shouldBe requestTransferValue.width
+            responseTransferValue.height shouldBe requestTransferValue.height
+        }
+    }
+
+    should("should save JSON transfer value with proper api-key in header when cache-write-secured and external-uuid-secured are disabled") {
+        // given: Prebid Cache with external-uuid-secured=false property
+        val prebidCacheApi = BaseSpec.getPrebidCacheApi(
+            prebidCacheConfig.getBaseRedisConfig(
+                allowExternalUuid = true,
+                cacheWriteSecured = false,
+                externalUuidSecured = false,
+                apiKey = getRandomString()
+            )
+        )
+
+        // and: Request object with JSON transfer value
+        val requestObject = RequestObject.getDefaultJsonRequestObject()
+        val requestTransferValue = objectMapper.readValue(requestObject.puts[0].value, TransferValue::class.java)
+
+        // when: POST cache endpoint is called
+        val postResponse = prebidCacheApi.postCache(requestObject)
+
+        // when: GET cache endpoint is called
+        val getCacheResponse = BaseSpec.getPrebidCacheApi().getCache(postResponse.responses[0].uuid)
+
+        // then: response content type is the same as request object type
+        getCacheResponse.contentType()?.contentType shouldBe "application"
+        getCacheResponse.contentType()?.contentSubtype shouldBe requestObject.puts[0].type.getValue()
+
+        // and: transfer value is returned
+        val responseTransferValue = objectMapper.readValue(getCacheResponse.bodyAsText(), TransferValue::class.java)
+
+        assertSoftly {
+            responseTransferValue.adm shouldBe requestTransferValue.adm
+            responseTransferValue.width shouldBe requestTransferValue.width
+            responseTransferValue.height shouldBe requestTransferValue.height
+        }
+    }
+
+    should("throw an exception when api key is missing for request cache_write_secured=true and external_uuid_Secured=true") {
+        // given: Prebid Cache with external-uuid-secured=true property
+        val prebidCacheApi = BaseSpec.getPrebidCacheApi(
+            prebidCacheConfig.getBaseRedisConfig(
+                allowExternalUuid = true,
+                cacheWriteSecured = true,
+                externalUuidSecured = true,
+                apiKey = getRandomString()
+            )
+        )
+
+        // and: Request object with set payload transfer key
+        val requestObject = RequestObject.getDefaultJsonRequestObject().apply { puts[0].key = getRandomUuid() }
+
+        // when: POST cache endpoint is called
+        val exception = shouldThrowExactly<ApiException> { prebidCacheApi.postCache(requestObject) }
+
+        // then: Unauthorized exception is thrown
+        assertSoftly {
+            exception.statusCode shouldBe UNAUTHORIZED.value()
+            exception.responseBody should beEmpty()
+        }
+    }
+
+    should("throw an exception when api key is missing for request and external_uuid_Secured=true") {
+        // given: Prebid Cache with external-uuid-secured=true property
+        val prebidCacheApi = BaseSpec.getPrebidCacheApi(
+            prebidCacheConfig.getBaseRedisConfig(
+                allowExternalUuid = true,
+                cacheWriteSecured = false,
+                externalUuidSecured = true,
+                apiKey = getRandomString()
+            )
+        )
+
+        // and: Request object with set payload transfer key
+        val requestObject = RequestObject.getDefaultJsonRequestObject().apply { puts[0].key = getRandomUuid() }
+
+        // when: POST cache endpoint is called
+        val exception = shouldThrowExactly<ApiException> { prebidCacheApi.postCache(requestObject) }
+
+        // then: Unauthorized exception is thrown
+        assertSoftly {
+            exception.statusCode shouldBe UNAUTHORIZED.value()
+            exception.responseBody shouldContain "\"message\":\"Prebid cache host forbids specifying UUID in request by unauthorized users.\""
+        }
+    }
+
+    should("throw an exception when api key is mismatched for request and external-uuid-secured=true") {
+        // given: Prebid Cache with external-uuid-secured=true property
+        val prebidCacheApi = BaseSpec.getPrebidCacheApi(
+            prebidCacheConfig.getBaseRedisConfig(
+                allowExternalUuid = true,
+                cacheWriteSecured = false,
+                externalUuidSecured = true,
+                apiKey = getRandomString()
+            )
+        )
+
+        // and: Request object with set payload transfer key
+        val requestObject = RequestObject.getDefaultJsonRequestObject().apply { puts[0].key = getRandomUuid() }
+
+        // when: POST cache endpoint is called
+        val exception = shouldThrowExactly<ApiException> { prebidCacheApi.postCache(requestObject, apiKey = getRandomString()) }
+
+        // then: Unauthorized exception is thrown
+        assertSoftly {
+            exception.statusCode shouldBe UNAUTHORIZED.value()
+            exception.responseBody shouldContain "\"message\":\"Prebid cache host forbids specifying UUID in request by unauthorized users.\""
+        }
+    }
 })
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 8a3c79a..c18b5a2 100644
--- a/src/test/kotlin/org/prebid/cache/functional/testcontainers/PrebidCacheContainerConfig.kt
+++ b/src/test/kotlin/org/prebid/cache/functional/testcontainers/PrebidCacheContainerConfig.kt
@@ -16,9 +16,10 @@ class PrebidCacheContainerConfig(
     fun getBaseRedisConfig(
         allowExternalUuid: Boolean,
         cacheWriteSecured: Boolean = false,
+        externalUuidSecured: Boolean = false,
         apiKey: String? = null
     ): Map<String, String> =
-        getBaseConfig(allowExternalUuid, cacheWriteSecured, apiKey) + getRedisConfig()
+        getBaseConfig(allowExternalUuid, cacheWriteSecured, apiKey, externalUuidSecured) + getRedisConfig()
 
     fun getBaseAerospikeConfig(
         allowExternalUuid: Boolean,
@@ -104,13 +105,14 @@ class PrebidCacheContainerConfig(
         allowExternalUuid: Boolean,
         cacheWriteSecured: Boolean = false,
         apiKey: String? = null,
+        externalUuidSecured: Boolean? = null,
         endpoint: String = "/storage"
     ): Map<String, String> =
         getCachePrefixConfig() +
                 getCacheExpiryConfig() +
                 getAllowExternalUuidConfig(allowExternalUuid) +
                 getCacheTimeoutConfig("2500") +
-                getApiConfig(endpoint, apiKey, cacheWriteSecured)
+                getApiConfig(endpoint, apiKey, externalUuidSecured, cacheWriteSecured)
 
     private fun getCachePrefixConfig(): Map<String, String> = mapOf("cache.prefix" to "prebid_")
 
@@ -120,10 +122,12 @@ class PrebidCacheContainerConfig(
     private fun getApiConfig(
         endpoint: String,
         apiKey: String? = null,
+        externalUuidSecured: Boolean? = null,
         allowPublicWrite: Boolean? = null
     ): Map<String, String> = buildMap {
         put("api.storage-path", endpoint)
         apiKey?.let { put("api.api-key", it) }
+        externalUuidSecured?.let { put("api.external-u-u-i-d-secured", it.toString()) }
         allowPublicWrite?.let { put("api.cache-write-secured", it.toString()) }
     }
 }
