diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml
index 5ec4971c40d..6958b0dc3b9 100644
--- a/extra/bundle/pom.xml
+++ b/extra/bundle/pom.xml
@@ -60,6 +60,11 @@
optable-targeting
${project.version}
+
+ org.prebid.server.hooks.modules
+ wurfl-devicedetection
+ ${project.version}
+
diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml
index 295bb47dbc1..10cfac79c6e 100644
--- a/extra/modules/pom.xml
+++ b/extra/modules/pom.xml
@@ -25,6 +25,7 @@
greenbids-real-time-data
pb-request-correction
optable-targeting
+ wurfl-devicedetection
diff --git a/extra/modules/wurfl-devicedetection/README.md b/extra/modules/wurfl-devicedetection/README.md
new file mode 100644
index 00000000000..2a5f9959c03
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/README.md
@@ -0,0 +1,250 @@
+## WURFL Device Enrichment Module
+
+### Overview
+
+The **WURFL Device Enrichment Module** for Prebid Server enhances the OpenRTB 2.x payload
+with comprehensive device detection data powered by **ScientiaMobile**’s WURFL device detection framework.
+Thanks to WURFL's device knowledge, the module provides accurate and comprehensive device-related information,
+enabling bidders to make better-informed targeting and optimization decisions.
+
+### Key features
+
+#### Device Field Enrichment:
+
+The WURFL module populates missing or empty fields in ortb2.device with the following data:
+ - **make**: Manufacturer of the device (e.g., "Apple", "Samsung").
+ - **model**: Device model (e.g., "iPhone 14", "Galaxy S22").
+ - **os**: Operating system (e.g., "iOS", "Android").
+ - **osv**: Operating system version (e.g., "16.0", "12.0").
+ - **h**: Screen height in pixels.
+ - **w**: Screen width in pixels.
+ - **ppi**: Screen pixels per inch (PPI).
+ - **pxratio**: Screen pixel density ratio.
+ - **devicetype**: Device type (e.g., mobile, tablet, desktop).
+ - **js**: Support for JavaScript, where 0 = no, 1 = yes
+ - **Note**: If these fields are already populated in the bid request, the module will not overwrite them.
+
+#### Publisher-Specific Enrichment:
+
+Device enrichment is selectively enabled for publishers based on their account ID.
+The module identifies publishers through the following fields:
+
+`site.publisher.id` (for web environments).
+`app.publisher.id` (for mobile app environments).
+`dooh.publisher.id` (for digital out-of-home environments).
+
+
+### Building WURFL Module with a licensed WURFL Onsite Java API
+
+In order to compile the WURFL module in the PBS Java server bundle using a licensed WURFL API, you must follow these steps:
+
+1 - Change the URL in the `` tag in the module's `pom.xml` file to the ScientiaMobile Maven repository URL:
+
+`https://maven.scientiamobile.com/repository/wurfl-onsite/`
+
+The repository is private and requires authentication: to set it up please check the paragraph
+"Configuring your Builds to work with ScientiaMobile's Private Maven Repository"
+[on this page](https://docs.scientiamobile.com/documentation/onsite/onsite-java-api).
+
+2 - Change the `artfactId` value in the module's `pom.xml` from `wurfl-mock` to `wurfl`
+
+3 - Update the `wurfl.version` property value to the latest WURFL Onsite Java API version available.
+
+
+When the `pom.xml` references the mock API artifact, the module will compile a demo version that returns sample data,
+allowing basic testing without an WURFL Onsite Java API license.
+
+4 - Build the Prebid Server Java bundle with the WURFL module using the following command:
+
+```bash
+mvn clean package --file extra/pom.xml
+```
+
+### Configuring the WURFL Module
+
+Below is a sample configuration for the WURFL module:
+
+```yaml
+hooks:
+ wurfl-devicedetection:
+ enabled: true
+ host-execution-plan: >
+ {
+ "endpoints": {
+ "/openrtb2/auction": {
+ "stages": {
+ "entrypoint": {
+ "groups": [
+ {
+ "timeout": 10,
+ "hook_sequence": [
+ {
+ "module_code": "wurfl-devicedetection",
+ "hook_impl_code": "wurfl-devicedetection-entrypoint-hook"
+ }
+ ]
+ }
+ ]
+ },
+ "raw_auction_request": {
+ "groups": [
+ {
+ "timeout": 10,
+ "hook_sequence": [
+ {
+ "module_code": "wurfl-devicedetection",
+ "hook_impl_code": "wurfl-devicedetection-raw-auction-request"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ modules:
+ wurfl-devicedetection:
+ file-dir-path:
+ file-snapshot-url: https://data.scientiamobile.com//wurfl.zip
+ cache-size: 200000
+ update-frequency-in-hours: 24
+ allowed-publisher-ids: 1
+ ext-caps: false
+```
+
+### Configuration Options
+
+| Parameter | Requirement | Description |
+|---------------------------------|-------------|---------------------------------------------------------------------------------------------------|
+| **`file-dir-path`** | Mandatory | Path to the directory where the WURFL file is downloaded. Directory must exist and be writable. |
+| **`file-snapshot-url`** | Mandatory | URL of the licensed WURFL snapshot file to be downloaded when Prebid Server Java starts. |
+| **`cache-size`** | Optional | Maximum number of devices stored in the WURFL cache. Defaults to the WURFL cache's standard size. |
+| **`ext-caps`** | Optional | If `true`, the module adds all licensed capabilities to the `device.ext` object. |
+| **`update-frequency-in-hours`** | Optional | Check interval (hours) for downloading updated wurfl file if modified. Defaults to 24 hours |
+| **`allowed-publisher-ids`** | Optional | List of publisher IDs permitted to use the module. Defaults to all publishers. |
+
+
+A valid WURFL license must include all the required capabilities for device enrichment.
+
+### Launching Prebid Server Java with the WURFL Module
+
+After configuring the module and successfully building the Prebid Server bundle, start the server with the following command:
+
+```bash
+java -jar target/prebid-server-bundle.jar --spring.config.additional-location=sample/configs/prebid-config-with-wurfl.yaml
+```
+
+This sample configuration contains the module hook basic configuration.
+
+When the server starts, it downloads the WURFL file from the `wurfl-snapshot-url` and loads it into the module.
+
+Sample request data for testing is available in the module's `sample` directory. Using the `auction` endpoint,
+you can observe WURFL-enriched device data in the response.
+
+### Sample Response
+
+Using the sample request data via `curl` when the module is configured with `ext-caps` set to `false` (or no value)
+
+```bash
+curl http://localhost:8080/openrtb2/auction --data @extra/modules/wurfl-devicedetection/sample/request_data.json
+```
+
+the device object in the response will include WURFL device detection data:
+
+```json
+"device": {
+ "ua": "Mozilla/5.0 (Linux; Android 15; Pixel 9 Pro XL Build/AP3A.241005.015;) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36 EdgA/124.0.2478.64",
+ "devicetype": 1,
+ "make": "Google",
+ "model": "Pixel 9 Pro XL",
+ "os": "Android",
+ "osv": "15",
+ "h": 2992,
+ "w": 1344,
+ "ppi": 481,
+ "pxratio": 2.55,
+ "js": 1,
+ "ext": {
+ "wurfl": {
+ "wurfl_id": "google_pixel_9_pro_xl_ver1_suban150"
+ }
+ }
+}
+```
+
+When `ext_caps` is set to `true`, the response will include all licensed capabilities:
+
+```json
+"device":{
+ "ua":"Mozilla/5.0 (Linux; Android 15; Pixel 9 Pro XL Build/AP3A.241005.015; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36 EdgA/124.0.2478.64",
+ "devicetype":1,
+ "make":"Google",
+ "model":"Pixel 9 Pro XL",
+ "os":"Android",
+ "osv":"15",
+ "h":2992,
+ "w":1344,
+ "ppi":481,
+ "pxratio":2.55,
+ "js":1,
+ "ext":{
+ "wurfl":{
+ "wurfl_id":"google_pixel_9_pro_xl_ver1_suban150",
+ "mobile_browser_version":"",
+ "resolution_height":"2992",
+ "resolution_width":"1344",
+ "is_wireless_device":"true",
+ "is_tablet":"false",
+ "physical_form_factor":"phone_phablet",
+ "ajax_support_javascript":"true",
+ "preferred_markup":"html_web_4_0",
+ "brand_name":"Google",
+ "can_assign_phone_number":"true",
+ "xhtml_support_level":"4",
+ "ux_full_desktop":"false",
+ "device_os":"Android",
+ "physical_screen_width":"71",
+ "is_connected_tv":"false",
+ "is_smarttv":"false",
+ "physical_screen_height":"158",
+ "model_name":"Pixel 9 Pro XL",
+ "is_ott":"false",
+ "density_class":"2.55",
+ "marketing_name":"",
+ "device_os_version":"15.0",
+ "mobile_browser":"Chrome Mobile",
+ "pointing_method":"touchscreen",
+ "is_app_webview":"false",
+ "advertised_app_name":"Edge Browser",
+ "is_smartphone":"true",
+ "is_robot":"false",
+ "advertised_device_os":"Android",
+ "is_largescreen":"true",
+ "is_android":"true",
+ "is_xhtmlmp_preferred":"false",
+ "device_name":"Google Pixel 9 Pro XL",
+ "is_ios":"false",
+ "is_touchscreen":"true",
+ "is_wml_preferred":"false",
+ "is_app":"false",
+ "is_mobile":"true",
+ "is_phone":"true",
+ "is_full_desktop":"false",
+ "is_generic":"false",
+ "advertised_browser":"Edge",
+ "complete_device_name":"Google Pixel 9 Pro XL",
+ "advertised_browser_version":"124.0.2478.64",
+ "is_html_preferred":"true",
+ "is_windows_phone":"false",
+ "pixel_density":"481",
+ "form_factor":"Smartphone",
+ "advertised_device_os_version":"15"
+ }
+ }
+}
+```
+
+## Maintainer
+
+prebid@scientiamobile.com
diff --git a/extra/modules/wurfl-devicedetection/pom.xml b/extra/modules/wurfl-devicedetection/pom.xml
new file mode 100644
index 00000000000..9438252841a
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/pom.xml
@@ -0,0 +1,34 @@
+
+
+ 4.0.0
+
+
+ org.prebid.server.hooks.modules
+ all-modules
+ 3.30.0-SNAPSHOT
+
+
+ wurfl-devicedetection
+
+ wurfl-devicedetection
+ WURFL device detection and data enrichment module
+
+
+ 1.0.0.0
+
+
+
+
+ com.scientiamobile.wurfl
+ https://maven.scientiamobile.com/repository/wurfl-onsite-tools/
+
+
+
+
+
+ com.scientiamobile.wurfl
+ wurfl-mock
+ ${wurfl.version}
+
+
+
diff --git a/extra/modules/wurfl-devicedetection/sample/request_data.json b/extra/modules/wurfl-devicedetection/sample/request_data.json
new file mode 100644
index 00000000000..42691bbc74d
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/sample/request_data.json
@@ -0,0 +1,119 @@
+{
+ "imp": [
+ {
+ "ext": {
+ "data": {
+ "adserver": {
+ "name": "gam",
+ "adslot": "test"
+ },
+ "pbadslot": "test",
+ "gpid": "test"
+ },
+ "gpid": "test",
+ "prebid": {
+ "bidder": {
+ "appnexus": {
+ "placement_id": 1,
+ "use_pmt_rule": false
+ },
+ "0test": {
+ "placement_id": 1
+ }
+ },
+ "adunitcode": "25e8ad9f-13a4-4404-ba74-f9eebff0e86c",
+ "floors": {
+ "floorMin": 0.01
+ }
+ }
+ },
+ "id": "2529eeea-813e-4da6-838f-f91c28d64867",
+ "banner": {
+ "topframe": 1,
+ "format": [
+ {
+ "w": 728,
+ "h": 90
+ }
+ ],
+ "pos": 1
+ },
+ "bidfloor": 0.01,
+ "bidfloorcur": "USD"
+ }
+ ],
+ "site": {
+ "domain": "test.com",
+ "publisher": {
+ "domain": "test.com",
+ "id": "1"
+ },
+ "page": "https://www.test.com/"
+ },
+ "device": {
+ "ua": "Mozilla/5.0 (Linux; Android 15; Pixel 9 Pro XL Build/AP3A.241005.015; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36 EdgA/124.0.2478.64"
+ },
+ "id": "fc4670ce-4985-4316-a245-b43c885dc37a",
+ "test": 1,
+ "cur": [
+ "USD"
+ ],
+ "source": {
+ "ext": {
+ "schain": {
+ "ver": "1.0",
+ "complete": 1,
+ "nodes": [
+ {
+ "asi": "example.com",
+ "sid": "1234",
+ "hp": 1
+ }
+ ]
+ }
+ }
+ },
+ "ext": {
+ "prebid": {
+ "cache": {
+ "bids": {
+ "returnCreative": true
+ },
+ "vastxml": {
+ "returnCreative": true
+ }
+ },
+ "auctiontimestamp": 1799310801804,
+ "targeting": {
+ "includewinners": true,
+ "includebidderkeys": false
+ },
+ "schains": [
+ {
+ "bidders": [
+ "appnexus"
+ ],
+ "schain": {
+ "ver": "1.0",
+ "complete": 1,
+ "nodes": [
+ {
+ "asi": "example.com",
+ "sid": "1234",
+ "hp": 1
+ }
+ ]
+ }
+ }
+ ],
+ "floors": {
+ "enabled": false,
+ "floorMin": 0.01,
+ "floorMinCur": "USD"
+ },
+ "createtids": false
+ }
+ },
+ "user": {},
+ "tmax": 2000
+}
diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfigProperties.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfigProperties.java
new file mode 100644
index 00000000000..116095f5a08
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfigProperties.java
@@ -0,0 +1,32 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config;
+
+import lombok.Data;
+
+import java.util.Collections;
+import java.util.Set;
+
+@Data
+public class WURFLDeviceDetectionConfigProperties {
+
+ private static final int DEFAULT_UPDATE_TIMEOUT = 5000;
+ private static final long DEFAULT_RETRY_INTERVAL = 200L;
+ private static final int DEFAULT_UPDATE_RETRIES = 3;
+
+ int cacheSize;
+
+ String fileDirPath;
+
+ String fileSnapshotUrl;
+
+ boolean extCaps;
+
+ int updateFrequencyInHours;
+
+ Set allowedPublisherIds = Collections.emptySet();
+
+ int updateConnTimeoutMs = DEFAULT_UPDATE_TIMEOUT;
+
+ int updateRetries = DEFAULT_UPDATE_RETRIES;
+
+ long retryIntervalMs = DEFAULT_RETRY_INTERVAL;
+}
diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfiguration.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfiguration.java
new file mode 100644
index 00000000000..809dad7b19a
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfiguration.java
@@ -0,0 +1,100 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config;
+
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.WURFLEngineUtils;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1.WURFLDeviceDetectionEntrypointHook;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1.WURFLDeviceDetectionModule;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1.WURFLDeviceDetectionRawAuctionRequestHook;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1.WURFLService;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import io.vertx.core.Vertx;
+import org.prebid.server.execution.file.syncer.FileSyncer;
+import org.prebid.server.spring.config.model.FileSyncerProperties;
+import org.prebid.server.spring.config.model.HttpClientProperties;
+import org.prebid.server.execution.file.FileUtil;
+import org.prebid.server.json.JacksonMapper;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.nio.file.Path;
+import java.util.List;
+
+@ConditionalOnProperty(prefix = "hooks." + WURFLDeviceDetectionModule.CODE, name = "enabled", havingValue = "true")
+@Configuration
+public class WURFLDeviceDetectionConfiguration {
+
+ private static final Long HOUR_IN_MILLIS = 3600000L;
+ private static final int DEFAULT_UPDATE_FREQ_IN_HOURS = 24;
+
+ @Bean
+ @ConfigurationProperties(prefix = "hooks.modules." + WURFLDeviceDetectionModule.CODE)
+ WURFLDeviceDetectionConfigProperties configProperties() {
+ return new WURFLDeviceDetectionConfigProperties();
+ }
+
+ @Bean
+ public WURFLDeviceDetectionModule wurflDeviceDetectionModule(WURFLDeviceDetectionConfigProperties configProperties,
+ JacksonMapper mapper,
+ Vertx vertx) {
+
+ final WURFLService wurflService = new WURFLService(null, configProperties);
+ final FileSyncer fileSyncer = createFileSyncer(configProperties, wurflService, vertx);
+ fileSyncer.sync();
+
+ return new WURFLDeviceDetectionModule(List.of(
+ new WURFLDeviceDetectionEntrypointHook(),
+ new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper)));
+ }
+
+ private FileSyncer createFileSyncer(WURFLDeviceDetectionConfigProperties configProperties,
+ WURFLService wurflService,
+ Vertx vertx) {
+
+ final FileSyncerProperties fileSyncerProperties = createFileSyncerProperties(configProperties);
+ return FileUtil.fileSyncerFor(wurflService, fileSyncerProperties, vertx);
+ }
+
+ private FileSyncerProperties createFileSyncerProperties(WURFLDeviceDetectionConfigProperties configProperties) {
+ final String downloadPath = createDownloadPath(configProperties);
+ final String tempPath = createTempPath(configProperties);
+ final HttpClientProperties httpProperties = createHttpProperties(configProperties);
+
+ final FileSyncerProperties fileSyncerProperties = new FileSyncerProperties();
+ fileSyncerProperties.setCheckSize(true);
+ fileSyncerProperties.setDownloadUrl(configProperties.getFileSnapshotUrl());
+ fileSyncerProperties.setSaveFilepath(downloadPath);
+ fileSyncerProperties.setTmpFilepath(tempPath);
+ fileSyncerProperties.setTimeoutMs((long) configProperties.getUpdateConnTimeoutMs());
+ fileSyncerProperties.setRetryCount(configProperties.getUpdateRetries());
+ fileSyncerProperties.setRetryIntervalMs(configProperties.getRetryIntervalMs());
+ fileSyncerProperties.setHttpClient(httpProperties);
+ int updateFreqInHours = configProperties.getUpdateFrequencyInHours();
+ if (updateFreqInHours <= 0) {
+ updateFreqInHours = DEFAULT_UPDATE_FREQ_IN_HOURS;
+ }
+ final long syncIntervalMillis = updateFreqInHours * HOUR_IN_MILLIS;
+ fileSyncerProperties.setUpdateIntervalMs(syncIntervalMillis);
+
+ return fileSyncerProperties;
+ }
+
+ private String createTempPath(WURFLDeviceDetectionConfigProperties configProperties) {
+ final String basePath = configProperties.getFileDirPath();
+ final String fileName = "tmp_"
+ + WURFLEngineUtils.extractWURFLFileName(configProperties.getFileSnapshotUrl());
+ return Path.of(basePath, fileName).toString();
+ }
+
+ private String createDownloadPath(WURFLDeviceDetectionConfigProperties configProperties) {
+ final String basePath = configProperties.getFileDirPath();
+ final String fileName = WURFLEngineUtils.extractWURFLFileName(configProperties.getFileSnapshotUrl());
+ return Path.of(basePath, fileName).toString();
+ }
+
+ private HttpClientProperties createHttpProperties(WURFLDeviceDetectionConfigProperties configProperties) {
+ final HttpClientProperties httpProperties = new HttpClientProperties();
+ httpProperties.setConnectTimeoutMs(configProperties.getUpdateConnTimeoutMs());
+ httpProperties.setMaxRedirects(1);
+ return httpProperties;
+ }
+}
diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/exc/WURFLDeviceDetectionException.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/exc/WURFLDeviceDetectionException.java
new file mode 100644
index 00000000000..97f46c69ee6
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/exc/WURFLDeviceDetectionException.java
@@ -0,0 +1,8 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.exc;
+
+public class WURFLDeviceDetectionException extends RuntimeException {
+
+ public WURFLDeviceDetectionException(String message) {
+ super(message);
+ }
+}
diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContext.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContext.java
new file mode 100644
index 00000000000..ee72b492bda
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContext.java
@@ -0,0 +1,26 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model;
+
+import lombok.Value;
+import org.prebid.server.model.CaseInsensitiveMultiMap;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+@Value
+public class AuctionRequestHeadersContext {
+
+ Map headers;
+
+ public static AuctionRequestHeadersContext from(CaseInsensitiveMultiMap headers) {
+ if (headers == null) {
+ return new AuctionRequestHeadersContext(Collections.emptyMap());
+ }
+
+ final Map headersMap = new HashMap<>();
+ for (String headerName : headers.names()) {
+ headersMap.put(headerName, headers.getAll(headerName).getFirst());
+ }
+ return new AuctionRequestHeadersContext(Collections.unmodifiableMap(headersMap));
+ }
+}
diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineUtils.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineUtils.java
new file mode 100644
index 00000000000..a1593e89131
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineUtils.java
@@ -0,0 +1,85 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model;
+
+import com.scientiamobile.wurfl.core.GeneralWURFLEngine;
+import com.scientiamobile.wurfl.core.WURFLEngine;
+import com.scientiamobile.wurfl.core.cache.LRUMapCacheProvider;
+import com.scientiamobile.wurfl.core.cache.NullCacheProvider;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.exc.WURFLDeviceDetectionException;
+
+import java.net.URI;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public class WURFLEngineUtils {
+
+ private WURFLEngineUtils() {
+ }
+
+ private static final Set REQUIRED_STATIC_CAPS = Set.of(
+ "ajax_support_javascript",
+ "brand_name",
+ "density_class",
+ "is_connected_tv",
+ "is_ott",
+ "is_tablet",
+ "model_name",
+ "resolution_height",
+ "resolution_width",
+ "physical_form_factor");
+
+ public static WURFLEngine initializeEngine(WURFLDeviceDetectionConfigProperties configProperties,
+ String wurflInFilePath) {
+
+ final String wurflFilePath = wurflInFilePath != null
+ ? wurflInFilePath
+ : wurflFilePathFromConfig(configProperties);
+
+ final WURFLEngine engine = new GeneralWURFLEngine(wurflFilePath);
+ verifyStaticCapabilitiesDefinition(engine);
+
+ final int cacheSize = configProperties.getCacheSize();
+ engine.setCacheProvider(cacheSize > 0
+ ? new LRUMapCacheProvider(configProperties.getCacheSize())
+ : new NullCacheProvider());
+
+ return engine;
+ }
+
+ private static String wurflFilePathFromConfig(WURFLDeviceDetectionConfigProperties configProperties) {
+ final String wurflFileName = extractWURFLFileName(configProperties.getFileSnapshotUrl());
+ return Paths.get(configProperties.getFileDirPath(), wurflFileName).toAbsolutePath().toString();
+ }
+
+ public static String extractWURFLFileName(String wurflSnapshotUrl) {
+ try {
+ final URI uri = new URI(wurflSnapshotUrl);
+ final String path = uri.getPath();
+ return path.substring(path.lastIndexOf('/') + 1);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Invalid WURFL snapshot URL: " + wurflSnapshotUrl, e);
+ }
+ }
+
+ private static void verifyStaticCapabilitiesDefinition(WURFLEngine engine) {
+ final List unsupportedStaticCaps = new ArrayList<>();
+ for (String requiredCapName : REQUIRED_STATIC_CAPS) {
+ if (!engine.getAllCapabilities().contains(requiredCapName)) {
+ unsupportedStaticCaps.add(requiredCapName);
+ }
+ }
+
+ if (!unsupportedStaticCaps.isEmpty()) {
+ Collections.sort(unsupportedStaticCaps);
+ final String failedCheckMessage = """
+ Static capabilities %s needed for device enrichment are not defined in WURFL.
+ Please make sure that your license has the needed capabilities or upgrade it.
+ """.formatted(String.join(",", unsupportedStaticCaps));
+
+ throw new WURFLDeviceDetectionException(failedCheckMessage);
+ }
+ }
+}
diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolver.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolver.java
new file mode 100644
index 00000000000..a156b3a9b33
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolver.java
@@ -0,0 +1,104 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.resolver;
+
+import com.iab.openrtb.request.BrandVersion;
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.UserAgent;
+import org.prebid.server.util.HttpUtil;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class HeadersResolver {
+
+ private HeadersResolver() {
+ }
+
+ public static Map resolve(Device device, Map headers) {
+ if (device == null && headers == null) {
+ return Collections.emptyMap();
+ }
+
+ final Map resolvedHeaders = resolveFromDevice(device);
+ return MapUtils.isNotEmpty(resolvedHeaders)
+ ? resolvedHeaders
+ : headers;
+ }
+
+ private static Map resolveFromDevice(Device device) {
+ if (device == null) {
+ return Collections.emptyMap();
+ }
+
+ final Map resolvedHeaders = new HashMap<>();
+ if (device.getUa() != null) {
+ resolvedHeaders.put(HttpUtil.USER_AGENT_HEADER.toString(), device.getUa());
+ }
+ resolvedHeaders.putAll(resolveFromSua(device.getSua()));
+
+ return resolvedHeaders;
+ }
+
+ private static Map resolveFromSua(UserAgent sua) {
+ if (sua == null) {
+ return Collections.emptyMap();
+ }
+
+ final List brands = sua.getBrowsers();
+ if (CollectionUtils.isEmpty(brands)) {
+ return Collections.emptyMap();
+ }
+
+ final Map headers = new HashMap<>();
+ final String brandList = brandListAsString(brands);
+ headers.put(HttpUtil.SEC_CH_UA.toString(), brandList);
+ headers.put(HttpUtil.SEC_CH_UA_FULL_VERSION_LIST.toString(), brandList);
+
+ final BrandVersion platform = sua.getPlatform();
+ if (platform != null) {
+ headers.put(HttpUtil.SEC_CH_UA_PLATFORM.toString(), platform.getBrand());
+ headers.put(HttpUtil.SEC_CH_UA_PLATFORM_VERSION.toString(), versionFromTokens(platform.getVersion()));
+ }
+
+ final String model = sua.getModel();
+ if (StringUtils.isNotEmpty(model)) {
+ headers.put(HttpUtil.SEC_CH_UA_MODEL.toString(), model);
+ }
+
+ final String arch = sua.getArchitecture();
+ if (StringUtils.isNotEmpty(arch)) {
+ headers.put(HttpUtil.SEC_CH_UA_ARCH.toString(), arch);
+ }
+
+ final Integer mobile = sua.getMobile();
+ if (mobile != null) {
+ headers.put(HttpUtil.SEC_CH_UA_MOBILE.toString(), "?" + mobile);
+ }
+
+ return headers;
+ }
+
+ private static String brandListAsString(List versions) {
+ return versions.stream()
+ .filter(brandVersion -> brandVersion.getBrand() != null)
+ .map(brandVersion -> "\"%s\";v=\"%s\"".formatted(
+ brandVersion.getBrand(),
+ versionFromTokens(brandVersion.getVersion())))
+ .collect(Collectors.joining(", "));
+ }
+
+ private static String versionFromTokens(List tokens) {
+ if (CollectionUtils.isEmpty(tokens)) {
+ return StringUtils.EMPTY;
+ }
+
+ return tokens.stream()
+ .filter(StringUtils::isNotEmpty)
+ .collect(Collectors.joining("."));
+ }
+}
diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdater.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdater.java
new file mode 100644
index 00000000000..b9b624b4f5b
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdater.java
@@ -0,0 +1,263 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1;
+
+import com.iab.openrtb.request.BidRequest;
+import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl;
+import com.iab.openrtb.request.Device;
+import com.scientiamobile.wurfl.core.exc.CapabilityNotDefinedException;
+import com.scientiamobile.wurfl.core.exc.VirtualCapabilityNotDefinedException;
+import org.prebid.server.log.Logger;
+import org.prebid.server.log.LoggerFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.proto.openrtb.ext.request.ExtDevice;
+import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
+import org.prebid.server.hooks.v1.PayloadUpdate;
+
+import java.math.BigDecimal;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Supplier;
+
+public class OrtbDeviceUpdater implements PayloadUpdate {
+
+ private static final Logger logger = LoggerFactory.getLogger(OrtbDeviceUpdater.class);
+
+ private static final String WURFL_PROPERTY = "wurfl";
+
+ private final com.scientiamobile.wurfl.core.Device wurflDevice;
+ private final Set staticCaps;
+ private final Set virtualCaps;
+ private final boolean addExtCaps;
+ private final JacksonMapper mapper;
+
+ public OrtbDeviceUpdater(com.scientiamobile.wurfl.core.Device wurflDevice,
+ Set staticCaps,
+ Set virtualCaps,
+ boolean addExtCaps,
+ JacksonMapper mapper) {
+
+ this.wurflDevice = Objects.requireNonNull(wurflDevice);
+ this.staticCaps = Objects.requireNonNull(staticCaps);
+ this.virtualCaps = Objects.requireNonNull(virtualCaps);
+ this.addExtCaps = addExtCaps;
+ this.mapper = Objects.requireNonNull(mapper);
+ }
+
+ @Override
+ public AuctionRequestPayload apply(AuctionRequestPayload auctionRequestPayload) {
+ final BidRequest bidRequest = auctionRequestPayload.bidRequest();
+ return AuctionRequestPayloadImpl.of(bidRequest.toBuilder()
+ .device(update(bidRequest.getDevice()))
+ .build());
+ }
+
+ private Device update(Device ortbDevice) {
+ final String make = tryUpdateField(ortbDevice.getMake(), this::getWurflMake);
+ final String model = tryUpdateField(ortbDevice.getModel(), this::getWurflModel);
+ final Integer deviceType = tryUpdateField(
+ Optional.ofNullable(ortbDevice.getDevicetype())
+ .filter(it -> it > 0)
+ .orElse(null),
+ this::getWurflDeviceType);
+ final String os = tryUpdateField(ortbDevice.getOs(), this::getWurflOs);
+ final String osv = tryUpdateField(ortbDevice.getOsv(), this::getWurflOsv);
+ final Integer h = tryUpdateField(ortbDevice.getH(), this::getWurflH);
+ final Integer w = tryUpdateField(ortbDevice.getW(), this::getWurflW);
+ final Integer ppi = tryUpdateField(ortbDevice.getPpi(), this::getWurflPpi);
+ final BigDecimal pxratio = tryUpdateField(ortbDevice.getPxratio(), this::getWurflPxRatio);
+ final Integer js = tryUpdateField(ortbDevice.getJs(), this::getWurflJs);
+
+ return ortbDevice.toBuilder()
+ .make(make)
+ .model(model)
+ .devicetype(deviceType)
+ .os(os)
+ .osv(osv)
+ .h(h)
+ .w(w)
+ .ppi(ppi)
+ .pxratio(pxratio)
+ .js(js)
+ .ext(updateExt(ortbDevice.getExt()))
+ .build();
+ }
+
+ private static T tryUpdateField(T fromOrtbDevice, Supplier fromWurflDeviceSupplier) {
+ if (fromOrtbDevice != null) {
+ return fromOrtbDevice;
+ }
+
+ final T fromWurflDevice = fromWurflDeviceSupplier.get();
+ return fromWurflDevice != null
+ ? fromWurflDevice
+ : fromOrtbDevice;
+ }
+
+ private String getWurflMake() {
+ return wurflDevice.getCapability("brand_name");
+ }
+
+ private String getWurflModel() {
+ return wurflDevice.getCapability("model_name");
+ }
+
+ private Integer getWurflDeviceType() {
+ try {
+ if (wurflDevice.getVirtualCapabilityAsBool("is_mobile")) {
+ // if at least one of these capabilities is not defined, the mobile device type is undefined
+ final boolean isPhone = wurflDevice.getVirtualCapabilityAsBool("is_phone");
+ final boolean isTablet = wurflDevice.getCapabilityAsBool("is_tablet");
+ return isPhone || isTablet ? 1 : 6;
+ }
+
+ if (wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")) {
+ return 2;
+ }
+
+ if (wurflDevice.getCapabilityAsBool("is_connected_tv")) {
+ return 3;
+ }
+
+ if (wurflDevice.getCapabilityAsBool("is_phone")) {
+ return 4;
+ }
+
+ if (wurflDevice.getCapabilityAsBool("is_tablet")) {
+ return 5;
+ }
+
+ if (wurflDevice.getCapabilityAsBool("is_ott")) {
+ return 7;
+ }
+
+ final String physicalFormFactor = wurflDevice.getCapability("physical_form_factor");
+ if (physicalFormFactor != null && physicalFormFactor.equals("out_of_home_device")) {
+ return 8;
+ }
+ } catch (CapabilityNotDefinedException | VirtualCapabilityNotDefinedException | NumberFormatException e) {
+ logger.warn("Failed to determine device type from WURFL device capabilities", e);
+ }
+ return null;
+ }
+
+ private String getWurflOs() {
+ try {
+ return wurflDevice.getVirtualCapability("advertised_device_os");
+ } catch (VirtualCapabilityNotDefinedException e) {
+ logger.warn("Failed to evaluate advertised device OS", e);
+ return null;
+ }
+ }
+
+ private String getWurflOsv() {
+ try {
+ return wurflDevice.getVirtualCapability("advertised_device_os_version");
+ } catch (VirtualCapabilityNotDefinedException e) {
+ logger.warn("Failed to evaluate advertised device OS version", e);
+ }
+ return null;
+ }
+
+ private Integer getWurflH() {
+ try {
+ return wurflDevice.getCapabilityAsInt("resolution_height");
+ } catch (NumberFormatException e) {
+ logger.warn("Failed to get resolution height from WURFL device capabilities", e);
+ return null;
+ }
+ }
+
+ private Integer getWurflW() {
+ try {
+ return wurflDevice.getCapabilityAsInt("resolution_width");
+ } catch (NumberFormatException e) {
+ logger.warn("Failed to get resolution width from WURFL device capabilities", e);
+ return null;
+ }
+ }
+
+ private Integer getWurflPpi() {
+ try {
+ return wurflDevice.getVirtualCapabilityAsInt("pixel_density");
+ } catch (VirtualCapabilityNotDefinedException e) {
+ logger.warn("Failed to get pixel density from WURFL device capabilities", e);
+ return null;
+ }
+ }
+
+ private BigDecimal getWurflPxRatio() {
+ try {
+ final String densityAsString = wurflDevice.getCapability("density_class");
+ return densityAsString != null
+ ? new BigDecimal(densityAsString)
+ : null;
+ } catch (CapabilityNotDefinedException | NumberFormatException e) {
+ logger.warn("Failed to get pixel ratio from WURFL device capabilities", e);
+ return null;
+ }
+ }
+
+ private Integer getWurflJs() {
+ try {
+ return wurflDevice.getCapabilityAsBool("ajax_support_javascript") ? 1 : 0;
+ } catch (CapabilityNotDefinedException | NumberFormatException e) {
+ logger.warn("Failed to get JS support from WURFL device capabilities", e);
+ return null;
+ }
+ }
+
+ private ExtDevice updateExt(ExtDevice ortbExtDevice) {
+ if (ortbExtDevice != null && ortbExtDevice.containsProperty(WURFL_PROPERTY)) {
+ return ortbExtDevice;
+ }
+
+ final ExtDevice updatedExt = Optional.ofNullable(ortbExtDevice)
+ .map(this::copyExtDevice)
+ .orElse(ExtDevice.empty());
+
+ updatedExt.addProperty(WURFL_PROPERTY, createWurflObject());
+
+ return updatedExt;
+ }
+
+ private ExtDevice copyExtDevice(ExtDevice original) {
+ final ExtDevice copy = ExtDevice.of(original.getAtts(), original.getPrebid());
+ mapper.fillExtension(copy, original);
+ return copy;
+ }
+
+ private ObjectNode createWurflObject() {
+ final ObjectNode wurfl = mapper.mapper().createObjectNode();
+
+ wurfl.put("wurfl_id", wurflDevice.getId());
+
+ if (!addExtCaps) {
+ return wurfl;
+ }
+
+ for (String capability : staticCaps) {
+ try {
+ final String value = wurflDevice.getCapability(capability);
+ if (value != null) {
+ wurfl.put(capability, value);
+ }
+ } catch (Exception e) {
+ logger.warn("Error getting capability for {}: {}", capability, e.getMessage());
+ }
+ }
+
+ for (String virtualCapability : virtualCaps) {
+ try {
+ final String value = wurflDevice.getVirtualCapability(virtualCapability);
+ if (value != null) {
+ wurfl.put(virtualCapability, value);
+ }
+ } catch (Exception e) {
+ logger.warn("Could not fetch virtual capability {}", virtualCapability);
+ }
+ }
+
+ return wurfl;
+ }
+}
diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHook.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHook.java
new file mode 100644
index 00000000000..102d4a164b2
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHook.java
@@ -0,0 +1,36 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1;
+
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.AuctionRequestHeadersContext;
+import org.prebid.server.hooks.v1.InvocationAction;
+import org.prebid.server.hooks.v1.InvocationContext;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.v1.InvocationStatus;
+import org.prebid.server.hooks.v1.entrypoint.EntrypointHook;
+import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import io.vertx.core.Future;
+
+public class WURFLDeviceDetectionEntrypointHook implements EntrypointHook {
+
+ private static final String CODE = "wurfl-devicedetection-entrypoint-hook";
+
+ @Override
+ public Future> call(EntrypointPayload entrypointPayload,
+ InvocationContext invocationContext) {
+
+ final AuctionRequestHeadersContext bidRequestHeadersContext = AuctionRequestHeadersContext.from(
+ entrypointPayload.headers());
+
+ return Future.succeededFuture(
+ InvocationResultImpl.builder()
+ .status(InvocationStatus.success)
+ .action(InvocationAction.no_action)
+ .moduleContext(bidRequestHeadersContext)
+ .build());
+ }
+
+ @Override
+ public String code() {
+ return CODE;
+ }
+}
diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModule.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModule.java
new file mode 100644
index 00000000000..f929a33ce70
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModule.java
@@ -0,0 +1,29 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1;
+
+import org.prebid.server.hooks.v1.Module;
+import org.prebid.server.hooks.v1.Hook;
+import org.prebid.server.hooks.v1.InvocationContext;
+
+import java.util.Collection;
+import java.util.List;
+
+public class WURFLDeviceDetectionModule implements Module {
+
+ public static final String CODE = "wurfl-devicedetection";
+
+ private final List extends Hook, ? extends InvocationContext>> hooks;
+
+ public WURFLDeviceDetectionModule(List extends Hook, ? extends InvocationContext>> hooks) {
+ this.hooks = hooks;
+ }
+
+ @Override
+ public String code() {
+ return CODE;
+ }
+
+ @Override
+ public Collection extends Hook, ? extends InvocationContext>> hooks() {
+ return this.hooks;
+ }
+}
diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHook.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHook.java
new file mode 100644
index 00000000000..337343f15d4
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHook.java
@@ -0,0 +1,114 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1;
+
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.BidRequest;
+import org.prebid.server.log.Logger;
+import org.prebid.server.log.LoggerFactory;
+import org.prebid.server.json.JacksonMapper;
+import org.apache.commons.collections4.CollectionUtils;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.AuctionRequestHeadersContext;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.resolver.HeadersResolver;
+import org.prebid.server.hooks.v1.InvocationAction;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import org.prebid.server.hooks.v1.InvocationStatus;
+import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
+import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
+import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook;
+import org.prebid.server.auction.model.AuctionContext;
+import org.prebid.server.settings.model.Account;
+import io.vertx.core.Future;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+public class WURFLDeviceDetectionRawAuctionRequestHook implements RawAuctionRequestHook {
+
+ private static final Logger logger = LoggerFactory.getLogger(WURFLDeviceDetectionRawAuctionRequestHook.class);
+
+ public static final String CODE = "wurfl-devicedetection-raw-auction-request";
+
+ private final WURFLService wurflService;
+ private final Set allowedPublisherIDs;
+ private final boolean addExtCaps;
+ private final JacksonMapper mapper;
+
+ public WURFLDeviceDetectionRawAuctionRequestHook(WURFLService wurflService,
+ WURFLDeviceDetectionConfigProperties configProperties,
+ JacksonMapper mapper) {
+
+ this.wurflService = Objects.requireNonNull(wurflService);
+ this.addExtCaps = Objects.requireNonNull(configProperties).isExtCaps();
+ this.allowedPublisherIDs = Objects.requireNonNull(configProperties.getAllowedPublisherIds());
+ this.mapper = Objects.requireNonNull(mapper);
+ }
+
+ @Override
+ public Future> call(AuctionRequestPayload auctionRequestPayload,
+ AuctionInvocationContext invocationContext) {
+
+ if (!shouldEnrichDevice(invocationContext)) {
+ return noActionResult();
+ }
+
+ final BidRequest bidRequest = auctionRequestPayload.bidRequest();
+ final Device device = bidRequest.getDevice();
+ if (device == null) {
+ logger.warn("Device is null");
+ return noActionResult();
+ }
+
+ final Map requestHeaders =
+ invocationContext.moduleContext() instanceof AuctionRequestHeadersContext moduleContext
+ ? moduleContext.getHeaders()
+ : null;
+
+ final Map headers = HeadersResolver.resolve(device, requestHeaders);
+ final Optional wurflDevice = wurflService.lookupDevice(headers);
+ if (wurflDevice.isEmpty()) {
+ logger.info("No WURFL device found, returning original bid request");
+ return noActionResult();
+ }
+
+ return Future.succeededFuture(
+ InvocationResultImpl.builder()
+ .status(InvocationStatus.success)
+ .action(InvocationAction.update)
+ .payloadUpdate(new OrtbDeviceUpdater(
+ wurflDevice.get(),
+ wurflService.getAllCapabilities(),
+ wurflService.getAllVirtualCapabilities(),
+ addExtCaps,
+ mapper))
+ .build());
+ }
+
+ private boolean shouldEnrichDevice(AuctionInvocationContext invocationContext) {
+ return CollectionUtils.isEmpty(allowedPublisherIDs) || isAccountValid(invocationContext.auctionContext());
+ }
+
+ private boolean isAccountValid(AuctionContext auctionContext) {
+ return Optional.ofNullable(auctionContext.getAccount())
+ .map(Account::getId)
+ .filter(StringUtils::isNotBlank)
+ .filter(allowedPublisherIDs::contains)
+ .isPresent();
+ }
+
+ private static Future> noActionResult() {
+ return Future.succeededFuture(
+ InvocationResultImpl.builder()
+ .status(InvocationStatus.success)
+ .action(InvocationAction.no_action)
+ .build());
+ }
+
+ @Override
+ public String code() {
+ return CODE;
+ }
+}
diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLService.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLService.java
new file mode 100644
index 00000000000..ccfc848399c
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLService.java
@@ -0,0 +1,66 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1;
+
+import com.scientiamobile.wurfl.core.Device;
+import com.scientiamobile.wurfl.core.WURFLEngine;
+import io.vertx.core.Future;
+import org.prebid.server.log.Logger;
+import org.prebid.server.log.LoggerFactory;
+import org.prebid.server.execution.file.FileProcessor;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.WURFLEngineUtils;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class WURFLService implements FileProcessor {
+
+ private static final Logger logger = LoggerFactory.getLogger(WURFLService.class);
+
+ private final AtomicReference wurflEngine;
+ private final WURFLDeviceDetectionConfigProperties configProperties;
+
+ public WURFLService(WURFLEngine wurflEngine, WURFLDeviceDetectionConfigProperties configProperties) {
+ this.wurflEngine = new AtomicReference<>(wurflEngine);
+ this.configProperties = Objects.requireNonNull(configProperties);
+ }
+
+ public Future> setDataPath(String dataFilePath) {
+ try {
+ final WURFLEngine engine = createEngine(dataFilePath);
+ this.wurflEngine.set(engine);
+ } catch (Exception e) {
+ return Future.failedFuture(e);
+ }
+
+ return Future.succeededFuture();
+ }
+
+ protected WURFLEngine createEngine(String dataFilePath) {
+ final WURFLEngine wurflEngine = WURFLEngineUtils.initializeEngine(configProperties, dataFilePath);
+ wurflEngine.load();
+ logger.info("WURFL Engine initialized");
+ return wurflEngine;
+ }
+
+ public Optional lookupDevice(Map headers) {
+ return Optional.ofNullable(wurflEngine.get())
+ .map(engine -> engine.getDeviceForRequest(headers));
+ }
+
+ public Set getAllCapabilities() {
+ return Optional.ofNullable(wurflEngine.get())
+ .map(WURFLEngine::getAllCapabilities)
+ .orElse(Collections.emptySet());
+ }
+
+ public Set getAllVirtualCapabilities() {
+ return Optional.ofNullable(wurflEngine.get())
+ .map(WURFLEngine::getAllVirtualCapabilities)
+ .orElse(Collections.emptySet());
+ }
+}
+
diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContextTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContextTest.java
new file mode 100644
index 00000000000..c69a4ab503b
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContextTest.java
@@ -0,0 +1,65 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model;
+
+import org.junit.jupiter.api.Test;
+import org.prebid.server.model.CaseInsensitiveMultiMap;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AuctionRequestHeadersContextTest {
+
+ @Test
+ public void fromShouldHandleNullHeaders() {
+ // when
+ final AuctionRequestHeadersContext result = AuctionRequestHeadersContext.from(null);
+
+ // then
+ assertThat(result.getHeaders()).isEmpty();
+ }
+
+ @Test
+ public void fromShouldConvertCaseInsensitiveMultiMapToHeaders() {
+ // given
+ final CaseInsensitiveMultiMap multiMap = CaseInsensitiveMultiMap.builder()
+ .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test")
+ .add("Header2", "value2")
+ .build();
+
+ // when
+ final AuctionRequestHeadersContext target = AuctionRequestHeadersContext.from(multiMap);
+
+ // then
+ assertThat(target.getHeaders())
+ .hasSize(2)
+ .containsEntry("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test")
+ .containsEntry("Header2", "value2");
+ }
+
+ @Test
+ public void fromShouldTakeFirstValueForDuplicateHeaders() {
+ // given
+ final CaseInsensitiveMultiMap multiMap = CaseInsensitiveMultiMap.builder()
+ .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test")
+ .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test2")
+ .build();
+
+ // when
+ final AuctionRequestHeadersContext target = AuctionRequestHeadersContext.from(multiMap);
+
+ // then
+ assertThat(target.getHeaders())
+ .hasSize(1)
+ .containsEntry("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test");
+ }
+
+ @Test
+ public void fromShouldHandleEmptyMultiMap() {
+ // given
+ final CaseInsensitiveMultiMap emptyMultiMap = CaseInsensitiveMultiMap.empty();
+
+ // when
+ final AuctionRequestHeadersContext target = AuctionRequestHeadersContext.from(emptyMultiMap);
+
+ // then
+ assertThat(target.getHeaders()).isEmpty();
+ }
+}
diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineUtilsTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineUtilsTest.java
new file mode 100644
index 00000000000..a383f875768
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineUtilsTest.java
@@ -0,0 +1,82 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties;
+
+import static org.mockito.Mock.Strictness.LENIENT;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@ExtendWith(MockitoExtension.class)
+public class WURFLEngineUtilsTest {
+
+ @Mock(strictness = LENIENT)
+ private WURFLDeviceDetectionConfigProperties configProperties;
+
+ @Test
+ public void extractWURFLFileNameShouldReturnCorrectFileName() {
+ // given
+ final String url = "https://data.examplehost.com/snapshot/wurfl-latest.zip";
+
+ // when
+ final String result = WURFLEngineUtils.extractWURFLFileName(url);
+
+ // then
+ assertThat(result).isEqualTo("wurfl-latest.zip");
+ }
+
+ @Test
+ public void extractWURFLFileNameShouldHandleSimpleFileName() {
+ // given
+ final String url = "http://example.com/wurfl.zip";
+
+ // when
+ final String result = WURFLEngineUtils.extractWURFLFileName(url);
+
+ // then
+ assertThat(result).isEqualTo("wurfl.zip");
+ }
+
+ @Test
+ public void extractWURFLFileNameShouldHandleComplexPath() {
+ // given
+ final String url = "https://examplehost.com/path/to/files/wurfl-snapshot.zip";
+
+ // when
+ final String result = WURFLEngineUtils.extractWURFLFileName(url);
+
+ // then
+ assertThat(result).isEqualTo("wurfl-snapshot.zip");
+ }
+
+ @Test
+ public void extractWURFLFileNameShouldHandleUrlWithQueryParams() {
+ // given
+ final String url = "https://example.com/wurfl.zip?version=latest&format=zip";
+
+ // when
+ final String result = WURFLEngineUtils.extractWURFLFileName(url);
+
+ // then
+ assertThat(result).isEqualTo("wurfl.zip");
+ }
+
+ @Test
+ public void extractWURFLFileNameShouldThrowExceptionForNullUrl() {
+ // when & then
+ assertThatThrownBy(() -> WURFLEngineUtils.extractWURFLFileName(null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Invalid WURFL snapshot URL: null");
+ }
+
+ @Test
+ public void initializeEngineShouldThrowExceptionForNullDataFilePath() {
+ // when & then
+ assertThatThrownBy(() -> WURFLEngineUtils.initializeEngine(configProperties, null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Invalid WURFL snapshot URL: null");
+ }
+}
diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolverTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolverTest.java
new file mode 100644
index 00000000000..ca45c27bf47
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolverTest.java
@@ -0,0 +1,172 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.resolver;
+
+import com.iab.openrtb.request.BrandVersion;
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.UserAgent;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class HeadersResolverTest {
+
+ private static final String TEST_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
+
+ @Test
+ public void resolveWithNullDeviceShouldReturnOriginalHeaders() {
+ // given
+ final Map headers = new HashMap<>();
+ headers.put("test", "value");
+
+ // when
+ final Map result = HeadersResolver.resolve(null, headers);
+
+ // then
+ assertThat(result).isEqualTo(headers);
+ }
+
+ @Test
+ public void resolveWithDeviceUaShouldReturnUserAgentHeader() {
+ // given
+ final Device device = Device.builder()
+ .ua("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
+ .build();
+
+ // when
+ final Map result = HeadersResolver.resolve(device, new HashMap<>());
+
+ // then
+ assertThat(result).containsEntry("User-Agent", TEST_USER_AGENT);
+ }
+
+ @Test
+ public void resolveWithFullSuaShouldReturnAllHeaders() {
+ // given
+ final BrandVersion brandVersion = new BrandVersion(
+ "Chrome",
+ Arrays.asList("100", "0", "0"),
+ null);
+
+ final BrandVersion winBrandVersion = new BrandVersion(
+ "Windows",
+ Arrays.asList("10", "0", "0"),
+ null);
+ final UserAgent sua = UserAgent.builder()
+ .browsers(List.of(brandVersion))
+ .platform(winBrandVersion)
+ .model("Test Model")
+ .architecture("x86")
+ .mobile(0)
+ .build();
+
+ final Device device = Device.builder()
+ .sua(sua)
+ .build();
+
+ // when
+ final Map result = HeadersResolver.resolve(device, new HashMap<>());
+
+ // then
+ assertThat(result)
+ .containsEntry("Sec-CH-UA", "\"Chrome\";v=\"100.0.0\"")
+ .containsEntry("Sec-CH-UA-Full-Version-List", "\"Chrome\";v=\"100.0.0\"")
+ .containsEntry("Sec-CH-UA-Platform", "Windows")
+ .containsEntry("Sec-CH-UA-Platform-Version", "10.0.0")
+ .containsEntry("Sec-CH-UA-Model", "Test Model")
+ .containsEntry("Sec-CH-UA-Arch", "x86")
+ .containsEntry("Sec-CH-UA-Mobile", "?0");
+ }
+
+ @Test
+ public void resolveWithFullDeviceAndHeadersShouldPrioritizeDevice() {
+ // given
+ final BrandVersion brandVersion = new BrandVersion(
+ "Chrome",
+ Arrays.asList("100", "0", "0"),
+ null);
+
+ final BrandVersion winBrandVersion = new BrandVersion(
+ "Windows",
+ Arrays.asList("10", "0", "0"),
+ null);
+ final UserAgent sua = UserAgent.builder()
+ .browsers(List.of(brandVersion))
+ .platform(winBrandVersion)
+ .model("Test Model")
+ .architecture("x86")
+ .mobile(0)
+ .build();
+
+ final Device device = Device.builder()
+ .sua(sua)
+ .ua(TEST_USER_AGENT)
+ .build();
+
+ final Map headers = new HashMap<>();
+ headers.put("Sec-CH-UA", "Test UA-CH");
+ headers.put("Sec-CH-UA-Full-Version-List", "Test-UA-Full-Version-List");
+ headers.put("Sec-CH-UA-Platform", "Test-UA-Platform");
+ headers.put("Sec-CH-UA-Platform-Version", "Test-UA-Platform-Version");
+ headers.put("Sec-CH-UA-Model", "Test-UA-Model");
+ headers.put("Sec-CH-UA-Arch", "Test-UA-Arch");
+ headers.put("Sec-CH-UA-Mobile", "Test-UA-Mobile");
+ headers.put("User-Agent", "Mozilla/5.0 (Test OS; 10) like Gecko");
+ // when
+ final Map result = HeadersResolver.resolve(device, headers);
+
+ // then
+ assertThat(result)
+ .containsEntry("Sec-CH-UA", "\"Chrome\";v=\"100.0.0\"")
+ .containsEntry("Sec-CH-UA-Full-Version-List", "\"Chrome\";v=\"100.0.0\"")
+ .containsEntry("Sec-CH-UA-Platform", "Windows")
+ .containsEntry("Sec-CH-UA-Platform-Version", "10.0.0")
+ .containsEntry("Sec-CH-UA-Model", "Test Model")
+ .containsEntry("Sec-CH-UA-Arch", "x86")
+ .containsEntry("Sec-CH-UA-Mobile", "?0");
+ }
+
+ @Test
+ public void resolveWithMultipleBrandVersionsShouldFormatCorrectly() {
+ // given
+ final BrandVersion chrome = new BrandVersion("Chrome",
+ Arrays.asList("100", "0"),
+ null);
+ final BrandVersion chromium = new BrandVersion("Chromium",
+ Arrays.asList("100", "0"),
+ null);
+
+ final BrandVersion notABrand = new BrandVersion("Not\\A;Brand",
+ Arrays.asList("99", "0"),
+ null);
+
+ final UserAgent sua = UserAgent.builder()
+ .browsers(Arrays.asList(chrome, chromium, notABrand))
+ .build();
+
+ final Device device = Device.builder()
+ .sua(sua)
+ .build();
+
+ // when
+ final Map result = HeadersResolver.resolve(device, new HashMap<>());
+
+ // then
+ final String expectedFormat = "\"Chrome\";v=\"100.0\", \"Chromium\";v=\"100.0\", \"Not\\A;Brand\";v=\"99.0\"";
+ assertThat(result)
+ .containsEntry("Sec-CH-UA", expectedFormat)
+ .containsEntry("Sec-CH-UA-Full-Version-List", expectedFormat);
+ }
+
+ @Test
+ public void resolveWithNullDeviceAndNullHeadersShouldReturnEmptyMap() {
+ // when
+ final Map result = HeadersResolver.resolve(null, null);
+
+ // then
+ assertThat(result).isEmpty();
+ }
+}
diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdaterTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdaterTest.java
new file mode 100644
index 00000000000..54e7464619d
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdaterTest.java
@@ -0,0 +1,397 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1;
+
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.iab.openrtb.request.Device;
+import org.mockito.Mock;
+import org.prebid.server.proto.openrtb.ext.request.ExtDevice;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.json.ObjectMapperProvider;
+import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
+import com.iab.openrtb.request.BidRequest;
+
+import java.math.BigDecimal;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mock.Strictness.LENIENT;
+
+@ExtendWith(MockitoExtension.class)
+public class OrtbDeviceUpdaterTest {
+
+ private Set staticCaps;
+ private Set virtualCaps;
+ private JacksonMapper mapper;
+
+ @Mock(strictness = LENIENT)
+ private AuctionRequestPayload payload;
+
+ @Mock(strictness = LENIENT)
+ private com.scientiamobile.wurfl.core.Device wurflDevice;
+
+ @BeforeEach
+ void setUp() {
+ staticCaps = Set.of("ajax_support_javascript", "brand_name", "density_class",
+ "is_connected_tv", "is_ott", "is_tablet", "model_name", "resolution_height", "resolution_width",
+ "physical_form_factor");
+ virtualCaps = Set.of("advertised_device_os", "advertised_device_os_version",
+ "is_full_desktop", "pixel_density");
+ mapper = new JacksonMapper(ObjectMapperProvider.mapper());
+ }
+
+ @Test
+ public void updateShouldUpdateDeviceMakeWhenOriginalIsEmpty() {
+ // given
+ given(wurflDevice.getCapability("brand_name")).willReturn("Apple");
+ given(wurflDevice.getCapability("model_name")).willReturn("iPhone");
+ given(wurflDevice.getCapabilityAsBool("ajax_support_javascript")).willReturn(true);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(true);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_phone")).willReturn(true);
+ given(wurflDevice.getCapabilityAsBool("is_tablet")).willReturn(false);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(false);
+ final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper);
+ final Device device = Device.builder().build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ given(payload.bidRequest()).willReturn(bidRequest);
+ // when
+ final AuctionRequestPayload result = target.apply(payload);
+
+ // then
+ final Device resultDevice = result.bidRequest().getDevice();
+ assertThat(resultDevice.getMake()).isEqualTo("Apple");
+ assertThat(resultDevice.getDevicetype()).isEqualTo(1);
+ }
+
+ @Test
+ public void updateShouldNotUpdateDeviceMakeWhenOriginalExists() {
+ // given
+ final Device device = Device.builder().make("Samsung").build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ given(wurflDevice.getCapability("brand_name")).willReturn("Apple");
+ given(wurflDevice.getCapability("model_name")).willReturn("iPhone");
+ given(wurflDevice.getCapability("ajax_support_javascript")).willReturn("true");
+ given(wurflDevice.getVirtualCapability("is_mobile")).willReturn("true");
+ given(wurflDevice.getVirtualCapability("is_phone")).willReturn("true");
+ given(wurflDevice.getCapability("is_tablet")).willReturn("false");
+ given(wurflDevice.getVirtualCapability("is_full_desktop")).willReturn("false");
+ final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper);
+ given(payload.bidRequest()).willReturn(bidRequest);
+
+ // when
+ final AuctionRequestPayload result = target.apply(payload);
+
+ // then
+ final Device resultDevice = result.bidRequest().getDevice();
+ assertThat(resultDevice.getMake()).isEqualTo("Samsung");
+ }
+
+ @Test
+ public void updateShouldNotUpdateDeviceMakeWhenOriginalBigIntegerExists() {
+ // given
+ final Device device = Device.builder().make("Apple").pxratio(new BigDecimal("1.0")).build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ given(wurflDevice.getCapability("brand_name")).willReturn("Apple");
+ given(wurflDevice.getCapability("model_name")).willReturn("iPhone");
+ given(wurflDevice.getCapability("ajax_support_javascript")).willReturn("true");
+ given(wurflDevice.getVirtualCapability("is_mobile")).willReturn("true");
+ given(wurflDevice.getVirtualCapability("is_phone")).willReturn("true");
+ given(wurflDevice.getCapability("is_tablet")).willReturn("false");
+ given(wurflDevice.getVirtualCapability("is_full_desktop")).willReturn("false");
+ final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper);
+ given(payload.bidRequest()).willReturn(bidRequest);
+
+ // when
+ final AuctionRequestPayload result = target.apply(payload);
+
+ // then
+ final Device resultDevice = result.bidRequest().getDevice();
+ assertThat(resultDevice.getMake()).isEqualTo("Apple");
+ assertThat(resultDevice.getPxratio()).isEqualTo("1.0");
+ }
+
+ @Test
+ public void updateShouldUpdateDeviceModelWhenOriginalIsEmpty() {
+ // given
+ final Device device = Device.builder().build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ given(wurflDevice.getCapability("brand_name")).willReturn("Apple");
+ given(wurflDevice.getCapability("model_name")).willReturn("iPhone");
+ given(wurflDevice.getCapability("ajax_support_javascript")).willReturn("true");
+ given(wurflDevice.getVirtualCapability("is_mobile")).willReturn("true");
+ given(wurflDevice.getVirtualCapability("is_phone")).willReturn("true");
+ given(wurflDevice.getCapability("is_tablet")).willReturn("false");
+ given(wurflDevice.getVirtualCapability("is_full_desktop")).willReturn("false");
+ final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper);
+ given(payload.bidRequest()).willReturn(bidRequest);
+
+ // when
+ final AuctionRequestPayload result = target.apply(payload);
+
+ // then
+ final Device resultDevice = result.bidRequest().getDevice();
+ assertThat(resultDevice.getModel()).isEqualTo("iPhone");
+ }
+
+ @Test
+ public void updateShouldUpdateDeviceOsWhenOriginalIsEmpty() {
+ // given
+ final Device device = Device.builder().build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ given(wurflDevice.getCapability("brand_name")).willReturn("Apple");
+ given(wurflDevice.getCapability("model_name")).willReturn("iPhone");
+ given(wurflDevice.getVirtualCapability("advertised_device_os")).willReturn("iOS");
+ given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(false);
+ final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper);
+ given(payload.bidRequest()).willReturn(bidRequest);
+
+ // when
+ final AuctionRequestPayload result = target.apply(payload);
+
+ // then
+ final Device resultDevice = result.bidRequest().getDevice();
+ assertThat(resultDevice.getOs()).isEqualTo("iOS");
+ }
+
+ @Test
+ public void updateShouldUpdateResolutionWhenOriginalIsEmpty() {
+ // given
+ final Device device = Device.builder().build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ given(wurflDevice.getCapability("brand_name")).willReturn("Apple");
+ given(wurflDevice.getCapability("model_name")).willReturn("iPhone");
+ given(wurflDevice.getCapabilityAsBool("ajax_support_javascript")).willReturn(true);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(true);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_phone")).willReturn(true);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_tablet")).willReturn(false);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(false);
+ given(wurflDevice.getCapabilityAsInt("resolution_width")).willReturn(3200);
+ given(wurflDevice.getCapabilityAsInt("resolution_height")).willReturn(1440);
+ final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper);
+ given(payload.bidRequest()).willReturn(bidRequest);
+ // when
+ final AuctionRequestPayload result = target.apply(payload);
+
+ // then
+ final Device resultDevice = result.bidRequest().getDevice();
+ assertThat(resultDevice.getW()).isEqualTo(3200);
+ assertThat(resultDevice.getH()).isEqualTo(1440);
+ }
+
+ @Test
+ public void updateShouldHandleJavascriptSupport() {
+ // given
+ final Device device = Device.builder().build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ given(wurflDevice.getCapabilityAsBool("ajax_support_javascript")).willReturn(true);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(true);
+ final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper);
+ given(payload.bidRequest()).willReturn(bidRequest);
+
+ // when
+ final AuctionRequestPayload result = target.apply(payload);
+
+ // then
+ final Device resultDevice = result.bidRequest().getDevice();
+ assertThat(resultDevice.getJs()).isEqualTo(1);
+ }
+
+ @Test
+ public void updateShouldHandleOttDeviceType() {
+ // given
+ final Device device = Device.builder().build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ given(wurflDevice.getCapabilityAsBool("is_ott")).willReturn(true);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(false);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(false);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_phone")).willReturn(false);
+ given(wurflDevice.getCapabilityAsBool("is_tablet")).willReturn(false);
+ final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper);
+ given(payload.bidRequest()).willReturn(bidRequest);
+ // when
+ final AuctionRequestPayload result = target.apply(payload);
+
+ // then
+ final Device resultDevice = result.bidRequest().getDevice();
+ assertThat(resultDevice.getDevicetype()).isEqualTo(7);
+ }
+
+ @Test
+ public void updateShouldHandleOutOfHomeDeviceType() {
+ // given
+ final Device device = Device.builder().build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ given(wurflDevice.getCapability("physical_form_factor")).willReturn("out_of_home_device");
+ given(wurflDevice.getCapabilityAsBool("is_tablet")).willReturn(false);
+ given(wurflDevice.getCapabilityAsBool("is_wireless_device")).willReturn(false);
+ final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper);
+ given(payload.bidRequest()).willReturn(bidRequest);
+ // when
+ final AuctionRequestPayload result = target.apply(payload);
+
+ // then
+ final Device resultDevice = result.bidRequest().getDevice();
+ assertThat(resultDevice.getDevicetype()).isEqualTo(8);
+ }
+
+ @Test
+ public void updateShouldReturnDeviceOtherMobileWhenMobileIsNotPhoneOrTablet() {
+ // given
+ final Device device = Device.builder().build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(true);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_phone")).willReturn(false);
+ given(wurflDevice.getCapabilityAsBool("is_tablet")).willReturn(false);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(false);
+ given(wurflDevice.getVirtualCapability("advertised_device_os")).willReturn("TestOs");
+ given(wurflDevice.getVirtualCapability("advertised_device_os_version")).willReturn("1.0");
+ final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper);
+ given(payload.bidRequest()).willReturn(bidRequest);
+
+ // when
+ final AuctionRequestPayload result = target.apply(payload);
+ // then
+ final Device resultDevice = result.bidRequest().getDevice();
+ assertThat(resultDevice.getDevicetype()).isEqualTo(6);
+ }
+
+ @Test
+ public void updateShouldReturnNullWhenMobileTypeIsUnknown() {
+ // given
+ given(wurflDevice.getVirtualCapability("is_mobile")).willReturn("false");
+ given(wurflDevice.getVirtualCapability("is_phone")).willReturn("false");
+ given(wurflDevice.getCapability("is_tablet")).willReturn("false");
+ given(wurflDevice.getVirtualCapability("is_full_desktop")).willReturn("false");
+ final Device device = Device.builder().build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper);
+ // when
+ given(payload.bidRequest()).willReturn(bidRequest);
+ final AuctionRequestPayload result = target.apply(payload);
+ // then
+ final Device resultDevice = result.bidRequest().getDevice();
+ assertThat(resultDevice.getDevicetype()).isNull();
+ }
+
+ @Test
+ public void updateShouldHandlePersonalComputerDeviceType() {
+ // given
+ final Device device = Device.builder().build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(false);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_phone")).willReturn(false);
+ given(wurflDevice.getCapabilityAsBool("is_tablet")).willReturn(false);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(true);
+ given(wurflDevice.getVirtualCapability("advertised_device_os")).willReturn("Windows");
+ given(wurflDevice.getVirtualCapability("advertised_device_os_version")).willReturn("10");
+ given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Desktop");
+ final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper);
+ // when
+ given(payload.bidRequest()).willReturn(bidRequest);
+ final AuctionRequestPayload result = target.apply(payload);
+
+ // then
+ final Device resultDevice = result.bidRequest().getDevice();
+ assertThat(resultDevice.getDevicetype()).isEqualTo(2);
+ }
+
+ @Test
+ public void updateShouldHandleConnectedTvDeviceType() {
+ // given
+ final Device device = Device.builder().build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ given(wurflDevice.getCapabilityAsBool("is_connected_tv")).willReturn(true);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(false);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(false);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_phone")).willReturn(false);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_tablet")).willReturn(false);
+ given(wurflDevice.getVirtualCapability("advertised_device_os")).willReturn("WebOS");
+ given(wurflDevice.getVirtualCapability("advertised_device_os_version")).willReturn("4");
+ given(wurflDevice.getCapabilityAsBool("is_connected_tv")).willReturn(true);
+ final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper);
+ // when
+ given(payload.bidRequest()).willReturn(bidRequest);
+ final AuctionRequestPayload result = target.apply(payload);
+ // then
+ final Device resultDevice = result.bidRequest().getDevice();
+ assertThat(resultDevice.getDevicetype()).isEqualTo(3);
+ }
+
+ @Test
+ public void updateShouldNotUpdateDeviceTypeWhenSet() {
+ // given
+ final Device device = Device.builder()
+ .devicetype(3)
+ .build();
+ given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(true);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(false);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_phone")).willReturn(false);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_tablet")).willReturn(false); // device type 2
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper);
+ // when
+ given(payload.bidRequest()).willReturn(bidRequest);
+ final AuctionRequestPayload result = target.apply(payload);
+ // then
+ final Device resultDevice = result.bidRequest().getDevice();
+ assertThat(resultDevice.getDevicetype()).isEqualTo(3); // unchanged
+ }
+
+ @Test
+ public void updateShouldHandleTabletDeviceType() {
+ // given
+ given(wurflDevice.getCapabilityAsBool("is_tablet")).willReturn(true);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(false);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(false);
+ given(wurflDevice.getVirtualCapabilityAsBool("is_phone")).willReturn(false);
+ given(wurflDevice.getCapability("brand_name")).willReturn("Samsung");
+ given(wurflDevice.getCapability("model_name")).willReturn("Galaxy Tab S9+");
+
+ final Device device = Device.builder().build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper);
+ given(payload.bidRequest()).willReturn(bidRequest);
+ // when
+ final AuctionRequestPayload result = target.apply(payload);
+
+ // then
+ final Device resultDevice = result.bidRequest().getDevice();
+ assertThat(resultDevice.getDevicetype()).isEqualTo(5);
+ }
+
+ @Test
+ public void updateShouldAddWurflPropertyToExtIfMissingAndPreserveExistingProperties() {
+ // given
+ final ExtDevice existingExt = ExtDevice.empty();
+ existingExt.addProperty("someProperty", TextNode.valueOf("value"));
+ final Device device = Device.builder()
+ .ext(existingExt)
+ .build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ given(wurflDevice.getCapability("brand_name")).willReturn("Apple");
+ given(wurflDevice.getCapability("model_name")).willReturn("iPhone");
+ given(wurflDevice.getCapability("ajax_support_javascript")).willReturn("true");
+ given(wurflDevice.getVirtualCapability("is_mobile")).willReturn("true");
+ given(wurflDevice.getVirtualCapability("is_phone")).willReturn("true");
+ given(wurflDevice.getCapability("is_tablet")).willReturn("false");
+ given(wurflDevice.getVirtualCapability("is_full_desktop")).willReturn("false");
+ final Set staticCaps = Set.of("brand_name");
+ final Set virtualCaps = Set.of("advertised_device_os");
+ final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper);
+
+ // when
+ given(payload.bidRequest()).willReturn(bidRequest);
+ final AuctionRequestPayload result = target.apply(payload);
+
+ // then
+ final Device resultDevice = result.bidRequest().getDevice();
+ final ExtDevice resultExt = resultDevice.getExt();
+ assertThat(resultExt).isNotNull();
+ assertThat(resultExt.getProperty("someProperty").textValue()).isEqualTo("value");
+ assertThat(resultExt.getProperty("wurfl")).isNotNull();
+
+ }
+}
diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHookTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHookTest.java
new file mode 100644
index 00000000000..49131c4131f
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHookTest.java
@@ -0,0 +1,80 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1;
+
+import io.vertx.core.Future;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.AuctionRequestHeadersContext;
+import org.prebid.server.hooks.v1.InvocationAction;
+import org.prebid.server.hooks.v1.InvocationContext;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.v1.InvocationStatus;
+import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload;
+import org.prebid.server.model.CaseInsensitiveMultiMap;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+
+@ExtendWith(MockitoExtension.class)
+public class WURFLDeviceDetectionEntrypointHookTest {
+
+ @Mock
+ private EntrypointPayload payload;
+
+ @Mock
+ private InvocationContext context;
+
+ @Test
+ public void codeShouldReturnCorrectHookCode() {
+
+ // given
+ final WURFLDeviceDetectionEntrypointHook target = new WURFLDeviceDetectionEntrypointHook();
+
+ // when
+ final String result = target.code();
+
+ // then
+ assertThat(result).isEqualTo("wurfl-devicedetection-entrypoint-hook");
+ }
+
+ @Test
+ public void callShouldReturnSuccessWithNoAction() {
+ // given
+ final WURFLDeviceDetectionEntrypointHook target = new WURFLDeviceDetectionEntrypointHook();
+ final CaseInsensitiveMultiMap headers = CaseInsensitiveMultiMap.builder()
+ .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test")
+ .build();
+ given(payload.headers()).willReturn(headers);
+
+ // when
+ final Future> result = target.call(payload, context);
+
+ // then
+ assertThat(result).isNotNull();
+ assertThat(result.succeeded()).isTrue();
+ final InvocationResult invocationResult = result.result();
+ assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success);
+ assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action);
+ assertThat(invocationResult.moduleContext()).isNotNull();
+ }
+
+ @Test
+ public void callShouldHandleNullHeaders() {
+ // given
+ final WURFLDeviceDetectionEntrypointHook target = new WURFLDeviceDetectionEntrypointHook();
+
+ // when
+ given(payload.headers()).willReturn(null);
+ final Future> result = target.call(payload, context);
+
+ // then
+ assertThat(result).isNotNull();
+ assertThat(result.succeeded()).isTrue();
+ final InvocationResult invocationResult = result.result();
+ assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success);
+ assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action);
+ assertThat(invocationResult.moduleContext()).isNotNull();
+ assertThat(invocationResult.moduleContext() instanceof AuctionRequestHeadersContext).isTrue();
+ }
+}
diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModuleTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModuleTest.java
new file mode 100644
index 00000000000..99ae360e5ac
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModuleTest.java
@@ -0,0 +1,40 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1;
+
+import org.junit.jupiter.api.Test;
+import org.prebid.server.hooks.v1.Hook;
+import org.prebid.server.hooks.v1.InvocationContext;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Collection;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class WURFLDeviceDetectionModuleTest {
+
+ @Test
+ public void codeShouldReturnCorrectModuleCode() {
+ // given
+ final List> hooks = new ArrayList<>();
+ final WURFLDeviceDetectionModule target = new WURFLDeviceDetectionModule(hooks);
+
+ // when
+ final String result = target.code();
+
+ // then
+ assertThat(result).isEqualTo("wurfl-devicedetection");
+ }
+
+ @Test
+ public void hooksShouldReturnProvidedHooks() {
+ // given
+ final List> hooks = new ArrayList<>();
+ final WURFLDeviceDetectionModule target = new WURFLDeviceDetectionModule(hooks);
+
+ // when
+ final Collection extends Hook, ? extends InvocationContext>> result = target.hooks();
+
+ // then
+ assertThat(result).isSameAs(hooks);
+ }
+}
diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHookTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHookTest.java
new file mode 100644
index 00000000000..8d82ebb0209
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHookTest.java
@@ -0,0 +1,215 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1;
+
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import com.scientiamobile.wurfl.core.WURFLEngine;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.auction.model.AuctionContext;
+import org.prebid.server.settings.model.Account;
+import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.json.ObjectMapperProvider;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.AuctionRequestHeadersContext;
+import org.prebid.server.hooks.v1.InvocationAction;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.v1.InvocationStatus;
+import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
+import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
+import org.prebid.server.model.CaseInsensitiveMultiMap;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.ArgumentMatchers.any;
+
+@ExtendWith(MockitoExtension.class)
+public class WURFLDeviceDetectionRawAuctionRequestHookTest {
+
+ @Mock
+ private WURFLEngine wurflEngine;
+
+ @Mock
+ private WURFLDeviceDetectionConfigProperties configProperties;
+
+ @Mock
+ private AuctionRequestPayload payload;
+
+ @Mock
+ private AuctionInvocationContext context;
+
+ private AuctionContext auctionContext;
+
+ @Mock(strictness = Mock.Strictness.LENIENT)
+ private Account account;
+
+ @Mock(strictness = Mock.Strictness.LENIENT)
+ private com.scientiamobile.wurfl.core.Device wurflDevice;
+
+ private JacksonMapper mapper = new JacksonMapper(ObjectMapperProvider.mapper());
+
+ private WURFLDeviceDetectionRawAuctionRequestHook target;
+
+ @BeforeEach
+ public void setUp() {
+ auctionContext = AuctionContext.builder().account(account).build();
+
+ final WURFLService wurflService = new WURFLService(wurflEngine, configProperties);
+ target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper);
+ }
+
+ @Test
+ public void codeShouldReturnCorrectHookCode() {
+ // when
+ final String result = target.code();
+
+ // then
+ assertThat(result).isEqualTo("wurfl-devicedetection-raw-auction-request");
+ }
+
+ @Test
+ public void callShouldReturnNoActionWhenDeviceIsNull() {
+ // given
+ final BidRequest bidRequest = BidRequest.builder().build();
+ given(payload.bidRequest()).willReturn(bidRequest);
+
+ // when
+ final InvocationResult result = target.call(payload, context).result();
+
+ // then
+ assertThat(result.status()).isEqualTo(InvocationStatus.success);
+ assertThat(result.action()).isEqualTo(InvocationAction.no_action);
+ }
+
+ @Test
+ public void callShouldUpdateDeviceWhenWurflDeviceIsDetected() throws Exception {
+ // given
+ final String ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_7_2) Version/17.4.1 Mobile/15E148 Safari/604.1";
+ final Device device = Device.builder().ua(ua).build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ given(payload.bidRequest()).willReturn(bidRequest);
+
+ final CaseInsensitiveMultiMap headers = CaseInsensitiveMultiMap.builder()
+ .add("User-Agent", ua)
+ .build();
+ final AuctionRequestHeadersContext headersContext = AuctionRequestHeadersContext.from(headers);
+
+ given(context.moduleContext()).willReturn(headersContext);
+ given(wurflEngine.getDeviceForRequest(any(Map.class))).willReturn(wurflDevice);
+ given(wurflDevice.getId()).willReturn("apple_iphone_ver1");
+ given(wurflDevice.getCapability("brand_name")).willReturn("Apple");
+ given(wurflDevice.getCapability("model_name")).willReturn("iPhone");
+
+ // when
+ final InvocationResult result = target.call(payload, context).result();
+
+ // then
+ assertThat(result.status()).isEqualTo(InvocationStatus.success);
+ assertThat(result.action()).isEqualTo(InvocationAction.update);
+ }
+
+ @Test
+ public void shouldEnrichDeviceWhenAllowedPublisherIdsIsEmpty() throws Exception {
+ // given
+ final String ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_7_2) Version/17.4.1 Mobile/15E148 Safari/604.1";
+ final Device device = Device.builder().ua(ua).build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ given(payload.bidRequest()).willReturn(bidRequest);
+
+ final CaseInsensitiveMultiMap headers = CaseInsensitiveMultiMap.builder()
+ .add("User-Agent", ua)
+ .build();
+ final AuctionRequestHeadersContext headersContext = AuctionRequestHeadersContext.from(headers);
+
+ given(context.moduleContext()).willReturn(headersContext);
+ given(wurflEngine.getDeviceForRequest(any(Map.class))).willReturn(wurflDevice);
+ given(wurflDevice.getId()).willReturn("apple_iphone_ver1");
+ given(wurflDevice.getCapability("brand_name")).willReturn("Apple");
+ given(wurflDevice.getCapability("model_name")).willReturn("iPhone");
+ given(configProperties.getAllowedPublisherIds()).willReturn(Collections.emptySet());
+
+ final WURFLService wurflService = new WURFLService(wurflEngine, configProperties);
+ target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper);
+
+ // when
+ final InvocationResult result = target.call(payload, context).result();
+
+ // then
+ assertThat(result.status()).isEqualTo(InvocationStatus.success);
+ assertThat(result.action()).isEqualTo(InvocationAction.update);
+ }
+
+ @Test
+ public void shouldEnrichDeviceWhenAccountIsAllowed() throws Exception {
+ // given
+ final String ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_7_2) Version/17.4.1 Mobile/15E148 Safari/604.1";
+ final Device device = Device.builder().ua(ua).build();
+ final BidRequest bidRequest = BidRequest.builder().device(device).build();
+ given(payload.bidRequest()).willReturn(bidRequest);
+
+ final CaseInsensitiveMultiMap headers = CaseInsensitiveMultiMap.builder()
+ .add("User-Agent", ua)
+ .build();
+ final AuctionRequestHeadersContext headersContext = AuctionRequestHeadersContext.from(headers);
+
+ given(context.moduleContext()).willReturn(headersContext);
+ given(wurflEngine.getDeviceForRequest(any(Map.class))).willReturn(wurflDevice);
+ given(wurflDevice.getId()).willReturn("apple_iphone_ver1");
+ given(wurflDevice.getCapability("brand_name")).willReturn("Apple");
+ given(wurflDevice.getCapability("model_name")).willReturn("iPhone");
+ given(account.getId()).willReturn("allowed-publisher");
+ given(configProperties.getAllowedPublisherIds()).willReturn(Set.of("allowed-publisher",
+ "another-allowed-publisher"));
+ given(context.auctionContext()).willReturn(auctionContext);
+
+ final WURFLService wurflService = new WURFLService(wurflEngine, configProperties);
+ target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper);
+
+ // when
+ final InvocationResult result = target.call(payload, context).result();
+
+ // then
+ assertThat(result.status()).isEqualTo(InvocationStatus.success);
+ assertThat(result.action()).isEqualTo(InvocationAction.update);
+ }
+
+ @Test
+ public void shouldNotEnrichDeviceWhenPublisherIdIsNotAllowed() {
+ // given
+ given(context.auctionContext()).willReturn(auctionContext);
+ given(account.getId()).willReturn("unknown-publisher");
+ given(configProperties.getAllowedPublisherIds()).willReturn(Set.of("allowed-publisher"));
+ final WURFLService wurflService = new WURFLService(wurflEngine, configProperties);
+ target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper);
+
+ // when
+ final InvocationResult result = target.call(payload, context).result();
+
+ // then
+ assertThat(result.status()).isEqualTo(InvocationStatus.success);
+ assertThat(result.action()).isEqualTo(InvocationAction.no_action);
+ }
+
+ @Test
+ public void shouldNotEnrichDeviceWhenPublisherIdIsEmpty() {
+ // given
+ given(context.auctionContext()).willReturn(auctionContext);
+ given(account.getId()).willReturn("");
+ given(configProperties.getAllowedPublisherIds()).willReturn(Set.of("allowed-publisher"));
+ final WURFLService wurflService = new WURFLService(wurflEngine, configProperties);
+ target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper);
+
+ // when
+ final InvocationResult result = target.call(payload, context).result();
+
+ // then
+ assertThat(result.status()).isEqualTo(InvocationStatus.success);
+ assertThat(result.action()).isEqualTo(InvocationAction.no_action);
+ }
+}
diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLServiceTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLServiceTest.java
new file mode 100644
index 00000000000..4dd2cfde2fe
--- /dev/null
+++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLServiceTest.java
@@ -0,0 +1,164 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1;
+
+import com.scientiamobile.wurfl.core.Device;
+import com.scientiamobile.wurfl.core.WURFLEngine;
+import io.vertx.core.Future;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mock.Strictness.LENIENT;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.doReturn;
+
+@ExtendWith(MockitoExtension.class)
+public class WURFLServiceTest {
+
+ @Mock(strictness = LENIENT)
+ private WURFLEngine wurflEngine;
+
+ @Mock(strictness = LENIENT)
+ private WURFLDeviceDetectionConfigProperties configProperties;
+
+ private WURFLService wurflService;
+
+ @BeforeEach
+ public void setUp() {
+ wurflService = new WURFLService(wurflEngine, configProperties);
+ }
+
+ @Test
+ public void setDataPathShouldReturnSucceededFutureWhenProcessingSucceeds() throws Exception {
+ // given
+ final String dataFilePath = "test-data-path";
+ final String wurflSnapshotUrl = "http://example.com/wurfl-snapshot.zip";
+ final String wurflFileDirPath = System.getProperty("java.io.tmpdir");
+
+ given(configProperties.getFileSnapshotUrl()).willReturn(wurflSnapshotUrl);
+ given(configProperties.getFileDirPath()).willReturn(wurflFileDirPath);
+
+ final WURFLService spyWurflService = spy(wurflService);
+ doReturn(wurflEngine).when(spyWurflService).createEngine(dataFilePath);
+
+ // when
+ final Future> future = spyWurflService.setDataPath(dataFilePath);
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+ }
+
+ @Test
+ public void setDataPathShouldReturnFailedFutureWhenExceptionOccurs() {
+ // given
+ final String dataFilePath = "test-data-path";
+ final WURFLService spyWurflService = spy(wurflService);
+ doThrow(new RuntimeException("Simulated load() failure"))
+ .when(spyWurflService).createEngine(dataFilePath);
+
+ // when
+ final Future> result = spyWurflService.setDataPath(dataFilePath);
+
+ // then
+ assertThat(result).isNotNull();
+ assertThat(result.failed()).isTrue();
+ assertThat(result.cause()).isInstanceOf(RuntimeException.class)
+ .hasMessage("Simulated load() failure");
+ }
+
+ @Test
+ public void lookupDeviceShouldReturnDeviceWhenEngineIsNotNull() {
+ // given
+ final Map headers = new HashMap<>();
+ headers.put("User-Agent", "test-user-agent");
+
+ final Device expectedDevice = mock(Device.class);
+ given(wurflEngine.getDeviceForRequest(headers)).willReturn(expectedDevice);
+
+ // when
+ final Optional result = wurflService.lookupDevice(headers);
+
+ // then
+ assertThat(result).isPresent();
+ assertThat(result.get()).isEqualTo(expectedDevice);
+ verify(wurflEngine).getDeviceForRequest(headers);
+ }
+
+ @Test
+ public void lookupDeviceShouldReturnEmptyWhenEngineIsNull() {
+ // given
+ wurflService = new WURFLService(null, configProperties);
+ final Map headers = new HashMap<>();
+
+ // when
+ final Optional result = wurflService.lookupDevice(headers);
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void getAllCapabilitiesShouldReturnCapabilitiesWhenEngineIsNotNull() {
+ // given
+ final Set expectedCapabilities = Set.of("capability1", "capability2");
+ given(wurflEngine.getAllCapabilities()).willReturn(expectedCapabilities);
+
+ // when
+ final Set result = wurflService.getAllCapabilities();
+
+ // then
+ assertThat(result).isEqualTo(expectedCapabilities);
+ verify(wurflEngine).getAllCapabilities();
+ }
+
+ @Test
+ public void getAllCapabilitiesShouldReturnEmptySetWhenEngineIsNull() {
+ // given
+ wurflService = new WURFLService(null, configProperties);
+
+ // when
+ final Set result = wurflService.getAllCapabilities();
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void getAllVirtualCapabilitiesShouldReturnCapabilitiesWhenEngineIsNotNull() {
+ // given
+ final Set expectedCapabilities = Set.of("virtualCapability1", "virtualCapability2");
+ given(wurflEngine.getAllVirtualCapabilities()).willReturn(expectedCapabilities);
+
+ // when
+ final Set result = wurflService.getAllVirtualCapabilities();
+
+ // then
+ assertThat(result).isEqualTo(expectedCapabilities);
+ verify(wurflEngine).getAllVirtualCapabilities();
+ }
+
+ @Test
+ public void getAllVirtualCapabilitiesShouldReturnEmptySetWhenEngineIsNull() {
+ // given
+ wurflService = new WURFLService(null, configProperties);
+
+ // when
+ final Set result = wurflService.getAllVirtualCapabilities();
+
+ // then
+ assertThat(result).isEmpty();
+ }
+}
diff --git a/sample/configs/prebid-config-with-wurfl.yaml b/sample/configs/prebid-config-with-wurfl.yaml
new file mode 100644
index 00000000000..5dff90201ee
--- /dev/null
+++ b/sample/configs/prebid-config-with-wurfl.yaml
@@ -0,0 +1,90 @@
+status-response: "ok"
+adapters:
+ appnexus:
+ enabled: true
+ ix:
+ enabled: true
+ openx:
+ enabled: true
+ pubmatic:
+ enabled: true
+ rubicon:
+ enabled: true
+metrics:
+ prefix: prebid
+cache:
+ scheme: http
+ host: localhost
+ path: /cache
+ query: uuid=
+settings:
+ enforce-valid-account: false
+ generate-storedrequest-bidrequest-id: true
+ filesystem:
+ settings-filename: sample/configs/sample-app-settings.yaml
+ stored-requests-dir: sample
+ stored-imps-dir: sample
+ stored-responses-dir: sample
+ categories-dir:
+gdpr:
+ default-value: 1
+ vendorlist:
+ v2:
+ cache-dir: /var/tmp/vendor2
+ v3:
+ cache-dir: /var/tmp/vendor3
+admin-endpoints:
+ logging-changelevel:
+ enabled: true
+ path: /logging/changelevel
+ on-application-port: true
+ protected: false
+hooks:
+ wurfl-devicedetection:
+ enabled: true
+ host-execution-plan: >
+ {
+ "endpoints": {
+ "/openrtb2/auction": {
+ "stages": {
+ "entrypoint": {
+ "groups": [
+ {
+ "timeout": 10,
+ "hook_sequence": [
+ {
+ "module_code": "wurfl-devicedetection",
+ "hook_impl_code": "wurfl-devicedetection-entrypoint-hook"
+ }
+ ]
+ }
+ ]
+ },
+ "raw_auction_request": {
+ "groups": [
+ {
+ "timeout": 10,
+ "hook_sequence": [
+ {
+ "module_code": "wurfl-devicedetection",
+ "hook_impl_code": "wurfl-devicedetection-raw-auction-request"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+
+
+ modules:
+ wurfl-devicedetection:
+ file-dir-path:
+ # replace with your wurfl file snapshot URL when using a licensed version of wurfl
+ file-snapshot-url: https://httpstat.us/200
+ cache-size: 200000
+ update-frequency-in-hours: 24
+ allowed-publisher-ids: 1
+ ext-caps: true
diff --git a/src/main/java/org/prebid/server/bidder/minutemedia/MinuteMediaBidder.java b/src/main/java/org/prebid/server/bidder/minutemedia/MinuteMediaBidder.java
index 544e4df12d8..e1ade2c251a 100644
--- a/src/main/java/org/prebid/server/bidder/minutemedia/MinuteMediaBidder.java
+++ b/src/main/java/org/prebid/server/bidder/minutemedia/MinuteMediaBidder.java
@@ -37,10 +37,12 @@ public class MinuteMediaBidder implements Bidder {
public static final String PUBLISHER_ID_MACRO = "{{PublisherId}}";
private final String endpointUrl;
+ private final String testEndpointUrl;
private final JacksonMapper mapper;
- public MinuteMediaBidder(String endpointUrl, JacksonMapper mapper) {
+ public MinuteMediaBidder(String endpointUrl, String testEndpointUrl, JacksonMapper mapper) {
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
+ this.testEndpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(testEndpointUrl));
this.mapper = Objects.requireNonNull(mapper);
}
@@ -49,15 +51,15 @@ public Result>> makeHttpRequests(BidRequest bidRequ
final String orgId;
try {
- orgId = extractFirstImpOrdId(bidRequest.getImp());
+ orgId = extractFirstImpOrgId(bidRequest.getImp());
} catch (PreBidException e) {
return Result.withError(BidderError.badInput(e.getMessage()));
}
- return Result.withValue(BidderUtil.defaultRequest(bidRequest, resolveEndpoint(endpointUrl, orgId), mapper));
+ return Result.withValue(BidderUtil.defaultRequest(bidRequest, makeUrl(orgId, bidRequest.getTest()), mapper));
}
- private String extractFirstImpOrdId(List imps) {
+ private String extractFirstImpOrgId(List imps) {
return imps.stream()
.findFirst()
.map(this::parseImpExt)
@@ -76,8 +78,9 @@ private ExtImpMinuteMedia parseImpExt(Imp imp) {
}
}
- private String resolveEndpoint(String endpointUrl, String orgId) {
- return endpointUrl.replace(PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(orgId));
+ private String makeUrl(String orgId, Integer test) {
+ final String url = Objects.equals(test, 1) ? testEndpointUrl : endpointUrl;
+ return url.replace(PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(orgId));
}
@Override
@@ -120,15 +123,11 @@ private static BidderBid makeBidderBid(Bid bid, String currency, List BidType.banner;
case 2 -> BidType.video;
- default -> throw new PreBidException(
- "Unsupported bid mediaType: %s for impression: %s".formatted(bid.getMtype(), bid.getImpid()));
+ case null, default -> throw new PreBidException(
+ "Unsupported bid mediaType: %s for impression: %s".formatted(markupType, bid.getImpid()));
};
}
}
diff --git a/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java b/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java
index 2622aae49cf..d6dd53b75d7 100644
--- a/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java
+++ b/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java
@@ -112,13 +112,13 @@ private static Publisher modifyPublisher(Publisher publisher, Integer networkId)
public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) {
try {
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
- return extractBids(httpCall.getRequest().getPayload(), bidResponse);
+ return extractBids(bidResponse);
} catch (DecodeException | PreBidException e) {
return Result.withError(BidderError.badServerResponse(e.getMessage()));
}
}
- private Result> extractBids(BidRequest bidRequest, BidResponse bidResponse) {
+ private Result> extractBids(BidResponse bidResponse) {
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
return Result.empty();
}
@@ -128,19 +128,18 @@ private Result> extractBids(BidRequest bidRequest, BidResponse b
.map(SeatBid::getBid)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
- .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur()))
+ .map(bid -> BidderBid.of(bid, getBidTypeFromMarkupType(bid.getMtype()), bidResponse.getCur()))
.toList();
return Result.of(bidderBids, errors);
}
- private static BidType getBidType(String impId, List imps) {
- for (Imp imp : imps) {
- if (imp.getId().equals(impId)) {
- return imp.getVideo() != null
- ? BidType.video
- : (imp.getXNative() != null ? BidType.xNative : BidType.banner);
- }
- }
- return BidType.banner;
+ private static BidType getBidTypeFromMarkupType(Integer markupType) {
+ return switch (markupType) {
+ case 1 -> BidType.banner;
+ case 2 -> BidType.video;
+ case 3 -> BidType.audio;
+ case 4 -> BidType.xNative;
+ case null, default -> BidType.banner;
+ };
}
}
diff --git a/src/main/java/org/prebid/server/bidder/tappx/TappxBidder.java b/src/main/java/org/prebid/server/bidder/tappx/TappxBidder.java
index ac511bb03b9..67224479693 100644
--- a/src/main/java/org/prebid/server/bidder/tappx/TappxBidder.java
+++ b/src/main/java/org/prebid/server/bidder/tappx/TappxBidder.java
@@ -36,7 +36,7 @@
public class TappxBidder implements Bidder {
- private static final String VERSION = "1.4";
+ private static final String VERSION = "1.6";
private static final String TYPE_CNN = "prebid";
private static final TypeReference> TAPX_EXT_TYPE_REFERENCE =
diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adagio/ExtImpAdagio.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adagio/ExtImpAdagio.java
index 37db89821b5..fb8cc3ed7b6 100644
--- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adagio/ExtImpAdagio.java
+++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adagio/ExtImpAdagio.java
@@ -11,6 +11,8 @@ public class ExtImpAdagio {
String placement;
+ String site;
+
String pagetype;
String category;
diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MinuteMediaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MinuteMediaConfiguration.java
index 3b01a2f482a..fd32784ea58 100644
--- a/src/main/java/org/prebid/server/spring/config/bidder/MinuteMediaConfiguration.java
+++ b/src/main/java/org/prebid/server/spring/config/bidder/MinuteMediaConfiguration.java
@@ -1,5 +1,8 @@
package org.prebid.server.spring.config.bidder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
import org.prebid.server.bidder.BidderDeps;
import org.prebid.server.bidder.minutemedia.MinuteMediaBidder;
import org.prebid.server.json.JacksonMapper;
@@ -23,19 +26,30 @@ public class MinuteMediaConfiguration {
@Bean("minutemediaConfigurationProperties")
@ConfigurationProperties("adapters.minutemedia")
- BidderConfigurationProperties configurationProperties() {
- return new BidderConfigurationProperties();
+ MinuteMediaConfigurationProperties configurationProperties() {
+ return new MinuteMediaConfigurationProperties();
}
@Bean
- BidderDeps minutemediaBidderDeps(BidderConfigurationProperties minutemediaConfigurationProperties,
+ BidderDeps minutemediaBidderDeps(MinuteMediaConfigurationProperties minutemediaConfigurationProperties,
@NotBlank @Value("${external-url}") String externalUrl,
JacksonMapper mapper) {
- return BidderDepsAssembler.forBidder(BIDDER_NAME)
+ return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(minutemediaConfigurationProperties)
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
- .bidderCreator(config -> new MinuteMediaBidder(config.getEndpoint(), mapper))
+ .bidderCreator(config -> new MinuteMediaBidder(
+ config.getEndpoint(),
+ config.getTestEndpoint(),
+ mapper))
.assemble();
}
+
+ @Data
+ @EqualsAndHashCode(callSuper = true)
+ @NoArgsConstructor
+ private static class MinuteMediaConfigurationProperties extends BidderConfigurationProperties {
+
+ private String testEndpoint;
+ }
}
diff --git a/src/main/java/org/prebid/server/util/HttpUtil.java b/src/main/java/org/prebid/server/util/HttpUtil.java
index 47a5f24eda8..e08a276c6fa 100644
--- a/src/main/java/org/prebid/server/util/HttpUtil.java
+++ b/src/main/java/org/prebid/server/util/HttpUtil.java
@@ -76,6 +76,12 @@ public final class HttpUtil {
public static final CharSequence SEC_CH_UA = HttpHeaders.createOptimized("Sec-CH-UA");
public static final CharSequence SEC_CH_UA_MOBILE = HttpHeaders.createOptimized("Sec-CH-UA-Mobile");
public static final CharSequence SEC_CH_UA_PLATFORM = HttpHeaders.createOptimized("Sec-CH-UA-Platform");
+ public static final CharSequence SEC_CH_UA_PLATFORM_VERSION =
+ HttpHeaders.createOptimized("Sec-CH-UA-Platform-Version");
+ public static final CharSequence SEC_CH_UA_ARCH = HttpHeaders.createOptimized("Sec-CH-UA-Arch");
+ public static final CharSequence SEC_CH_UA_MODEL = HttpHeaders.createOptimized("Sec-CH-UA-Model");
+ public static final CharSequence SEC_CH_UA_FULL_VERSION_LIST =
+ HttpHeaders.createOptimized("Sec-CH-UA-Full-Version-List");
public static final String MACROS_OPEN = "{{";
public static final String MACROS_CLOSE = "}}";
diff --git a/src/main/resources/bidder-config/adagio.yaml b/src/main/resources/bidder-config/adagio.yaml
index c252b39c5af..1d25dca1cab 100644
--- a/src/main/resources/bidder-config/adagio.yaml
+++ b/src/main/resources/bidder-config/adagio.yaml
@@ -2,9 +2,9 @@ adapters:
adagio:
# Please deploy this config in each of your datacenters with the appropriate regional subdomain.
# Replace the `REGION` by one of the value below:
- # - For AMER: las => (https://mp-las.4dex.io/pbserver)
- # - For EMEA: ams => (https://mp-ams.4dex.io/pbserver)
- # - For APAC: tyo => (https://mp-tyo.4dex.io/pbserver)
+ # - For AMER: las => (https://mp-las.4dex.io/pbserver and https://u-las.4dex.io/pbserver/usync.html)
+ # - For EMEA: ams => (https://mp-ams.4dex.io/pbserver and https://u-amx.4dex.io/pbserver/usync.html)
+ # - For APAC: tyo => (https://mp-tyo.4dex.io/pbserver and https://u-tyo.4dex.io/pbserver/usync.html)
endpoint: https://mp-REGION.4dex.io/pbserver
ortb-version: "2.6"
endpoint-compression: gzip
@@ -20,3 +20,9 @@ adapters:
- native
supported-vendors:
vendor-id: 617
+ usersync:
+ cookie-family-name: adagio
+ iframe:
+ url: https://u-REGION.4dex.io/pbserver/usync.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&&gpp_sid={{gpp_sid}}&r={{redirect_url}}
+ support-cors: false
+ uid-macro: '{UID}'
diff --git a/src/main/resources/bidder-config/aso.yaml b/src/main/resources/bidder-config/aso.yaml
index ddae491f357..fa6a6381741 100644
--- a/src/main/resources/bidder-config/aso.yaml
+++ b/src/main/resources/bidder-config/aso.yaml
@@ -12,6 +12,11 @@ adapters:
endpoint: https://srv.bidgx.com/pbs/bidder?zid={{ZoneID}}
meta-info:
maintainer-email: aso@bidgency.com
+ kuantyx:
+ enabled: false
+ endpoint: https://srv.kntxy.com/pbs/bidder?zid={{ZoneID}}
+ meta-info:
+ maintainer-email: ssp@kuantyx.com
meta-info:
maintainer-email: support@adsrv.org
app-media-types:
diff --git a/src/main/resources/bidder-config/minutemedia.yaml b/src/main/resources/bidder-config/minutemedia.yaml
index 5271b51be67..096f7776ded 100644
--- a/src/main/resources/bidder-config/minutemedia.yaml
+++ b/src/main/resources/bidder-config/minutemedia.yaml
@@ -1,6 +1,7 @@
adapters:
minutemedia:
endpoint: https://pbs.minutemedia-prebid.com/pbs-mm?publisher_id={{PublisherId}}
+ test-endpoint: https://pbs.minutemedia-prebid.com/pbs-test?publisher_id={{PublisherId}}
modifying-vast-xml-allowed: true
meta-info:
maintainer-email: hb@minutemedia.com
diff --git a/src/main/resources/bidder-config/ogury.yaml b/src/main/resources/bidder-config/ogury.yaml
index 90fe12ccd76..b1172241a25 100644
--- a/src/main/resources/bidder-config/ogury.yaml
+++ b/src/main/resources/bidder-config/ogury.yaml
@@ -14,6 +14,10 @@ adapters:
usersync:
cookie-family-name: ogury
iframe:
- url: "https://ms-cookie-sync.presage.io/user-sync.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}&source=prebids2s"
+ url: https://ms-cookie-sync.presage.io/user-sync.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&source=prebids2s
+ uid-macro: "{{OGURY_UID}}"
+ support-cors: false
+ redirect:
+ url: https://ms-cookie-sync.presage.io/user-sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}&us_privacy={{us_privacy}}&redirect={{redirect_url}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&partner=prebids2s
uid-macro: "{{OGURY_UID}}"
support-cors: false
diff --git a/src/main/resources/bidder-config/smartadserver.yaml b/src/main/resources/bidder-config/smartadserver.yaml
index a4ff6fff510..f1233b2e754 100644
--- a/src/main/resources/bidder-config/smartadserver.yaml
+++ b/src/main/resources/bidder-config/smartadserver.yaml
@@ -1,16 +1,22 @@
adapters:
smartadserver:
endpoint: https://ssb-global.smartadserver.com
+ endpoint-compression: gzip
+ aliases:
+ equativ:
+ enabled: false
meta-info:
maintainer-email: supply-partner-integration@equativ.com
app-media-types:
- banner
- video
- native
+ - audio
site-media-types:
- banner
- video
- native
+ - audio
supported-vendors:
vendor-id: 45
usersync:
diff --git a/src/main/resources/bidder-config/vidazoo.yaml b/src/main/resources/bidder-config/vidazoo.yaml
index be5e0a10fda..13ff0644788 100644
--- a/src/main/resources/bidder-config/vidazoo.yaml
+++ b/src/main/resources/bidder-config/vidazoo.yaml
@@ -1,6 +1,20 @@
adapters:
vidazoo:
endpoint: https://prebidsrvr.cootlogix.com/openrtb/
+ aliases:
+ progx:
+ enabled: false
+ endpoint: https://exchange.programmaticx.ai/openrtb/
+ meta-info:
+ maintainer-email: pxteam@programmaticx.ai
+ vendor-id: 1344
+ usersync:
+ enabled: true
+ cookie-family-name: progx
+ iframe:
+ url: https://sync.programmaticx.ai/api/user/html/685297194d85991a5e6e36dd?pbs=true&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}
+ support-cors: false
+ uid-macro: '${userId}'
endpoint-compression: gzip
ortb-version: "2.6"
meta-info:
diff --git a/src/main/resources/static/bidder-params/adagio.json b/src/main/resources/static/bidder-params/adagio.json
index bacb62125ca..ce053031e49 100644
--- a/src/main/resources/static/bidder-params/adagio.json
+++ b/src/main/resources/static/bidder-params/adagio.json
@@ -13,6 +13,11 @@
"description": "Refers to the placement of an adunit in a page. Must not contain any information about the type of device.",
"maxLength": 30
},
+ "site": {
+ "type": "string",
+ "description": "Name of the site. Handed out by Adagio.",
+ "maxLength": 50
+ },
"pagetype": {
"type": "string",
"description": "Describes what kind of content will be present in the page.",
diff --git a/src/test/java/org/prebid/server/bidder/minutemedia/MinuteMediaBidderTest.java b/src/test/java/org/prebid/server/bidder/minutemedia/MinuteMediaBidderTest.java
index 02d22021d6f..a9df3c14105 100644
--- a/src/test/java/org/prebid/server/bidder/minutemedia/MinuteMediaBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/minutemedia/MinuteMediaBidderTest.java
@@ -33,13 +33,20 @@
public class MinuteMediaBidderTest extends VertxTest {
private static final String ENDPOINT_URL = "https://randomurl.com/exchange?publisherId={{PublisherId}}";
+ private static final String TEST_ENDPOINT_URL = "https://test.com/exchange?publisherId={{PublisherId}}";
- private final MinuteMediaBidder target = new MinuteMediaBidder(ENDPOINT_URL, jacksonMapper);
+ private final MinuteMediaBidder target = new MinuteMediaBidder(ENDPOINT_URL, TEST_ENDPOINT_URL, jacksonMapper);
@Test
public void creationShouldFailOnInvalidEndpointUrl() {
assertThatIllegalArgumentException()
- .isThrownBy(() -> new MinuteMediaBidder("invalid_url", jacksonMapper));
+ .isThrownBy(() -> new MinuteMediaBidder("invalid_url", TEST_ENDPOINT_URL, jacksonMapper));
+ }
+
+ @Test
+ public void creationShouldFailOnInvalidTestEndpointUrl() {
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> new MinuteMediaBidder(ENDPOINT_URL, "invalid_url", jacksonMapper));
}
@Test
@@ -76,6 +83,26 @@ public void makeHttpRequestsShouldResolveEndpointUrlUsingFirstImp() {
assertThat(result.getErrors()).isEmpty();
}
+ @Test
+ public void makeHttpRequestsShouldResolveTestEndpointUrlWhenTestIsOne() {
+ // given
+ final BidRequest bidRequest = givenBidRequest(
+ impBuilder -> impBuilder.ext(givenImpExt("123")),
+ impBuilder -> impBuilder.ext(givenImpExt("456")))
+ .toBuilder()
+ .test(1)
+ .build();
+
+ // when
+ final Result>> result = target.makeHttpRequests(bidRequest);
+
+ // then
+ assertThat(result.getValue())
+ .extracting(HttpRequest::getUri)
+ .containsExactly("https://test.com/exchange?publisherId=123");
+ assertThat(result.getErrors()).isEmpty();
+ }
+
@Test
public void makeHttpRequestsShouldReturnErrorOnAbsentImpExtBidderOrg() {
// given
diff --git a/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java b/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java
index 35b7470f758..d31213136fb 100644
--- a/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java
@@ -4,7 +4,6 @@
import com.iab.openrtb.request.Banner;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Imp;
-import com.iab.openrtb.request.Native;
import com.iab.openrtb.request.Publisher;
import com.iab.openrtb.request.Site;
import com.iab.openrtb.request.Video;
@@ -27,8 +26,10 @@
import java.util.function.Function;
import static java.util.Collections.singletonList;
+import static java.util.function.UnaryOperator.identity;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.prebid.server.proto.openrtb.ext.response.BidType.audio;
import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
@@ -66,7 +67,7 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
public void makeHttpRequestsShouldCreateCorrectURL() {
// given
final BidRequest bidRequest = BidRequest.builder()
- .imp(singletonList(givenImp(Function.identity())))
+ .imp(singletonList(givenImp(identity())))
.build();
// when
@@ -83,7 +84,7 @@ public void makeHttpRequestsShouldCreateCorrectURL() {
public void makeHttpRequestsShouldUpdateSiteObjectIfPresent() {
// given
final BidRequest bidRequest = BidRequest.builder()
- .imp(singletonList(givenImp(Function.identity())))
+ .imp(singletonList(givenImp(identity())))
.site(Site.builder()
.domain("www.foo.com")
.publisher(Publisher.builder().domain("foo.com").build())
@@ -110,7 +111,7 @@ public void makeHttpRequestsShouldUpdateSiteObjectIfPresent() {
public void makeHttpRequestsShouldCreateRequestForEveryValidImp() {
// given
final BidRequest bidRequest = BidRequest.builder()
- .imp(Arrays.asList(givenImp(Function.identity()),
+ .imp(Arrays.asList(givenImp(identity()),
givenImp(impBuilder -> impBuilder.id("456"))
))
.build();
@@ -196,14 +197,12 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso
}
@Test
- public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessingException {
+ public void makeBidsShouldReturnBannerBidIfMarkupTypeIsBanner() throws JsonProcessingException {
// given
final BidderCall httpCall = givenHttpCall(
- BidRequest.builder()
- .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
- .build(),
+ BidRequest.builder().build(),
mapper.writeValueAsString(
- givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
+ givenBidResponse(bidBuilder -> bidBuilder.mtype(1))));
// when
final Result> result = target.makeBids(httpCall, null);
@@ -211,18 +210,33 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessi
// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue())
- .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "EUR"));
+ .containsOnly(BidderBid.of(Bid.builder().mtype(1).build(), banner, "EUR"));
}
@Test
- public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException {
+ public void makeBidsShouldReturnAudioBidIfMarkupTypeIsAudio() throws JsonProcessingException {
// given
final BidderCall httpCall = givenHttpCall(
- BidRequest.builder()
- .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
- .build(),
+ BidRequest.builder().build(),
mapper.writeValueAsString(
- givenBidResponse(Function.identity())));
+ givenBidResponse(bidBuilder -> bidBuilder.mtype(3))));
+
+ // when
+ final Result> result = target.makeBids(httpCall, null);
+
+ // then
+ assertThat(result.getErrors()).isEmpty();
+ assertThat(result.getValue())
+ .containsOnly(BidderBid.of(Bid.builder().mtype(3).build(), audio, "EUR"));
+ }
+
+ @Test
+ public void makeBidsShouldReturnBannerBidIfMarkupTypeIsNull() throws JsonProcessingException {
+ // given
+ final BidderCall httpCall = givenHttpCall(
+ BidRequest.builder().build(),
+ mapper.writeValueAsString(
+ givenBidResponse(identity())));
// when
final Result> result = target.makeBids(httpCall, null);
@@ -234,14 +248,29 @@ public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingExcept
}
@Test
- public void makeBidsShouldReturnVideoBidIfVideoIsPresent() throws JsonProcessingException {
+ public void makeBidsShouldReturnBannerBidIfMarkupTypeOutOfBounds() throws JsonProcessingException {
+ // given
+ final BidderCall httpCall = givenHttpCall(
+ BidRequest.builder().build(),
+ mapper.writeValueAsString(
+ givenBidResponse(bidBuilder -> bidBuilder.mtype(5))));
+
+ // when
+ final Result> result = target.makeBids(httpCall, null);
+
+ // then
+ assertThat(result.getErrors()).isEmpty();
+ assertThat(result.getValue())
+ .containsOnly(BidderBid.of(Bid.builder().mtype(5).build(), banner, "EUR"));
+ }
+
+ @Test
+ public void makeBidsShouldReturnVideoBidIfMarkupTypeIsVideo() throws JsonProcessingException {
// given
final BidderCall httpCall = givenHttpCall(
- BidRequest.builder()
- .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build()))
- .build(),
+ BidRequest.builder().build(),
mapper.writeValueAsString(
- givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
+ givenBidResponse(bidBuilder -> bidBuilder.mtype(2))));
// when
final Result> result = target.makeBids(httpCall, null);
@@ -249,18 +278,16 @@ public void makeBidsShouldReturnVideoBidIfVideoIsPresent() throws JsonProcessing
// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue())
- .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "EUR"));
+ .containsOnly(BidderBid.of(Bid.builder().mtype(2).build(), video, "EUR"));
}
@Test
- public void makeBidsShouldReturnNativeBidIfNativeIsPresent() throws JsonProcessingException {
+ public void makeBidsShouldReturnNativeBidIfMarkupTypeIsNative() throws JsonProcessingException {
// given
final BidderCall httpCall = givenHttpCall(
- BidRequest.builder()
- .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
- .build(),
+ BidRequest.builder().build(),
mapper.writeValueAsString(
- givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
+ givenBidResponse(bidBuilder -> bidBuilder.mtype(4))));
// when
final Result> result = target.makeBids(httpCall, null);
@@ -268,7 +295,7 @@ public void makeBidsShouldReturnNativeBidIfNativeIsPresent() throws JsonProcessi
// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue())
- .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "EUR"));
+ .containsOnly(BidderBid.of(Bid.builder().mtype(4).build(), xNative, "EUR"));
}
private static Imp givenImp(Function impCustomizer) {
diff --git a/src/test/java/org/prebid/server/bidder/tappx/TappxBidderTest.java b/src/test/java/org/prebid/server/bidder/tappx/TappxBidderTest.java
index 90ff9fd7a62..5372c135896 100644
--- a/src/test/java/org/prebid/server/bidder/tappx/TappxBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/tappx/TappxBidderTest.java
@@ -141,7 +141,7 @@ public void makeHttpRequestsShouldMakeRequestWithUrl() {
// then
assertThat(result.getErrors()).isEmpty();
- final String expectedUri = "https://ssp.api.domain/rtb/v2/endpoint?tappxkey=tappxkey&v=1.4&type_cnn=prebid";
+ final String expectedUri = "https://ssp.api.domain/rtb/v2/endpoint?tappxkey=tappxkey&v=1.6&type_cnn=prebid";
assertThat(result.getValue()).hasSize(1)
.allSatisfy(httpRequest -> {
assertThat(httpRequest.getUri()).isEqualTo(expectedUri);
@@ -165,7 +165,7 @@ public void makeHttpRequestShouldBuildCorrectUriWithPathInHostParameterButWithou
// then
assertThat(result.getErrors()).isEmpty();
- final String expectedUri = "https://ssp.api.domain/rtb/v2/endpoint?tappxkey=tappxkey&v=1.4&type_cnn=prebid";
+ final String expectedUri = "https://ssp.api.domain/rtb/v2/endpoint?tappxkey=tappxkey&v=1.6&type_cnn=prebid";
assertThat(result.getValue()).hasSize(1)
.allSatisfy(httpRequest -> {
assertThat(httpRequest.getUri()).isEqualTo(expectedUri);
@@ -190,7 +190,7 @@ public void makeHttpRequestShouldBuildCorrectUriWithEndPointParameterIfMatched()
// then
assertThat(result.getErrors()).isEmpty();
final String expectedUri =
- "https://zz855226test.pub.domain/rtb/?tappxkey=tappxkey&v=1.4&type_cnn=prebid";
+ "https://zz855226test.pub.domain/rtb/?tappxkey=tappxkey&v=1.6&type_cnn=prebid";
assertThat(result.getValue()).hasSize(1)
.allSatisfy(httpRequest -> {
assertThat(httpRequest.getUri()).isEqualTo(expectedUri);
@@ -214,7 +214,7 @@ public void makeHttpRequestsShouldModifyUrl() {
// then
assertThat(result.getErrors()).isEmpty();
- final String expectedUri = "https://ssp.api.domain/rtb/v2/endpoint?tappxkey=tappxkey&v=1.4&type_cnn=prebid";
+ final String expectedUri = "https://ssp.api.domain/rtb/v2/endpoint?tappxkey=tappxkey&v=1.6&type_cnn=prebid";
assertThat(result.getValue()).hasSize(1)
.allSatisfy(httpRequest -> {
assertThat(httpRequest.getUri()).isEqualTo(expectedUri);
diff --git a/src/test/java/org/prebid/server/it/EquativTest.java b/src/test/java/org/prebid/server/it/EquativTest.java
new file mode 100644
index 00000000000..e3464330edd
--- /dev/null
+++ b/src/test/java/org/prebid/server/it/EquativTest.java
@@ -0,0 +1,36 @@
+package org.prebid.server.it;
+
+import io.restassured.response.Response;
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+import org.prebid.server.model.Endpoint;
+
+import java.io.IOException;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static java.util.Collections.singletonList;
+
+public class EquativTest extends IntegrationTest {
+
+ @Test
+ public void openrtb2AuctionShouldRespondWithBidsFromEquativ() throws IOException, JSONException {
+ // given
+ WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/smartadserver-exchange/api/bid"))
+ .withRequestBody(
+ equalToJson(jsonFrom("openrtb2/equativ/test-equativ-bid-request.json")))
+ .willReturn(aResponse()
+ .withBody(jsonFrom("openrtb2/equativ/test-equativ-bid-response.json"))));
+
+ // when
+ final Response response = responseFor(
+ "openrtb2/equativ/test-auction-equativ-request.json",
+ Endpoint.openrtb2_auction);
+
+ // then
+ assertJsonEquals("openrtb2/equativ/test-auction-equativ-response.json", response,
+ singletonList("equativ"));
+ }
+}
diff --git a/src/test/java/org/prebid/server/it/KuantyxTest.java b/src/test/java/org/prebid/server/it/KuantyxTest.java
new file mode 100644
index 00000000000..853feb95345
--- /dev/null
+++ b/src/test/java/org/prebid/server/it/KuantyxTest.java
@@ -0,0 +1,37 @@
+package org.prebid.server.it;
+
+import io.restassured.response.Response;
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+import org.prebid.server.model.Endpoint;
+
+import java.io.IOException;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static java.util.Collections.singletonList;
+
+public class KuantyxTest extends IntegrationTest {
+
+ @Test
+ public void openrtb2AuctionShouldRespondWithBidsFromKuantyx() throws IOException, JSONException {
+ // given
+ WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/kuantyx-exchange"))
+ .withQueryParam("zid", equalTo("1"))
+ .withRequestBody(equalToJson(
+ jsonFrom("openrtb2/kuantyx/test-kuantyx-bid-request.json")))
+ .willReturn(aResponse().withBody(
+ jsonFrom("openrtb2/kuantyx/test-kuantyx-bid-response.json"))));
+
+ // when
+ final Response response = responseFor("openrtb2/kuantyx/test-auction-kuantyx-request.json",
+ Endpoint.openrtb2_auction);
+
+ // then
+ assertJsonEquals("openrtb2/kuantyx/test-auction-kuantyx-response.json", response,
+ singletonList("kuantyx"));
+ }
+}
diff --git a/src/test/java/org/prebid/server/it/ProgxTest.java b/src/test/java/org/prebid/server/it/ProgxTest.java
new file mode 100644
index 00000000000..6f77d217a86
--- /dev/null
+++ b/src/test/java/org/prebid/server/it/ProgxTest.java
@@ -0,0 +1,35 @@
+package org.prebid.server.it;
+
+import io.restassured.response.Response;
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+import org.prebid.server.model.Endpoint;
+
+import java.io.IOException;
+import java.util.List;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+
+public class ProgxTest extends IntegrationTest {
+
+ @Test
+ public void openrtb2AuctionShouldRespondWithBidsFromProgx() throws IOException, JSONException {
+ // given
+ WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/progx-exchange/connectionId"))
+ .withRequestBody(equalToJson(jsonFrom("openrtb2/progx/test-progx-bid-request.json")))
+ .willReturn(aResponse().withBody(jsonFrom("openrtb2/progx/test-progx-bid-response.json"))));
+
+ // when
+ final Response response = responseFor(
+ "openrtb2/progx/test-auction-progx-request.json",
+ Endpoint.openrtb2_auction
+ );
+
+ // then
+ assertJsonEquals("openrtb2/progx/test-auction-progx-response.json", response, List.of("progx"));
+ }
+
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/equativ/test-auction-equativ-request.json b/src/test/resources/org/prebid/server/it/openrtb2/equativ/test-auction-equativ-request.json
new file mode 100644
index 00000000000..acd98861296
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/equativ/test-auction-equativ-request.json
@@ -0,0 +1,30 @@
+{
+ "id": "request_id",
+ "imp": [
+ {
+ "id": "imp_id",
+ "banner": {
+ "w": 300,
+ "h": 250
+ },
+ "ext": {
+ "prebid": {
+ "bidder": {
+ "equativ": {
+ "siteId": 1,
+ "pageId": 2,
+ "formatId": 3,
+ "networkId": 73
+ }
+ }
+ }
+ }
+ }
+ ],
+ "tmax": 5000,
+ "regs": {
+ "ext": {
+ "gdpr": 0
+ }
+ }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/equativ/test-auction-equativ-response.json b/src/test/resources/org/prebid/server/it/openrtb2/equativ/test-auction-equativ-response.json
new file mode 100644
index 00000000000..04029bdb400
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/equativ/test-auction-equativ-response.json
@@ -0,0 +1,46 @@
+{
+ "id": "request_id",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "bid_id",
+ "impid": "imp_id",
+ "exp": 300,
+ "price": 0.5,
+ "adm": "some-test-ad",
+ "adid": "adid",
+ "adomain": [
+ "advertsite.com"
+ ],
+ "cid": "cid",
+ "crid": "crid",
+ "w": 1024,
+ "h": 576,
+ "ext": {
+ "prebid": {
+ "type": "banner",
+ "meta": {
+ "adaptercode": "equativ"
+ }
+ },
+ "origbidcpm": 0.5
+ },
+ "mtype": 1
+ }
+ ],
+ "seat": "equativ",
+ "group": 0
+ }
+ ],
+ "cur": "USD",
+ "ext": {
+ "responsetimemillis": {
+ "equativ": "{{ equativ.response_time_ms }}"
+ },
+ "prebid": {
+ "auctiontimestamp": 0
+ },
+ "tmaxrequest": 5000
+ }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/equativ/test-equativ-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/equativ/test-equativ-bid-request.json
new file mode 100644
index 00000000000..05fec0b445d
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/equativ/test-equativ-bid-request.json
@@ -0,0 +1,60 @@
+{
+ "id": "request_id",
+ "imp": [
+ {
+ "id": "imp_id",
+ "secure": 1,
+ "banner": {
+ "w": 300,
+ "h": 250
+ },
+ "ext": {
+ "tid": "${json-unit.any-string}",
+ "bidder": {
+ "siteId": 1,
+ "pageId": 2,
+ "formatId": 3,
+ "networkId": 73
+ }
+ }
+ }
+ ],
+ "source": {
+ "tid": "${json-unit.any-string}"
+ },
+ "site": {
+ "domain": "www.example.com",
+ "page": "http://www.example.com",
+ "publisher": {
+ "id": "73",
+ "domain": "example.com"
+ },
+ "ext": {
+ "amp": 0
+ }
+ },
+ "device": {
+ "ua": "userAgent",
+ "ip": "193.168.244.1"
+ },
+ "at": 1,
+ "tmax": "${json-unit.any-number}",
+ "cur": [
+ "USD"
+ ],
+ "regs": {
+ "ext": {
+ "gdpr": 0
+ }
+ },
+ "ext": {
+ "prebid": {
+ "server": {
+ "externalurl": "http://localhost:8080",
+ "gvlid": 1,
+ "datacenter": "local",
+ "endpoint": "/openrtb2/auction"
+ }
+ }
+ }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/equativ/test-equativ-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/equativ/test-equativ-bid-response.json
new file mode 100644
index 00000000000..09d1cc62eb9
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/equativ/test-equativ-bid-response.json
@@ -0,0 +1,24 @@
+{
+ "id": "request_id",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "bid_id",
+ "impid": "imp_id",
+ "price": 0.500000,
+ "adid": "adid",
+ "adm": "some-test-ad",
+ "adomain": [
+ "advertsite.com"
+ ],
+ "cid": "cid",
+ "crid": "crid",
+ "h": 576,
+ "w": 1024,
+ "mtype": 1
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kuantyx/test-auction-kuantyx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/kuantyx/test-auction-kuantyx-request.json
new file mode 100644
index 00000000000..013558bc3d8
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/kuantyx/test-auction-kuantyx-request.json
@@ -0,0 +1,23 @@
+{
+ "id": "request_id",
+ "imp": [
+ {
+ "id": "imp_id",
+ "banner": {
+ "w": 300,
+ "h": 250
+ },
+ "ext": {
+ "kuantyx": {
+ "zone" : 1
+ }
+ }
+ }
+ ],
+ "tmax": 5000,
+ "regs": {
+ "ext": {
+ "gdpr": 0
+ }
+ }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kuantyx/test-auction-kuantyx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kuantyx/test-auction-kuantyx-response.json
new file mode 100644
index 00000000000..97034bb4626
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/kuantyx/test-auction-kuantyx-response.json
@@ -0,0 +1,39 @@
+{
+ "id": "request_id",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "bid_id",
+ "impid": "imp_id",
+ "exp": 300,
+ "price": 4.7,
+ "adm": "adm6_4.7",
+ "nurl": "nurl_4.7",
+ "crid": "crid6",
+ "ext": {
+ "prebid": {
+ "type": "banner",
+ "meta": {
+ "adaptercode": "kuantyx"
+ }
+ },
+ "origbidcpm": 4.7
+ }
+ }
+ ],
+ "seat": "kuantyx",
+ "group": 0
+ }
+ ],
+ "cur": "USD",
+ "ext": {
+ "responsetimemillis": {
+ "kuantyx": "{{ kuantyx.response_time_ms }}"
+ },
+ "prebid": {
+ "auctiontimestamp": 0
+ },
+ "tmaxrequest": 5000
+ }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kuantyx/test-kuantyx-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/kuantyx/test-kuantyx-bid-request.json
new file mode 100644
index 00000000000..a50e33e55e4
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/kuantyx/test-kuantyx-bid-request.json
@@ -0,0 +1,56 @@
+{
+ "id": "request_id",
+ "imp": [
+ {
+ "id": "imp_id",
+ "secure": 1,
+ "banner": {
+ "w": 300,
+ "h": 250
+ },
+ "ext": {
+ "tid": "${json-unit.any-string}",
+ "bidder" : {
+ "zone" : 1
+ }
+ }
+ }
+ ],
+ "source": {
+ "tid": "${json-unit.any-string}"
+ },
+ "site": {
+ "domain": "www.example.com",
+ "page": "http://www.example.com",
+ "publisher": {
+ "domain": "example.com"
+ },
+ "ext": {
+ "amp": 0
+ }
+ },
+ "device": {
+ "ua": "userAgent",
+ "ip": "193.168.244.1"
+ },
+ "at": 1,
+ "tmax": "${json-unit.any-number}",
+ "cur": [
+ "USD"
+ ],
+ "regs": {
+ "ext": {
+ "gdpr": 0
+ }
+ },
+ "ext": {
+ "prebid": {
+ "server": {
+ "externalurl": "http://localhost:8080",
+ "gvlid": 1,
+ "datacenter": "local",
+ "endpoint": "/openrtb2/auction"
+ }
+ }
+ }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kuantyx/test-kuantyx-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kuantyx/test-kuantyx-bid-response.json
new file mode 100644
index 00000000000..665be9472f4
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/kuantyx/test-kuantyx-bid-response.json
@@ -0,0 +1,22 @@
+{
+ "id": "request_id",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "bid_id",
+ "impid": "imp_id",
+ "price": 4.7,
+ "adm": "adm6_${AUCTION_PRICE}",
+ "nurl": "nurl_${AUCTION_PRICE}",
+ "crid": "crid6",
+ "ext": {
+ "prebid": {
+ "type": "banner"
+ }
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/progx/test-auction-progx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/progx/test-auction-progx-request.json
new file mode 100644
index 00000000000..3c416a84304
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/progx/test-auction-progx-request.json
@@ -0,0 +1,24 @@
+{
+ "id": "request_id",
+ "imp": [
+ {
+ "id": "imp_id",
+ "secure": 1,
+ "banner": {
+ "w": 320,
+ "h": 250
+ },
+ "ext": {
+ "progx": {
+ "cId": "connectionId"
+ }
+ }
+ }
+ ],
+ "tmax": 5000,
+ "regs": {
+ "ext": {
+ "gdpr": 0
+ }
+ }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/progx/test-auction-progx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/progx/test-auction-progx-response.json
new file mode 100644
index 00000000000..b7437013277
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/progx/test-auction-progx-response.json
@@ -0,0 +1,40 @@
+{
+ "id": "request_id",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "bid_id",
+ "impid": "imp_id",
+ "exp": 300,
+ "price": 0.01,
+ "adid": "2068416",
+ "cid": "8048",
+ "crid": "24080",
+ "mtype": 1,
+ "ext": {
+ "prebid": {
+ "type": "banner",
+ "meta": {
+ "adaptercode": "progx"
+ }
+ },
+ "origbidcpm": 0.01
+ }
+ }
+ ],
+ "seat": "progx",
+ "group": 0
+ }
+ ],
+ "cur": "USD",
+ "ext": {
+ "responsetimemillis": {
+ "progx": "{{ progx.response_time_ms }}"
+ },
+ "prebid": {
+ "auctiontimestamp": 0
+ },
+ "tmaxrequest": 5000
+ }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/progx/test-progx-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/progx/test-progx-bid-request.json
new file mode 100644
index 00000000000..eb67f5687e5
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/progx/test-progx-bid-request.json
@@ -0,0 +1,54 @@
+{
+ "id": "request_id",
+ "imp": [
+ {
+ "id": "imp_id",
+ "secure": 1,
+ "banner": {
+ "w": 320,
+ "h": 250
+ },
+ "ext": {
+ "tid": "${json-unit.any-string}",
+ "bidder": {
+ "cId": "connectionId"
+ }
+ }
+ }
+ ],
+ "source": {
+ "tid": "${json-unit.any-string}"
+ },
+ "site": {
+ "domain": "www.example.com",
+ "page": "http://www.example.com",
+ "publisher": {
+ "domain": "example.com"
+ },
+ "ext": {
+ "amp": 0
+ }
+ },
+ "device": {
+ "ua": "userAgent",
+ "ip": "193.168.244.1"
+ },
+ "at": 1,
+ "tmax": "${json-unit.any-number}",
+ "cur": [
+ "USD"
+ ],
+ "regs": {
+ "gdpr": 0
+ },
+ "ext": {
+ "prebid": {
+ "server": {
+ "externalurl": "http://localhost:8080",
+ "gvlid": 1,
+ "datacenter": "local",
+ "endpoint": "/openrtb2/auction"
+ }
+ }
+ }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/progx/test-progx-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/progx/test-progx-bid-response.json
new file mode 100644
index 00000000000..47d4f8718ea
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/progx/test-progx-bid-response.json
@@ -0,0 +1,19 @@
+{
+ "id": "tid",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "crid": "24080",
+ "adid": "2068416",
+ "price": 0.01,
+ "id": "bid_id",
+ "impid": "imp_id",
+ "cid": "8048",
+ "mtype": 1
+ }
+ ],
+ "type": "banner"
+ }
+ ]
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json
index 4bb230de7f8..bd97f80820d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json
@@ -25,7 +25,8 @@
}
},
"origbidcpm": 0.5
- }
+ },
+ "mtype": 1
}
],
"seat": "smartadserver",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-response.json
index cee26a03931..09d1cc62eb9 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-response.json
@@ -15,7 +15,8 @@
"cid": "cid",
"crid": "crid",
"h": 576,
- "w": 1024
+ "w": 1024,
+ "mtype": 1
}
]
}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-request.json
index ab61d636585..2ca78608b77 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-request.json
@@ -12,7 +12,8 @@
"tappxkey": "pub-12345-android-9876",
"endpoint": "test",
"bidfloor": 1.5
- }
+ },
+ "gpid": "/19968336/header-bid-tag-0"
}
}
],
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-tappx-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-tappx-bid-request.json
index e575a001420..848e14bc3a8 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-tappx-bid-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-tappx-bid-request.json
@@ -15,7 +15,8 @@
"tappxkey": "pub-12345-android-9876",
"endpoint": "test",
"bidfloor": 1.5
- }
+ },
+ "gpid": "/19968336/header-bid-tag-0"
}
}
],
diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties
index 30a8e1be569..3851b37e2d4 100644
--- a/src/test/resources/org/prebid/server/it/test-application.properties
+++ b/src/test/resources/org/prebid/server/it/test-application.properties
@@ -139,6 +139,8 @@ adapters.aso.aliases.bcmint.enabled=true
adapters.aso.aliases.bcmint.endpoint=http://localhost:8090/bcmint-exchange?zid={{ZoneID}}
adapters.aso.aliases.bidagency.enabled=true
adapters.aso.aliases.bidagency.endpoint=http://localhost:8090/bidagency-exchange?zid={{ZoneID}}
+adapters.aso.aliases.kuantyx.enabled=true
+adapters.aso.aliases.kuantyx.endpoint=http://localhost:8090/kuantyx-exchange?zid={{ZoneID}}
adapters.automatad.enabled=true
adapters.automatad.endpoint=http://localhost:8090/automatad-exchange
adapters.avocet.enabled=true
@@ -375,6 +377,7 @@ adapters.mgidX.enabled=true
adapters.mgidX.endpoint=http://localhost:8090/mgidx-exchange
adapters.minutemedia.enabled=true
adapters.minutemedia.endpoint=http://localhost:8090/minutemedia-exchange?publisherId={{PublisherId}}
+adapters.minutemedia.test-endpoint=http://localhost:8090/minutemedia-exchange?publisherId={{PublisherId}}
adapters.missena.enabled=true
adapters.missena.endpoint=http://localhost:8090/missena-exchange
adapters.mobfoxpb.enabled=true
@@ -484,6 +487,7 @@ adapters.smaato.enabled=true
adapters.smaato.endpoint=http://localhost:8090/smaato-exchange
adapters.smartadserver.enabled=true
adapters.smartadserver.endpoint=http://localhost:8090/smartadserver-exchange
+adapters.smartadserver.aliases.equativ.enabled=true
adapters.smartrtb.enabled=true
adapters.smartrtb.endpoint=http://localhost:8090/smartrtb-exchange/
adapters.smartx.enabled=true
@@ -594,6 +598,8 @@ adapters.xeworks.aliases.connektai.enabled=true
adapters.xeworks.aliases.connektai.endpoint=http://localhost:8090/connektai-exchange
adapters.vidazoo.enabled=true
adapters.vidazoo.endpoint=http://localhost:8090/vidazoo-exchange/
+adapters.vidazoo.aliases.progx.enabled=true
+adapters.vidazoo.aliases.progx.endpoint=http://localhost:8090/progx-exchange/
adapters.videobyte.enabled=true
adapters.videobyte.endpoint=http://localhost:8090/videobyte-exchange
adapters.videoheroes.enabled=true