diff --git a/pom.xml b/pom.xml
index 25d6287bcc9..f02c03d7f4a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -332,6 +332,11 @@
mysql
test
+
+ org.testcontainers
+ localstack
+ test
+
org.testcontainers
postgresql
diff --git a/src/test/groovy/org/prebid/server/functional/service/S3Service.groovy b/src/test/groovy/org/prebid/server/functional/service/S3Service.groovy
new file mode 100644
index 00000000000..4a25b6d6ca0
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/service/S3Service.groovy
@@ -0,0 +1,103 @@
+package org.prebid.server.functional.service
+
+import org.prebid.server.functional.model.config.AccountConfig
+import org.prebid.server.functional.model.db.StoredImp
+import org.prebid.server.functional.model.db.StoredRequest
+import org.prebid.server.functional.model.db.StoredResponse
+import org.prebid.server.functional.util.ObjectMapperWrapper
+import org.testcontainers.containers.localstack.LocalStackContainer
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
+import software.amazon.awssdk.core.sync.RequestBody
+import software.amazon.awssdk.regions.Region
+import software.amazon.awssdk.services.s3.S3Client
+import software.amazon.awssdk.services.s3.model.CreateBucketRequest
+import software.amazon.awssdk.services.s3.model.DeleteBucketRequest
+import software.amazon.awssdk.services.s3.model.DeleteObjectRequest
+import software.amazon.awssdk.services.s3.model.ListObjectsV2Request
+import software.amazon.awssdk.services.s3.model.PutObjectRequest
+import software.amazon.awssdk.services.s3.model.PutObjectResponse
+
+final class S3Service implements ObjectMapperWrapper {
+
+ private final S3Client s3PbsService
+ private final LocalStackContainer localStackContainer
+
+ static final def DEFAULT_ACCOUNT_DIR = 'account'
+ static final def DEFAULT_IMPS_DIR = 'stored-impressions'
+ static final def DEFAULT_REQUEST_DIR = 'stored-requests'
+ static final def DEFAULT_RESPONSE_DIR = 'stored-responses'
+
+ S3Service(LocalStackContainer localStackContainer) {
+ this.localStackContainer = localStackContainer
+ s3PbsService = S3Client.builder()
+ .endpointOverride(localStackContainer.getEndpointOverride(LocalStackContainer.Service.S3))
+ .credentialsProvider(
+ StaticCredentialsProvider.create(
+ AwsBasicCredentials.create(
+ localStackContainer.getAccessKey(),
+ localStackContainer.getSecretKey())))
+ .region(Region.of(localStackContainer.getRegion()))
+ .build()
+ }
+
+ String getAccessKeyId() {
+ localStackContainer.accessKey
+ }
+
+ String getSecretKeyId() {
+ localStackContainer.secretKey
+ }
+
+ String getEndpoint() {
+ "http://${localStackContainer.getNetworkAliases().get(0)}:${localStackContainer.getExposedPorts().get(0)}"
+ }
+
+ String getRegion() {
+ localStackContainer.region
+ }
+
+ void createBucket(String bucketName) {
+ CreateBucketRequest createBucketRequest = CreateBucketRequest.builder()
+ .bucket(bucketName)
+ .build()
+ s3PbsService.createBucket(createBucketRequest)
+ }
+
+ void deleteBucket(String bucketName) {
+ DeleteBucketRequest deleteBucketRequest = DeleteBucketRequest.builder()
+ .bucket(bucketName)
+ .build()
+ s3PbsService.deleteBucket(deleteBucketRequest)
+ }
+
+ void purgeBucketFiles(String bucketName) {
+ s3PbsService.listObjectsV2(ListObjectsV2Request.builder().bucket(bucketName).build()).contents().each { files ->
+ s3PbsService.deleteObject(DeleteObjectRequest.builder().bucket(bucketName).key(files.key()).build())
+ }
+ }
+
+ PutObjectResponse uploadAccount(String bucketName, AccountConfig account, String fileName = account.id) {
+ uploadFile(bucketName, encode(account), "${DEFAULT_ACCOUNT_DIR}/${fileName}.json")
+ }
+
+ PutObjectResponse uploadStoredRequest(String bucketName, StoredRequest storedRequest, String fileName = storedRequest.requestId) {
+ uploadFile(bucketName, encode(storedRequest.requestData), "${DEFAULT_REQUEST_DIR}/${fileName}.json")
+ }
+
+ PutObjectResponse uploadStoredResponse(String bucketName, StoredResponse storedRequest, String fileName = storedRequest.responseId) {
+ uploadFile(bucketName, encode(storedRequest.storedAuctionResponse), "${DEFAULT_RESPONSE_DIR}/${fileName}.json")
+ }
+
+ PutObjectResponse uploadStoredImp(String bucketName, StoredImp storedImp, String fileName = storedImp.impId) {
+ uploadFile(bucketName, encode(storedImp.impData), "${DEFAULT_IMPS_DIR}/${fileName}.json")
+ }
+
+ PutObjectResponse uploadFile(String bucketName, String fileBody, String path) {
+ PutObjectRequest putObjectRequest = PutObjectRequest.builder()
+ .bucket(bucketName)
+ .key(path)
+ .build()
+ s3PbsService.putObject(putObjectRequest, RequestBody.fromString(fileBody))
+ }
+}
diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy
index 53cbecf2289..ef2575ea3ed 100644
--- a/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy
+++ b/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy
@@ -4,8 +4,10 @@ import org.prebid.server.functional.testcontainers.container.NetworkServiceConta
import org.prebid.server.functional.util.SystemProperties
import org.testcontainers.containers.MySQLContainer
import org.testcontainers.containers.Network
+import org.testcontainers.containers.localstack.LocalStackContainer
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.lifecycle.Startables
+import org.testcontainers.utility.DockerImageName
import static org.prebid.server.functional.util.SystemProperties.MOCKSERVER_VERSION
@@ -34,16 +36,20 @@ class Dependencies {
static final NetworkServiceContainer networkServiceContainer = new NetworkServiceContainer(MOCKSERVER_VERSION)
.withNetwork(network)
+ static final LocalStackContainer localStackContainer = new LocalStackContainer(DockerImageName.parse("localstack/localstack:s3-latest"))
+ .withNetwork(Dependencies.network)
+ .withServices(LocalStackContainer.Service.S3)
+
static void start() {
if (IS_LAUNCH_CONTAINERS) {
- Startables.deepStart([networkServiceContainer, mysqlContainer])
+ Startables.deepStart([networkServiceContainer, mysqlContainer, localStackContainer])
.join()
}
}
static void stop() {
if (IS_LAUNCH_CONTAINERS) {
- [networkServiceContainer, mysqlContainer].parallelStream()
+ [networkServiceContainer, mysqlContainer, localStackContainer].parallelStream()
.forEach({ it.stop() })
}
}
diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/AccountS3Spec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/AccountS3Spec.groovy
new file mode 100644
index 00000000000..3a87be7b9e7
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/tests/storage/AccountS3Spec.groovy
@@ -0,0 +1,118 @@
+package org.prebid.server.functional.tests.storage
+
+import org.prebid.server.functional.model.AccountStatus
+import org.prebid.server.functional.model.config.AccountConfig
+import org.prebid.server.functional.model.request.auction.BidRequest
+import org.prebid.server.functional.service.PrebidServerException
+import org.prebid.server.functional.service.PrebidServerService
+import org.prebid.server.functional.service.S3Service
+import org.prebid.server.functional.testcontainers.PbsServiceFactory
+import org.prebid.server.functional.util.PBSUtils
+
+import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED
+
+class AccountS3Spec extends StorageBaseSpec {
+
+ protected PrebidServerService s3StorageAccountPbsService = PbsServiceFactory.getService(s3StorageConfig +
+ mySqlDisabledConfig +
+ ['settings.enforce-valid-account': 'true'])
+
+ def "PBS should process request when active account is present in S3 storage"() {
+ given: "Default BidRequest with account"
+ def accountId = PBSUtils.randomNumber as String
+ def bidRequest = BidRequest.defaultBidRequest.tap {
+ setAccountId(accountId)
+ }
+
+ and: "Active account config"
+ def account = new AccountConfig(id: accountId, status: AccountStatus.ACTIVE)
+
+ and: "Saved account in AWS S3 storage"
+ s3Service.uploadAccount(DEFAULT_BUCKET, account)
+
+ when: "PBS processes auction request"
+ def response = s3StorageAccountPbsService.sendAuctionRequest(bidRequest)
+
+ then: "Response should contain seatbid"
+ assert response.seatbid.size() == 1
+ }
+
+ def "PBS should throw exception when inactive account is present in S3 storage"() {
+ given: "Default BidRequest with account"
+ def accountId = PBSUtils.randomNumber as String
+ def bidRequest = BidRequest.defaultBidRequest.tap {
+ setAccountId(accountId)
+ }
+
+ and: "Inactive account config"
+ def account = new AccountConfig(id: accountId, status: AccountStatus.INACTIVE)
+
+ and: "Saved account in AWS S3 storage"
+ s3Service.uploadAccount(DEFAULT_BUCKET, account)
+
+ when: "PBS processes auction request"
+ s3StorageAccountPbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS should reject the entire auction"
+ def exception = thrown(PrebidServerException)
+ assert exception.statusCode == UNAUTHORIZED.code()
+ assert exception.responseBody == "Account $accountId is inactive"
+ }
+
+ def "PBS should throw exception when account id isn't match with bid request account id"() {
+ given: "Default BidRequest with account"
+ def accountId = PBSUtils.randomNumber as String
+ def bidRequest = BidRequest.defaultBidRequest.tap {
+ setAccountId(accountId)
+ }
+
+ and: "Account config with different accountId"
+ def account = new AccountConfig(id: PBSUtils.randomString, status: AccountStatus.ACTIVE)
+
+ and: "Saved account in AWS S3 storage"
+ s3Service.uploadAccount(DEFAULT_BUCKET, account, accountId)
+
+ when: "PBS processes auction request"
+ s3StorageAccountPbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS should reject the entire auction"
+ def exception = thrown(PrebidServerException)
+ assert exception.statusCode == UNAUTHORIZED.code()
+ assert exception.responseBody == "Unauthorized account id: ${accountId}"
+ }
+
+ def "PBS should throw exception when account is invalid in S3 storage json file"() {
+ given: "Default BidRequest"
+ def accountId = PBSUtils.randomNumber as String
+ def bidRequest = BidRequest.defaultBidRequest.tap {
+ setAccountId(accountId)
+ }
+
+ and: "Saved invalid account in AWS S3 storage"
+ s3Service.uploadFile(DEFAULT_BUCKET, INVALID_FILE_BODY, "${S3Service.DEFAULT_ACCOUNT_DIR}/${accountId}.json")
+
+ when: "PBS processes auction request"
+ s3StorageAccountPbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS should reject the entire auction"
+ def exception = thrown(PrebidServerException)
+ assert exception.statusCode == UNAUTHORIZED.code()
+ assert exception.responseBody == "Unauthorized account id: ${accountId}"
+ }
+
+ def "PBS should throw exception when account is not present in S3 storage and valid account enforced"() {
+ given: "Default BidRequest"
+ def accountId = PBSUtils.randomNumber as String
+ def bidRequest = BidRequest.defaultBidRequest.tap {
+ setAccountId(accountId)
+ }
+
+ when: "PBS processes auction request"
+ s3StorageAccountPbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS should reject the entire auction"
+ def exception = thrown(PrebidServerException)
+ assert exception.statusCode == UNAUTHORIZED.code()
+ assert exception.responseBody == "Unauthorized account id: ${accountId}"
+ }
+}
diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/AmpS3Spec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/AmpS3Spec.groovy
new file mode 100644
index 00000000000..e6dda6b407c
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/tests/storage/AmpS3Spec.groovy
@@ -0,0 +1,115 @@
+package org.prebid.server.functional.tests.storage
+
+import org.prebid.server.functional.model.db.StoredRequest
+import org.prebid.server.functional.model.request.amp.AmpRequest
+import org.prebid.server.functional.model.request.auction.BidRequest
+import org.prebid.server.functional.model.request.auction.Site
+import org.prebid.server.functional.service.PrebidServerException
+import org.prebid.server.functional.service.S3Service
+import org.prebid.server.functional.util.PBSUtils
+import spock.lang.PendingFeature
+
+import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST
+
+class AmpS3Spec extends StorageBaseSpec {
+
+ def "PBS should take parameters from the stored request on S3 service when it's not specified in the request"() {
+ given: "AMP request"
+ def ampRequest = new AmpRequest(tagId: PBSUtils.randomString).tap {
+ account = PBSUtils.randomNumber as String
+ }
+
+ and: "Default stored request"
+ def ampStoredRequest = BidRequest.defaultStoredRequest.tap {
+ site = Site.defaultSite
+ setAccountId(ampRequest.account)
+ }
+
+ and: "Stored request in S3 service"
+ def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
+ s3Service.uploadStoredRequest(DEFAULT_BUCKET, storedRequest)
+
+ when: "PBS processes amp request"
+ s3StoragePbsService.sendAmpRequest(ampRequest)
+
+ then: "Bidder request should contain parameters from the stored request"
+ def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id)
+
+ assert bidderRequest.site?.page == ampStoredRequest.site.page
+ assert bidderRequest.site?.publisher?.id == ampStoredRequest.site.publisher.id
+ assert !bidderRequest.imp[0]?.tagId
+ assert bidderRequest.imp[0]?.banner?.format[0]?.height == ampStoredRequest.imp[0].banner.format[0].height
+ assert bidderRequest.imp[0]?.banner?.format[0]?.weight == ampStoredRequest.imp[0].banner.format[0].weight
+ assert bidderRequest.regs?.gdpr == ampStoredRequest.regs.gdpr
+ }
+
+ @PendingFeature
+ def "PBS should throw exception when trying to take parameters from the stored request on S3 service with invalid id in file"() {
+ given: "AMP request"
+ def ampRequest = new AmpRequest(tagId: PBSUtils.randomString).tap {
+ account = PBSUtils.randomNumber as String
+ }
+
+ and: "Default stored request"
+ def ampStoredRequest = BidRequest.defaultStoredRequest.tap {
+ site = Site.defaultSite
+ setAccountId(ampRequest.account)
+ }
+
+ and: "Stored request in S3 service"
+ def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest).tap {
+ it.requestId = PBSUtils.randomNumber
+ }
+ s3Service.uploadStoredRequest(DEFAULT_BUCKET, storedRequest, ampRequest.tagId)
+
+ when: "PBS processes amp request"
+ s3StoragePbsService.sendAmpRequest(ampRequest)
+
+ then: "PBS should throw request format error"
+ def exception = thrown(PrebidServerException)
+ assert exception.statusCode == BAD_REQUEST.code()
+ assert exception.responseBody == "Invalid request format: Stored request processing failed: " +
+ "No stored request found for id: ${ampRequest.tagId}"
+ }
+
+ def "PBS should throw exception when trying to take parameters from request where id isn't match with stored request id"() {
+ given: "AMP request"
+ def ampRequest = new AmpRequest(tagId: PBSUtils.randomString).tap {
+ account = PBSUtils.randomNumber as String
+ }
+
+ and: "Default stored request"
+ def ampStoredRequest = BidRequest.defaultStoredRequest.tap {
+ site = Site.defaultSite
+ setAccountId(ampRequest.account)
+ }
+
+ and: "Stored request in S3 service"
+ s3Service.uploadFile(DEFAULT_BUCKET, INVALID_FILE_BODY, "${S3Service.DEFAULT_REQUEST_DIR}/${ampRequest.tagId}.json")
+
+ when: "PBS processes amp request"
+ s3StoragePbsService.sendAmpRequest(ampRequest)
+
+ then: "PBS should throw request format error"
+ def exception = thrown(PrebidServerException)
+ assert exception.statusCode == BAD_REQUEST.code()
+ assert exception.responseBody == "Invalid request format: Stored request processing failed: " +
+ "Can't parse Json for stored request with id ${ampRequest.tagId}"
+ }
+
+ def "PBS should throw an exception when trying to take parameters from stored request on S3 service that do not exist"() {
+ given: "AMP request"
+ def ampRequest = new AmpRequest(tagId: PBSUtils.randomString).tap {
+ account = PBSUtils.randomNumber as String
+ }
+
+ when: "PBS processes amp request"
+ s3StoragePbsService.sendAmpRequest(ampRequest)
+
+ then: "PBS should throw request format error"
+ def exception = thrown(PrebidServerException)
+ assert exception.statusCode == BAD_REQUEST.code()
+ assert exception.responseBody == "Invalid request format: Stored request processing failed: " +
+ "No stored request found for id: ${ampRequest.tagId}"
+ }
+}
diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/AuctionS3Spec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/AuctionS3Spec.groovy
new file mode 100644
index 00000000000..51d39dd5af9
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/tests/storage/AuctionS3Spec.groovy
@@ -0,0 +1,117 @@
+package org.prebid.server.functional.tests.storage
+
+import org.prebid.server.functional.model.db.StoredImp
+import org.prebid.server.functional.model.request.auction.BidRequest
+import org.prebid.server.functional.model.request.auction.Imp
+import org.prebid.server.functional.model.request.auction.PrebidStoredRequest
+import org.prebid.server.functional.model.request.auction.SecurityLevel
+import org.prebid.server.functional.service.PrebidServerException
+import org.prebid.server.functional.service.S3Service
+import org.prebid.server.functional.util.PBSUtils
+import spock.lang.PendingFeature
+
+import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST
+
+class AuctionS3Spec extends StorageBaseSpec {
+
+ def "PBS auction should populate imp[0].secure depend which value in imp stored request from S3 service"() {
+ given: "Default bid request"
+ def storedRequestId = PBSUtils.randomString
+ def bidRequest = BidRequest.defaultBidRequest.tap {
+ imp[0].tap {
+ it.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId)
+ it.secure = null
+ }
+ }
+
+ and: "Save storedImp into S3 service"
+ def secureStoredRequest = PBSUtils.getRandomEnum(SecurityLevel.class)
+ def storedImp = StoredImp.getStoredImp(bidRequest).tap {
+ impData = Imp.defaultImpression.tap {
+ secure = secureStoredRequest
+ }
+ }
+ s3Service.uploadStoredImp(DEFAULT_BUCKET, storedImp)
+
+ when: "Requesting PBS auction"
+ s3StoragePbsService.sendAuctionRequest(bidRequest)
+
+ then: "Response should contain imp[0].secure same value as in request"
+ def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+ assert bidderRequest.imp[0].secure == secureStoredRequest
+ }
+
+ @PendingFeature
+ def "PBS should throw exception when trying to populate imp[0].secure from imp stored request on S3 service with impId that doesn't matches"() {
+ given: "Default bid request"
+ def storedRequestId = PBSUtils.randomString
+ def bidRequest = BidRequest.defaultBidRequest.tap {
+ imp[0].tap {
+ it.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId)
+ it.secure = null
+ }
+ }
+
+ and: "Save storedImp with different impId into S3 service"
+ def secureStoredRequest = PBSUtils.getRandomNumber(0, 1)
+ def storedImp = StoredImp.getStoredImp(bidRequest).tap {
+ impId = PBSUtils.randomString
+ impData = Imp.defaultImpression.tap {
+ it.secure = secureStoredRequest
+ }
+ }
+ s3Service.uploadStoredImp(DEFAULT_BUCKET, storedImp, storedRequestId)
+
+ when: "Requesting PBS auction"
+ s3StoragePbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS should throw request format error"
+ def exception = thrown(PrebidServerException)
+ assert exception.statusCode == BAD_REQUEST.code()
+ assert exception.responseBody == "Invalid request format: Stored request processing failed: " +
+ "No stored impression found for id: ${storedRequestId}"
+ }
+
+ def "PBS should throw exception when trying to populate imp[0].secure from invalid imp stored request on S3 service"() {
+ given: "Default bid request"
+ def storedRequestId = PBSUtils.randomString
+ def bidRequest = BidRequest.defaultBidRequest.tap {
+ imp[0].tap {
+ it.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId)
+ it.secure = null
+ }
+ }
+
+ and: "Save storedImp into S3 service"
+ s3Service.uploadFile(DEFAULT_BUCKET, INVALID_FILE_BODY, "${S3Service.DEFAULT_IMPS_DIR}/${storedRequestId}.json" )
+
+ when: "Requesting PBS auction"
+ s3StoragePbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS should throw request format error"
+ def exception = thrown(PrebidServerException)
+ assert exception.statusCode == BAD_REQUEST.code()
+ assert exception.responseBody == "Invalid request format: Stored request processing failed: " +
+ "Can't parse Json for stored request with id ${storedRequestId}"
+ }
+
+ def "PBS should throw exception when trying to populate imp[0].secure from unexciting imp stored request on S3 service"() {
+ given: "Default bid request"
+ def storedRequestId = PBSUtils.randomString
+ def bidRequest = BidRequest.defaultBidRequest.tap {
+ imp[0].tap {
+ it.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId)
+ it.secure = null
+ }
+ }
+
+ when: "Requesting PBS auction"
+ s3StoragePbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS should throw request format error"
+ def exception = thrown(PrebidServerException)
+ assert exception.statusCode == BAD_REQUEST.code()
+ assert exception.responseBody == "Invalid request format: Stored request processing failed: " +
+ "No stored impression found for id: ${storedRequestId}"
+ }
+}
diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/StorageBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/StorageBaseSpec.groovy
new file mode 100644
index 00000000000..583d6d97e06
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/tests/storage/StorageBaseSpec.groovy
@@ -0,0 +1,56 @@
+package org.prebid.server.functional.tests.storage
+
+import org.prebid.server.functional.service.PrebidServerService
+import org.prebid.server.functional.service.S3Service
+import org.prebid.server.functional.testcontainers.Dependencies
+import org.prebid.server.functional.testcontainers.PbsServiceFactory
+import org.prebid.server.functional.tests.BaseSpec
+import org.prebid.server.functional.util.PBSUtils
+
+class StorageBaseSpec extends BaseSpec {
+
+ protected static final String INVALID_FILE_BODY = 'INVALID'
+ protected static final String DEFAULT_BUCKET = PBSUtils.randomString.toLowerCase()
+
+ protected static final S3Service s3Service = new S3Service(Dependencies.localStackContainer)
+
+ def setupSpec() {
+ s3Service.createBucket(DEFAULT_BUCKET)
+ }
+
+ def cleanupSpec() {
+ s3Service.purgeBucketFiles(DEFAULT_BUCKET)
+ s3Service.deleteBucket(DEFAULT_BUCKET)
+ }
+
+ protected static Map s3StorageConfig = [
+ 'settings.s3.accessKeyId' : s3Service.accessKeyId,
+ 'settings.s3.secretAccessKey' : s3Service.secretKeyId,
+ 'settings.s3.endpoint' : s3Service.endpoint,
+ 'settings.s3.bucket' : DEFAULT_BUCKET,
+ 'settings.s3.region' : s3Service.region,
+ 'settings.s3.force-path-style' : 'true',
+ 'settings.s3.accounts-dir' : S3Service.DEFAULT_ACCOUNT_DIR,
+ 'settings.s3.stored-imps-dir' : S3Service.DEFAULT_IMPS_DIR,
+ 'settings.s3.stored-requests-dir' : S3Service.DEFAULT_REQUEST_DIR,
+ 'settings.s3.stored-responses-dir': S3Service.DEFAULT_RESPONSE_DIR,
+ ]
+
+ protected static Map mySqlDisabledConfig =
+ ['settings.database.type' : null,
+ 'settings.database.host' : null,
+ 'settings.database.port' : null,
+ 'settings.database.dbname' : null,
+ 'settings.database.user' : null,
+ 'settings.database.password' : null,
+ 'settings.database.pool-size' : null,
+ 'settings.database.provider-class' : null,
+ 'settings.database.account-query' : null,
+ 'settings.database.stored-requests-query' : null,
+ 'settings.database.amp-stored-requests-query': null,
+ 'settings.database.stored-responses-query' : null
+ ].asImmutable() as Map
+
+
+ protected PrebidServerService s3StoragePbsService = PbsServiceFactory.getService(s3StorageConfig + mySqlDisabledConfig)
+}
diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/StoredResponseS3Spec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/StoredResponseS3Spec.groovy
new file mode 100644
index 00000000000..e07b5b71f2e
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/tests/storage/StoredResponseS3Spec.groovy
@@ -0,0 +1,99 @@
+package org.prebid.server.functional.tests.storage
+
+import org.prebid.server.functional.model.db.StoredResponse
+import org.prebid.server.functional.model.request.auction.BidRequest
+import org.prebid.server.functional.model.request.auction.StoredAuctionResponse
+import org.prebid.server.functional.model.response.auction.SeatBid
+import org.prebid.server.functional.service.PrebidServerException
+import org.prebid.server.functional.service.S3Service
+import org.prebid.server.functional.util.PBSUtils
+import spock.lang.PendingFeature
+
+import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST
+
+class StoredResponseS3Spec extends StorageBaseSpec {
+
+ def "PBS should return info from S3 stored auction response when it defined in request"() {
+ given: "Default basic BidRequest with stored response"
+ def bidRequest = BidRequest.defaultBidRequest
+ def storedResponseId = PBSUtils.randomNumber
+ bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId)
+
+ and: "Stored auction response in S3 storage"
+ def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest)
+ def storedResponse = new StoredResponse(responseId: storedResponseId,
+ storedAuctionResponse: storedAuctionResponse)
+ s3Service.uploadStoredResponse(DEFAULT_BUCKET, storedResponse)
+
+ when: "PBS processes auction request"
+ def response = s3StoragePbsService.sendAuctionRequest(bidRequest)
+
+ then: "Response should contain information from stored auction response"
+ assert response.id == bidRequest.id
+ assert response.seatbid[0]?.seat == storedAuctionResponse.seat
+ assert response.seatbid[0]?.bid?.size() == storedAuctionResponse.bid.size()
+ assert response.seatbid[0]?.bid[0]?.impid == storedAuctionResponse.bid[0].impid
+ assert response.seatbid[0]?.bid[0]?.price == storedAuctionResponse.bid[0].price
+ assert response.seatbid[0]?.bid[0]?.id == storedAuctionResponse.bid[0].id
+
+ and: "PBS not send request to bidder"
+ assert !bidder.getRequestCount(bidRequest.id)
+ }
+
+ @PendingFeature
+ def "PBS should throw request format exception when stored auction response id isn't match with requested response id"() {
+ given: "Default basic BidRequest with stored response"
+ def bidRequest = BidRequest.defaultBidRequest
+ def storedResponseId = PBSUtils.randomNumber
+ bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId)
+
+ and: "Stored auction response in S3 storage with different id"
+ def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest)
+ def storedResponse = new StoredResponse(responseId: PBSUtils.randomNumber,
+ storedAuctionResponse: storedAuctionResponse)
+ s3Service.uploadStoredResponse(DEFAULT_BUCKET, storedResponse, storedResponseId as String)
+
+ when: "PBS processes auction request"
+ s3StoragePbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS should throw request format error"
+ def exception = thrown(PrebidServerException)
+ assert exception.statusCode == BAD_REQUEST.code()
+ assert exception.responseBody == "Invalid request format: Failed to fetch stored auction response for " +
+ "impId = ${bidRequest.imp[0].id} and storedAuctionResponse id = ${storedResponseId}."
+ }
+
+ def "PBS should throw request format exception when invalid stored auction response defined in S3 storage"() {
+ given: "Default basic BidRequest with stored response"
+ def bidRequest = BidRequest.defaultBidRequest
+ def storedResponseId = PBSUtils.randomNumber
+ bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId)
+
+ and: "Invalid stored auction response in S3 storage"
+ s3Service.uploadFile(DEFAULT_BUCKET, INVALID_FILE_BODY, "${S3Service.DEFAULT_RESPONSE_DIR}/${storedResponseId}.json")
+
+ when: "PBS processes auction request"
+ s3StoragePbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS should throw request format error"
+ def exception = thrown(PrebidServerException)
+ assert exception.statusCode == BAD_REQUEST.code()
+ assert exception.responseBody == "Invalid request format: Can't parse Json for stored response with id ${storedResponseId}"
+ }
+
+ def "PBS should throw request format exception when stored auction response defined in request but not defined in S3 storage"() {
+ given: "Default basic BidRequest with stored response"
+ def bidRequest = BidRequest.defaultBidRequest
+ def storedResponseId = PBSUtils.randomNumber
+ bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId)
+
+ when: "PBS processes auction request"
+ s3StoragePbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS should throw request format error"
+ def exception = thrown(PrebidServerException)
+ assert exception.statusCode == BAD_REQUEST.code()
+ assert exception.responseBody == "Invalid request format: Failed to fetch stored auction response for " +
+ "impId = ${bidRequest.imp[0].id} and storedAuctionResponse id = ${storedResponseId}."
+ }
+}