diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml
index 9eb4d2aaf33..4a5ca3144ed 100644
--- a/extra/bundle/pom.xml
+++ b/extra/bundle/pom.xml
@@ -55,6 +55,13 @@
pb-request-correction
${project.version}
+
diff --git a/extra/modules/WURFL-devicedetection/README.md b/extra/modules/WURFL-devicedetection/README.md
new file mode 100644
index 00000000000..68f0fcb59ca
--- /dev/null
+++ b/extra/modules/WURFL-devicedetection/README.md
@@ -0,0 +1,255 @@
+## 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 database, 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).
+ - **pixelratio**: Screen pixel density ratio.
+ - **devicetype**: Device type (e.g., mobile, tablet, desktop).
+ - **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 `getAccount()` method in the `AuctionContext` class.
+
+
+### Build prerequisites
+
+To build the WURFL module, you need to download the WURFL Onsite Java API (both JAR and POM files)
+from the ScientiaMobile private repository and install it in your local Maven repository.
+Access to the WURFL Onsite Java API repository requires a valid ScientiaMobile WURFL license.
+For more details, visit: [ScientiaMobile WURFL Onsite API for Java](https://www.scientiamobile.com/secondary-products/wurfl-onsite-api-for-java/).
+
+Run the following command to install the WURFL API:
+
+```bash
+mvn install:install-file \
+ -Dfile= \
+ -DgroupId=com.scientiamobile.wurfl \
+ -DartifactId=wurfl \
+ -Dversion= \
+ -Dpackaging=jar \
+ -DpomFile=
+```
+
+### Activating the WURFL Module
+
+The WURFL module is disabled by default. Building the Prebid Server Java with the default bundle option
+does not include the WURFL module in the server's JAR file.
+
+To include the WURFL module in the Prebid Server Java bundle, follow these steps:
+
+1. Uncomment the WURFL Java API dependency in `extra/modules/WURFL-devicedetection/pom.xml`.
+2. Uncomment the WURFL module dependency in `extra/bundle/pom.xml`.
+3. Uncomment the WURFL module name in the module list in `extra/modules/pom.xml`.
+
+After making these changes, you can 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:
+ wurfl-file-dir-path:
+ wurfl-snapshot-url: https://data.scientiamobile.com//wurfl.zip
+ cache-size: 200000
+ wurfl-run-updater: true
+ allowed-publisher-ids: 1
+ ext-caps: false
+```
+
+### Configuration Options
+
+| Parameter | Requirement | Description |
+|---------------------------|-------------|-------------------------------------------------------------------------------------------------------|
+| **`wurfl-file-dir-path`** | Mandatory | Path to the directory where the WURFL file is downloaded. Directory must exist and be writable. |
+| **`wurfl-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. |
+| **`wurfl-run-updater`** | Optional | Enables the WURFL updater. Defaults to no updates. |
+| **`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. All the other module configuration options
+are located in the `WURFL-devicedetection.yaml` inside the module.
+
+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..da087b44cca
--- /dev/null
+++ b/extra/modules/WURFL-devicedetection/pom.xml
@@ -0,0 +1,29 @@
+
+
+ 4.0.0
+
+
+ org.prebid.server.hooks.modules
+ all-modules
+ 3.19.0-SNAPSHOT
+
+
+ wurfl-devicedetection
+
+ wurfl-devicedetection
+ WURFL device detection and data enrichment module
+
+
+ 1.13.2.1
+
+
+
+
+
+
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..e9d60e6880f
--- /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,51 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config;
+
+import lombok.Data;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1.WURFLDeviceDetectionModule;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.List;
+import java.util.Set;
+
+@ConfigurationProperties(prefix = "hooks.modules." + WURFLDeviceDetectionModule.CODE)
+@Data
+
+public class WURFLDeviceDetectionConfigProperties {
+
+ public 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 final Set REQUIRED_VIRTUAL_CAPS = Set.of(
+
+ "advertised_device_os",
+ "advertised_device_os_version",
+ "complete_device_name",
+ "is_full_desktop",
+ "is_mobile",
+ "is_phone",
+ "form_factor",
+ "pixel_density"
+ );
+
+ int cacheSize;
+
+ String wurflFileDirPath;
+
+ String wurflSnapshotUrl;
+
+ boolean extCaps;
+
+ boolean wurflRunUpdater = true;
+
+ List allowedPublisherIds = List.of();
+}
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..770ae1cd88f
--- /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,37 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config;
+
+import com.scientiamobile.wurfl.core.WURFLEngine;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.WURFLEngineInitializer;
+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.spring.env.YamlPropertySourceFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+
+import java.util.List;
+
+@ConditionalOnProperty(prefix = "hooks." + WURFLDeviceDetectionModule.CODE, name = "enabled", havingValue = "true")
+@Configuration
+@PropertySource(
+ value = "classpath:/module-config/WURFL-devicedetection.yaml",
+ factory = YamlPropertySourceFactory.class)
+@EnableConfigurationProperties(WURFLDeviceDetectionConfigProperties.class)
+public class WURFLDeviceDetectionConfiguration {
+
+ @Bean
+ public WURFLDeviceDetectionModule wurflDeviceDetectionModule(WURFLDeviceDetectionConfigProperties
+ configProperties) {
+
+ final WURFLEngine wurflEngine = WURFLEngineInitializer.builder()
+ .configProperties(configProperties)
+ .build().initWURFLEngine();
+ wurflEngine.load();
+
+ return new WURFLDeviceDetectionModule(List.of(new WURFLDeviceDetectionEntrypointHook(),
+ new WURFLDeviceDetectionRawAuctionRequestHook(wurflEngine, configProperties)));
+ }
+}
diff --git a/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/exc/WURFLModuleConfigurationException.java b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/exc/WURFLModuleConfigurationException.java
new file mode 100644
index 00000000000..d2c767c45c6
--- /dev/null
+++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/exc/WURFLModuleConfigurationException.java
@@ -0,0 +1,8 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.exc;
+
+public class WURFLModuleConfigurationException extends RuntimeException {
+
+ public WURFLModuleConfigurationException(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..95a32fbfabe
--- /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,31 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model;
+
+import lombok.Getter;
+import org.prebid.server.model.CaseInsensitiveMultiMap;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+@Getter
+public class AuctionRequestHeadersContext {
+
+ Map headers;
+
+ private AuctionRequestHeadersContext(Map headers) {
+ this.headers = headers;
+ }
+
+ public static AuctionRequestHeadersContext from(final CaseInsensitiveMultiMap headers) {
+ final Map headersMap = new HashMap<>();
+ if (Objects.isNull(headers)) {
+ return new AuctionRequestHeadersContext(headersMap);
+ }
+
+ for (String headerName : headers.names()) {
+ headersMap.put(headerName, headers.getAll(headerName).getFirst());
+ }
+ return new AuctionRequestHeadersContext(headersMap);
+ }
+
+}
diff --git a/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineInitializer.java b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineInitializer.java
new file mode 100644
index 00000000000..45e288dad33
--- /dev/null
+++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineInitializer.java
@@ -0,0 +1,112 @@
+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 com.scientiamobile.wurfl.core.updater.Frequency;
+import com.scientiamobile.wurfl.core.updater.WURFLUpdater;
+import lombok.Builder;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.exc.WURFLModuleConfigurationException;
+
+import java.net.URI;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Builder
+public class WURFLEngineInitializer {
+
+ private WURFLDeviceDetectionConfigProperties configProperties;
+
+ public WURFLEngine initWURFLEngine() {
+ downloadWurflFile(configProperties);
+ final WURFLEngine engine = initializeEngine(configProperties);
+ setupUpdater(configProperties, engine);
+ return engine;
+ }
+
+ static void downloadWurflFile(WURFLDeviceDetectionConfigProperties configProperties) {
+ if (StringUtils.isNotBlank(configProperties.getWurflSnapshotUrl())
+ && StringUtils.isNotBlank(configProperties.getWurflFileDirPath())) {
+ GeneralWURFLEngine.wurflDownload(
+ configProperties.getWurflSnapshotUrl(),
+ configProperties.getWurflFileDirPath());
+ }
+ }
+
+ static WURFLEngine initializeEngine(WURFLDeviceDetectionConfigProperties configProperties) {
+
+ final String wurflFileName = extractWURFLFileName(configProperties.getWurflSnapshotUrl());
+
+ final Path wurflPath = Paths.get(
+ configProperties.getWurflFileDirPath(),
+ wurflFileName
+ );
+ final WURFLEngine engine = new GeneralWURFLEngine(wurflPath.toString());
+ verifyStaticCapabilitiesDefinition(engine);
+
+ if (configProperties.getCacheSize() > 0) {
+ engine.setCacheProvider(new LRUMapCacheProvider(configProperties.getCacheSize()));
+ } else {
+ engine.setCacheProvider(new NullCacheProvider());
+ }
+ return engine;
+ }
+
+ private 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);
+ }
+ }
+
+ static void verifyStaticCapabilitiesDefinition(WURFLEngine engine) {
+
+ final List unsupportedStaticCaps = new ArrayList<>();
+ final Map allCaps = engine.getAllCapabilities().stream()
+ .collect(Collectors.toMap(
+ key -> key,
+ value -> true
+ ));
+
+ for (String requiredCapName : WURFLDeviceDetectionConfigProperties.REQUIRED_STATIC_CAPS) {
+ if (!allCaps.containsKey(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 WURFLModuleConfigurationException(failedCheckMessage);
+ }
+
+ }
+
+ static void setupUpdater(WURFLDeviceDetectionConfigProperties configProperties, WURFLEngine engine) {
+ final boolean runUpdater = configProperties.isWurflRunUpdater();
+
+ if (runUpdater) {
+ final WURFLUpdater updater = new WURFLUpdater(engine, configProperties.getWurflSnapshotUrl());
+ updater.setFrequency(Frequency.DAILY);
+ updater.performPeriodicUpdate();
+ }
+ }
+}
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..5051b47a9dd
--- /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,132 @@
+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 lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.MapUtils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Slf4j
+public class HeadersResolver {
+
+ static final String SEC_CH_UA = "Sec-CH-UA";
+ static final String SEC_CH_UA_PLATFORM = "Sec-CH-UA-Platform";
+ static final String SEC_CH_UA_PLATFORM_VERSION = "Sec-CH-UA-Platform-Version";
+ static final String SEC_CH_UA_MOBILE = "Sec-CH-UA-Mobile";
+ static final String SEC_CH_UA_ARCH = "Sec-CH-UA-Arch";
+ static final String SEC_CH_UA_MODEL = "Sec-CH-UA-Model";
+ static final String SEC_CH_UA_FULL_VERSION_LIST = "Sec-CH-UA-Full-Version-List";
+ static final String USER_AGENT = "User-Agent";
+
+ public Map resolve(final Device device, final Map headers) {
+
+ if (Objects.isNull(device) && Objects.isNull(headers)) {
+ return new HashMap<>();
+ }
+
+ final Map resolvedHeaders = resolveFromOrtbDevice(device);
+ if (MapUtils.isEmpty(resolvedHeaders)) {
+ return headers;
+ }
+
+ return resolvedHeaders;
+ }
+
+ private Map resolveFromOrtbDevice(Device device) {
+
+ final Map resolvedHeaders = new HashMap<>();
+
+ if (Objects.isNull(device)) {
+ log.warn("ORBT Device is null");
+ return resolvedHeaders;
+ }
+
+ if (Objects.nonNull(device.getUa())) {
+ resolvedHeaders.put(USER_AGENT, device.getUa());
+ }
+
+ resolvedHeaders.putAll(resolveFromSua(device.getSua()));
+ return resolvedHeaders;
+ }
+
+ private Map resolveFromSua(UserAgent sua) {
+
+ final Map headers = new HashMap<>();
+
+ if (Objects.isNull(sua)) {
+ log.warn("Sua is null, returning empty headers");
+ return new HashMap<>();
+ }
+
+ // Browser brands and versions
+ final List brands = sua.getBrowsers();
+ if (CollectionUtils.isEmpty(brands)) {
+ log.warn("No browser brands and versions found");
+ return headers;
+ }
+
+ final String brandList = brandListAsString(brands);
+ headers.put(SEC_CH_UA, brandList);
+ headers.put(SEC_CH_UA_FULL_VERSION_LIST, brandList);
+
+ // Platform
+ final PlatformNameVersion platformNameVersion = PlatformNameVersion.from(sua.getPlatform());
+ if (Objects.nonNull(platformNameVersion)) {
+ headers.put(SEC_CH_UA_PLATFORM, escape(platformNameVersion.getPlatformName()));
+ headers.put(SEC_CH_UA_PLATFORM_VERSION, escape(platformNameVersion.getPlatformVersion()));
+ }
+
+ // Model
+ final String model = sua.getModel();
+ if (Objects.nonNull(model) && !model.isEmpty()) {
+ headers.put(SEC_CH_UA_MODEL, escape(model));
+ }
+
+ // Architecture
+ final String arch = sua.getArchitecture();
+ if (Objects.nonNull(arch) && !arch.isEmpty()) {
+ headers.put(SEC_CH_UA_ARCH, escape(arch));
+ }
+
+ // Mobile
+ final Integer mobile = sua.getMobile();
+ if (Objects.nonNull(mobile)) {
+ headers.put(SEC_CH_UA_MOBILE, "?" + mobile);
+ }
+ return headers;
+ }
+
+ private String brandListAsString(List versions) {
+
+ final String brandNameString = versions.stream()
+ .filter(brandVersion -> brandVersion.getBrand() != null)
+ .map(brandVersion -> {
+ final String brandName = escape(brandVersion.getBrand());
+ final String versionString = versionFromTokens(brandVersion.getVersion());
+ return brandName + ";v=\"" + versionString + "\"";
+ })
+ .collect(Collectors.joining(", "));
+ return brandNameString;
+ }
+
+ private static String escape(String value) {
+ return '"' + value.replace("\"", "\\\"") + '"';
+ }
+
+ public static String versionFromTokens(List tokens) {
+ if (tokens == null || tokens.isEmpty()) {
+ return "";
+ }
+
+ return tokens.stream()
+ .filter(token -> token != null && !token.isEmpty())
+ .collect(Collectors.joining("."));
+ }
+}
diff --git a/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/PlatformNameVersion.java b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/PlatformNameVersion.java
new file mode 100644
index 00000000000..dc01aa127b2
--- /dev/null
+++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/PlatformNameVersion.java
@@ -0,0 +1,33 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.resolver;
+
+import com.iab.openrtb.request.BrandVersion;
+import lombok.Getter;
+
+import java.util.Objects;
+
+public class PlatformNameVersion {
+
+ @Getter
+ private String platformName;
+
+ private String platformVersion;
+
+ public static PlatformNameVersion from(BrandVersion platform) {
+ if (Objects.isNull(platform)) {
+ return null;
+ }
+ final PlatformNameVersion platformNameVersion = new PlatformNameVersion();
+ platformNameVersion.platformName = platform.getBrand();
+ platformNameVersion.platformVersion = HeadersResolver.versionFromTokens(platform.getVersion());
+ return platformNameVersion;
+ }
+
+ public String getPlatformVersion() {
+ return platformVersion;
+ }
+
+ public String toString() {
+ return platformName + " " + platformVersion;
+ }
+
+}
diff --git a/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/AccountValidator.java b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/AccountValidator.java
new file mode 100644
index 00000000000..0d930479fab
--- /dev/null
+++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/AccountValidator.java
@@ -0,0 +1,31 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.auction.model.AuctionContext;
+import org.prebid.server.settings.model.Account;
+import lombok.Builder;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Map;
+
+@Slf4j
+@Builder
+public class AccountValidator {
+
+ Map allowedPublisherIds;
+ AuctionContext auctionContext;
+
+ public boolean isAccountValid() {
+
+ return Optional.ofNullable(auctionContext)
+ .map(AuctionContext::getAccount)
+ .map(Account::getId)
+ .filter(StringUtils::isNotBlank)
+ .map(allowedPublisherIds::get)
+ .filter(Objects::nonNull)
+ .isPresent();
+ }
+
+}
diff --git a/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/ExtWURFLMapper.java b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/ExtWURFLMapper.java
new file mode 100644
index 00000000000..33500ebfce6
--- /dev/null
+++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/ExtWURFLMapper.java
@@ -0,0 +1,65 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.Builder;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+@Builder
+@Slf4j
+public class ExtWURFLMapper {
+
+ private final List staticCaps;
+ private final List virtualCaps;
+ private boolean addExtCaps;
+ private final com.scientiamobile.wurfl.core.Device wurflDevice;
+ private static final String NULL_VALUE_TOKEN = "$null$";
+ private static final String WURFL_ID_PROPERTY = "wurfl_id";
+
+ public JsonNode mapExtProperties() {
+
+ final ObjectMapper objectMapper = new ObjectMapper();
+ final ObjectNode wurflNode = objectMapper.createObjectNode();
+
+ try {
+ wurflNode.put(WURFL_ID_PROPERTY, wurflDevice.getId());
+
+ if (addExtCaps) {
+
+ staticCaps.stream()
+ .map(sc -> {
+ try {
+ return Map.entry(sc, wurflDevice.getCapability(sc));
+ } catch (Exception e) {
+ log.error("Error getting capability for {}: {}", sc, e.getMessage());
+ return Map.entry(sc, NULL_VALUE_TOKEN);
+ }
+ })
+ .filter(entry -> Objects.nonNull(entry.getValue())
+ && !NULL_VALUE_TOKEN.equals(entry.getValue()))
+ .forEach(entry -> wurflNode.put(entry.getKey(), entry.getValue()));
+
+ virtualCaps.stream()
+ .map(vc -> Map.entry(vc, wurflDevice.getVirtualCapability(vc)))
+ .filter(entry -> Objects.nonNull(entry.getValue()))
+ .forEach(entry -> wurflNode.put(entry.getKey(), entry.getValue()));
+ }
+ } catch (Exception e) {
+ log.error("Exception while updating EXT");
+ }
+
+ JsonNode node = null;
+ try {
+ node = objectMapper.readTree(wurflNode.toString());
+ } catch (JsonProcessingException e) {
+ log.error("Error creating WURFL ext device JSON: {}", e.getMessage());
+ }
+ return node;
+ }
+}
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..f8e9a9259c5
--- /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,259 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1;
+
+import com.iab.openrtb.request.Device;
+import com.scientiamobile.wurfl.core.exc.CapabilityNotDefinedException;
+import com.scientiamobile.wurfl.core.exc.VirtualCapabilityNotDefinedException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.model.UpdateResult;
+import org.prebid.server.proto.openrtb.ext.request.ExtDevice;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+@Slf4j
+public class OrtbDeviceUpdater {
+
+ private static final String WURFL_PROPERTY = "wurfl";
+ private static final Map NAME_TO_IS_VIRTUAL_CAPABILITY = Map.of(
+ "brand_name", false,
+ "model_name", false,
+ "resolution_width", false,
+ "resolution_height", false,
+ "advertised_device_os", true,
+ "advertised_device_os_version", true,
+ "pixel_density", true,
+ "density_class", false,
+ "ajax_support_javascript", false
+ );
+
+ public Device update(Device ortbDevice, com.scientiamobile.wurfl.core.Device wurflDevice,
+ List staticCaps, List virtualCaps, boolean addExtCaps) {
+
+ final Device.DeviceBuilder deviceBuilder = ortbDevice.toBuilder();
+
+ // make
+ final UpdateResult updatedMake = tryUpdateStringField(ortbDevice.getMake(), wurflDevice,
+ "brand_name");
+ if (updatedMake.isUpdated()) {
+ deviceBuilder.make(updatedMake.getValue());
+ }
+
+ // model
+ final UpdateResult updatedModel = tryUpdateStringField(ortbDevice.getModel(), wurflDevice,
+ "model_name");
+ if (updatedModel.isUpdated()) {
+ deviceBuilder.model(updatedModel.getValue());
+ }
+
+ // deviceType
+ final UpdateResult updatedDeviceType = tryUpdateDeviceTypeField(ortbDevice.getDevicetype(),
+ getOrtb2DeviceType(wurflDevice));
+ if (updatedDeviceType.isUpdated()) {
+ deviceBuilder.devicetype(updatedDeviceType.getValue());
+ }
+
+ // os
+ final UpdateResult updatedOS = tryUpdateStringField(ortbDevice.getOs(), wurflDevice,
+ "advertised_device_os");
+ if (updatedOS.isUpdated()) {
+ deviceBuilder.os(updatedOS.getValue());
+ }
+
+ // os version
+ final UpdateResult updatedOsv = tryUpdateStringField(ortbDevice.getOsv(), wurflDevice,
+ "advertised_device_os_version");
+ if (updatedOS.isUpdated()) {
+ deviceBuilder.osv(updatedOsv.getValue());
+ }
+
+ // h (resolution height)
+ final UpdateResult updatedH = tryUpdateIntegerField(ortbDevice.getH(), wurflDevice,
+ "resolution_height", false);
+ if (updatedH.isUpdated()) {
+ deviceBuilder.h(updatedH.getValue());
+ }
+
+ // w (resolution height)
+ final UpdateResult updatedW = tryUpdateIntegerField(ortbDevice.getW(), wurflDevice,
+ "resolution_width", false);
+ if (updatedW.isUpdated()) {
+ deviceBuilder.w(updatedW.getValue());
+ }
+
+ // Pixels per inch
+ final UpdateResult updatedPpi = tryUpdateIntegerField(ortbDevice.getPpi(), wurflDevice,
+ "pixel_density", false);
+ if (updatedPpi.isUpdated()) {
+ deviceBuilder.ppi(updatedPpi.getValue());
+ }
+
+ // Pixel ratio
+ final UpdateResult updatedPxRatio = tryUpdateBigDecimalField(ortbDevice.getPxratio(), wurflDevice,
+ "density_class");
+ if (updatedPxRatio.isUpdated()) {
+ deviceBuilder.pxratio(updatedPxRatio.getValue());
+ }
+
+ // Javascript support
+ final UpdateResult updatedJs = tryUpdateIntegerField(ortbDevice.getJs(), wurflDevice,
+ "ajax_support_javascript", true);
+ if (updatedJs.isUpdated()) {
+ deviceBuilder.js(updatedJs.getValue());
+ }
+
+ // Ext
+ final ExtWURFLMapper extMapper = ExtWURFLMapper.builder()
+ .wurflDevice(wurflDevice)
+ .staticCaps(staticCaps)
+ .virtualCaps(virtualCaps)
+ .addExtCaps(addExtCaps)
+ .build();
+ final ExtDevice updatedExt = ExtDevice.empty();
+ final ExtDevice ortbDeviceExt = ortbDevice.getExt();
+
+ if (Objects.nonNull(ortbDeviceExt)) {
+ updatedExt.addProperties(ortbDeviceExt.getProperties());
+ if (!ortbDeviceExt.containsProperty(WURFL_PROPERTY)) {
+ updatedExt.addProperty("wurfl", extMapper.mapExtProperties());
+ }
+ } else {
+ updatedExt.addProperty("wurfl", extMapper.mapExtProperties());
+ }
+ deviceBuilder.ext(updatedExt);
+ return deviceBuilder.build();
+ }
+
+ private UpdateResult tryUpdateStringField(String fromOrtbDevice,
+ com.scientiamobile.wurfl.core.Device wurflDevice,
+ String capName) {
+ if (StringUtils.isNotBlank(fromOrtbDevice)) {
+ return UpdateResult.unaltered(fromOrtbDevice);
+ }
+
+ final String fromWurfl = isVirtualCapability(capName)
+ ? wurflDevice.getVirtualCapability(capName)
+ : wurflDevice.getCapability(capName);
+
+ if (Objects.nonNull(fromWurfl)) {
+ return UpdateResult.updated(fromWurfl);
+ }
+
+ return UpdateResult.unaltered(fromOrtbDevice);
+ }
+
+ private UpdateResult tryUpdateIntegerField(Integer fromOrtbDevice,
+ com.scientiamobile.wurfl.core.Device wurflDevice,
+ String capName, boolean convertFromBool) {
+ if (Objects.nonNull(fromOrtbDevice)) {
+ return UpdateResult.unaltered(fromOrtbDevice);
+ }
+
+ final String fromWurfl = isVirtualCapability(capName)
+ ? wurflDevice.getVirtualCapability(capName)
+ : wurflDevice.getCapability(capName);
+
+ if (StringUtils.isNotBlank(fromWurfl)) {
+
+ if (convertFromBool) {
+ return fromWurfl.equalsIgnoreCase("true")
+ ? UpdateResult.updated(1)
+ : UpdateResult.updated(0);
+ }
+
+ return UpdateResult.updated(Integer.parseInt(fromWurfl));
+ }
+ return UpdateResult.unaltered(fromOrtbDevice);
+ }
+
+ private UpdateResult tryUpdateBigDecimalField(BigDecimal fromOrtbDevice,
+ com.scientiamobile.wurfl.core.Device wurflDevice,
+ String capName) {
+
+ if (Objects.nonNull(fromOrtbDevice)) {
+ return UpdateResult.unaltered(fromOrtbDevice);
+ }
+
+ final String fromWurfl = isVirtualCapability(capName)
+ ? wurflDevice.getVirtualCapability(capName)
+ : wurflDevice.getCapability(capName);
+
+ if (Objects.nonNull(fromWurfl)) {
+
+ BigDecimal pxRatio = null;
+ try {
+ pxRatio = new BigDecimal(fromWurfl);
+ return UpdateResult.updated(pxRatio);
+ } catch (NullPointerException e) {
+ log.warn("Cannot convert WURFL device pixel density {} to ortb device pxratio", pxRatio);
+ }
+ }
+
+ return UpdateResult.unaltered(fromOrtbDevice);
+ }
+
+ private boolean isVirtualCapability(String capName) {
+ return NAME_TO_IS_VIRTUAL_CAPABILITY.get(capName);
+ }
+
+ private UpdateResult tryUpdateDeviceTypeField(Integer fromOrtbDevice, Integer fromWurfl) {
+ final boolean isNotNullAndPositive = Objects.nonNull(fromOrtbDevice) && fromOrtbDevice > 0;
+ if (isNotNullAndPositive) {
+ return UpdateResult.unaltered(fromOrtbDevice);
+ }
+
+ if (Objects.nonNull(fromWurfl)) {
+ return UpdateResult.updated(fromWurfl);
+ }
+
+ return UpdateResult.unaltered(fromOrtbDevice);
+ }
+
+ public static Integer getOrtb2DeviceType(final com.scientiamobile.wurfl.core.Device wurflDevice) {
+
+ final boolean isPhone;
+ final boolean isTablet;
+
+ if (wurflDevice.getVirtualCapabilityAsBool("is_mobile")) {
+ // if at least one if these capabilities is not defined the mobile device type is undefined
+ try {
+ isPhone = wurflDevice.getVirtualCapabilityAsBool("is_phone");
+ isTablet = wurflDevice.getCapabilityAsBool("is_tablet");
+ } catch (CapabilityNotDefinedException | VirtualCapabilityNotDefinedException e) {
+ return null;
+ }
+
+ if (isPhone || isTablet) {
+ return 1;
+ }
+ return 6;
+ }
+
+ // desktop device
+ if (wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")) {
+ return 2;
+ }
+
+ // connected tv
+ 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;
+ }
+
+ return null; // Return null for undefined device type
+ }
+
+}
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..ccca8c4038b
--- /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,37 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1;
+
+import lombok.extern.slf4j.Slf4j;
+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;
+
+@Slf4j
+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..3bdaceff99f
--- /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..1824b4e520c
--- /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,143 @@
+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 com.scientiamobile.wurfl.core.exc.CapabilityNotDefinedException;
+import com.scientiamobile.wurfl.core.exc.VirtualCapabilityNotDefinedException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.MapUtils;
+import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl;
+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 io.vertx.core.Future;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Slf4j
+public class WURFLDeviceDetectionRawAuctionRequestHook implements RawAuctionRequestHook {
+
+ public static final String CODE = "wurfl-devicedetection-raw-auction-request";
+
+ private final WURFLEngine wurflEngine;
+ private final List staticCaps;
+ private final List virtualCaps;
+ private final OrtbDeviceUpdater ortbDeviceUpdater;
+ private final Map allowedPublisherIDs;
+ private final boolean addExtCaps;
+
+ public WURFLDeviceDetectionRawAuctionRequestHook(WURFLEngine wurflEngine,
+ WURFLDeviceDetectionConfigProperties configProperties) {
+ this.wurflEngine = wurflEngine;
+ this.staticCaps = wurflEngine.getAllCapabilities().stream().toList();
+ this.virtualCaps = safeGetVirtualCaps(wurflEngine);
+ this.ortbDeviceUpdater = new OrtbDeviceUpdater();
+ this.addExtCaps = configProperties.isExtCaps();
+ this.allowedPublisherIDs = configProperties.getAllowedPublisherIds().stream()
+ .collect(Collectors.toMap(item -> item, item -> item));
+ }
+
+ private List safeGetVirtualCaps(WURFLEngine wurflEngine) {
+ final List allVcaps = wurflEngine.getAllVirtualCapabilities().stream().toList();
+ final List safeVcaps = new ArrayList<>();
+ final var device = wurflEngine.getDeviceById("generic");
+ allVcaps.forEach(vc -> {
+ try {
+ device.getVirtualCapability(vc);
+ safeVcaps.add(vc);
+ } catch (VirtualCapabilityNotDefinedException | CapabilityNotDefinedException ignored) { }
+ });
+ return safeVcaps;
+ }
+
+ @Override
+ public Future> call(AuctionRequestPayload auctionRequestPayload,
+ AuctionInvocationContext invocationContext) {
+ if (!shouldEnrichDevice(invocationContext)) {
+ return noUpdateResultFuture();
+ }
+
+ final BidRequest bidRequest = auctionRequestPayload.bidRequest();
+ Device ortbDevice = null;
+ if (bidRequest == null) {
+ log.warn("BidRequest is null");
+ return noUpdateResultFuture();
+ } else {
+ ortbDevice = bidRequest.getDevice();
+ if (ortbDevice == null) {
+ log.warn("Device is null");
+ return noUpdateResultFuture();
+ }
+ }
+
+ final AuctionRequestHeadersContext headersContext;
+ Map requestHeaders = null;
+ if (invocationContext.moduleContext() instanceof AuctionRequestHeadersContext) {
+ headersContext = (AuctionRequestHeadersContext) invocationContext.moduleContext();
+ if (headersContext != null) {
+ requestHeaders = headersContext.getHeaders();
+ }
+
+ final Map headers = new HeadersResolver().resolve(ortbDevice, requestHeaders);
+ final com.scientiamobile.wurfl.core.Device wurflDevice = wurflEngine.getDeviceForRequest(headers);
+
+ try {
+ final Device updatedDevice = ortbDeviceUpdater.update(ortbDevice, wurflDevice, staticCaps,
+ virtualCaps, addExtCaps);
+ return Future.succeededFuture(
+ InvocationResultImpl.builder()
+ .status(InvocationStatus.success)
+ .action(InvocationAction.update)
+ .payloadUpdate(payload ->
+ AuctionRequestPayloadImpl.of(bidRequest.toBuilder()
+ .device(updatedDevice)
+ .build()))
+ .build()
+ );
+ } catch (Exception e) {
+ log.error("Exception " + e.getMessage());
+ }
+
+ }
+
+ return noUpdateResultFuture();
+ }
+
+ private static Future> noUpdateResultFuture() {
+ return Future.succeededFuture(
+ InvocationResultImpl.builder()
+ .status(InvocationStatus.success)
+ .action(InvocationAction.no_action)
+ .build());
+ }
+
+ private boolean shouldEnrichDevice(AuctionInvocationContext invocationContext) {
+ if (MapUtils.isEmpty(allowedPublisherIDs)) {
+ return true;
+ }
+
+ final AuctionContext auctionContext = invocationContext.auctionContext();
+ return AccountValidator.builder().allowedPublisherIds(allowedPublisherIDs)
+ .auctionContext(auctionContext)
+ .build()
+ .isAccountValid();
+ }
+
+ @Override
+ public String code() {
+ return CODE;
+ }
+
+}
diff --git a/extra/modules/WURFL-devicedetection/src/main/resources/module-config/WURFL-devicedetection.yaml b/extra/modules/WURFL-devicedetection/src/main/resources/module-config/WURFL-devicedetection.yaml
new file mode 100644
index 00000000000..e8c4f2a5229
--- /dev/null
+++ b/extra/modules/WURFL-devicedetection/src/main/resources/module-config/WURFL-devicedetection.yaml
@@ -0,0 +1,46 @@
+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:
+ wurfl-file-dir-path:
+ wurfl-snapshot-url: https://data.scientiamobile.com//wurfl.zip
+ cache-size: 200000
+ wurfl-run-updater: true
+ allowed-publisher-ids: 1
+ ext-caps: false
diff --git a/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfigPropertiesTest.java b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfigPropertiesTest.java
new file mode 100644
index 00000000000..dcce5123e34
--- /dev/null
+++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfigPropertiesTest.java
@@ -0,0 +1,46 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class WURFLDeviceDetectionConfigPropertiesTest {
+
+ @Test
+ void shouldInitializeWithEmptyValues() {
+ // given
+ final WURFLDeviceDetectionConfigProperties properties = new WURFLDeviceDetectionConfigProperties();
+
+ // then
+ assertThat(properties.getCacheSize()).isEqualTo(0);
+ assertThat(properties.getWurflFileDirPath()).isNull();
+ assertThat(properties.getWurflSnapshotUrl()).isNull();
+ assertThat(properties.isExtCaps()).isFalse();
+ assertThat(properties.isWurflRunUpdater()).isTrue();
+ }
+
+ @Test
+ void shouldSetAndGetProperties() {
+ // given
+ final WURFLDeviceDetectionConfigProperties properties = new WURFLDeviceDetectionConfigProperties();
+
+ // when
+ properties.setCacheSize(1000);
+ properties.setWurflFileDirPath("/path/to/file");
+
+ properties.setWurflSnapshotUrl("https://example-scientiamobile.com/wurfl.zip");
+ properties.setWurflRunUpdater(false);
+ properties.setAllowedPublisherIds(List.of("1", "3"));
+ properties.setExtCaps(true);
+
+ // then
+ assertThat(properties.getCacheSize()).isEqualTo(1000);
+ assertThat(properties.getWurflFileDirPath()).isEqualTo("/path/to/file");
+ assertThat(properties.getWurflSnapshotUrl()).isEqualTo("https://example-scientiamobile.com/wurfl.zip");
+ assertThat(properties.isWurflRunUpdater()).isEqualTo(false);
+ assertThat(properties.getAllowedPublisherIds()).isEqualTo(List.of("1", "3"));
+ assertThat(properties.isExtCaps()).isTrue();
+ }
+}
diff --git a/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/mock/WURFLDeviceMock.java b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/mock/WURFLDeviceMock.java
new file mode 100644
index 00000000000..a3280fe7d42
--- /dev/null
+++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/mock/WURFLDeviceMock.java
@@ -0,0 +1,282 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock;
+
+import com.scientiamobile.wurfl.core.exc.CapabilityNotDefinedException;
+import com.scientiamobile.wurfl.core.exc.VirtualCapabilityNotDefinedException;
+import com.scientiamobile.wurfl.core.matchers.MatchType;
+import lombok.Builder;
+
+import java.util.Map;
+
+@Builder
+public class WURFLDeviceMock implements com.scientiamobile.wurfl.core.Device {
+
+ private Map capabilities;
+ private String id;
+ private Map virtualCapabilities;
+
+ @Override
+ public MatchType getMatchType() {
+ return MatchType.conclusive;
+ }
+
+ @Override
+ public String getVirtualCapability(String vcapName) throws VirtualCapabilityNotDefinedException,
+ CapabilityNotDefinedException {
+
+ if (!virtualCapabilities.containsKey(vcapName)) {
+ throw new VirtualCapabilityNotDefinedException(vcapName);
+ }
+
+ return virtualCapabilities.get(vcapName);
+ }
+
+ @Override
+ public int getVirtualCapabilityAsInt(String s) throws VirtualCapabilityNotDefinedException,
+ CapabilityNotDefinedException, NumberFormatException {
+ return 0;
+ }
+
+ @Override
+ public boolean getVirtualCapabilityAsBool(String vcapName) throws VirtualCapabilityNotDefinedException,
+ CapabilityNotDefinedException, NumberFormatException {
+
+ if (vcapName.equals("is_phone") || vcapName.equals("is_full_desktop") || vcapName.equals("is_connected_tv")
+ || vcapName.equals("is_mobile") || vcapName.equals("is_tablet")) {
+ return Boolean.parseBoolean(getVirtualCapability(vcapName));
+ }
+
+ return false;
+ }
+
+ @Override
+ public Map getVirtualCapabilities() {
+ return Map.of();
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public String getWURFLUserAgent() {
+ return "";
+ }
+
+ @Override
+ public String getCapability(String capName) throws CapabilityNotDefinedException {
+
+ if (!capabilities.containsKey(capName)) {
+ throw new CapabilityNotDefinedException(capName);
+ }
+
+ return capabilities.get(capName);
+
+ }
+
+ @Override
+ public int getCapabilityAsInt(String capName) throws CapabilityNotDefinedException, NumberFormatException {
+ return switch (capName) {
+ case "resolution_height", "resolution_width" -> Integer.parseInt(capabilities.get(capName));
+ default -> 0;
+ };
+ }
+
+ @Override
+ public boolean getCapabilityAsBool(String capName) throws CapabilityNotDefinedException, NumberFormatException {
+ return switch (capName) {
+ case "ajax_support_javascript", "is_connected_tv", "is_ott", "is_tablet", "is_mobile" ->
+ Boolean.parseBoolean(getCapability(capName));
+ default -> false;
+ };
+ }
+
+ @Override
+ public Map getCapabilities() {
+ return Map.of();
+ }
+
+ @Override
+ public boolean isActualDeviceRoot() {
+ return true;
+ }
+
+ @Override
+ public String getDeviceRootId() {
+ return "";
+ }
+
+ public static class WURFLDeviceMockFactory {
+
+ public static com.scientiamobile.wurfl.core.Device mockIPhone() {
+
+ return builder().capabilities(Map.of(
+ "brand_name", "Apple",
+ "model_name", "iPhone",
+ "ajax_support_javascript", "true",
+ "density_class", "1.0",
+ "is_connected_tv", "false",
+ "is_ott", "false",
+ "is_tablet", "false",
+ "resolution_height", "1440",
+ "resolution_width", "3200"
+ )).virtualCapabilities(
+ Map.of("advertised_device_os", "iOS",
+ "advertised_device_os_version", "17.1",
+ "complete_device_name", "Apple iPhone",
+ "is_full_desktop", "false",
+ "is_mobile", "true",
+ "is_phone", "true",
+ "form_factor", "Smartphone",
+ "pixel_density", "515"))
+ .id("apple_iphone_ver1")
+ .build();
+ }
+
+ public static com.scientiamobile.wurfl.core.Device mockOttDevice() {
+
+ return builder().capabilities(Map.of(
+ "brand_name", "Diyomate",
+ "model_name", "A6",
+ "ajax_support_javascript", "true",
+ "density_class", "1.5",
+ "is_connected_tv", "false",
+ "is_ott", "true",
+ "is_tablet", "false",
+ "resolution_height", "1080",
+ "resolution_width", "1920"
+ )).virtualCapabilities(
+ Map.of("advertised_device_os", "Android",
+ "advertised_device_os_version", "4.0",
+ "complete_device_name", "Diyomate A6",
+ "is_full_desktop", "false",
+ "is_mobile", "false",
+ "is_phone", "false",
+ "form_factor", "Smart-TV",
+ "pixel_density", "69"))
+ .id("diyomate_a6_ver1")
+ .build();
+ }
+
+ public static com.scientiamobile.wurfl.core.Device mockMobileUndefinedDevice() {
+
+ return builder().capabilities(Map.of(
+ "brand_name", "TestUnd",
+ "model_name", "U1",
+ "ajax_support_javascript", "false",
+ "density_class", "1.0",
+ "is_connected_tv", "false",
+ "is_ott", "false",
+ "is_tablet", "false",
+ "resolution_height", "128",
+ "resolution_width", "128"
+ )).virtualCapabilities(
+ Map.of("advertised_device_os", "TestOS",
+ "advertised_device_os_version", "1.0",
+ "complete_device_name", "TestUnd U1",
+ "is_full_desktop", "false",
+ "is_mobile", "true",
+ "is_phone", "false",
+ "form_factor", "Test-non-phone",
+ "pixel_density", "69"))
+ .build();
+ }
+
+ public static com.scientiamobile.wurfl.core.Device mockUnknownDevice() {
+
+ return builder().capabilities(Map.of(
+ "brand_name", "TestUnd",
+ "model_name", "U1",
+ "ajax_support_javascript", "false",
+ "density_class", "1.0",
+ "is_connected_tv", "false",
+ "is_ott", "false",
+ "is_tablet", "false",
+ "resolution_height", "128",
+ "resolution_width", "128"
+ )).virtualCapabilities(
+ Map.of("advertised_device_os", "TestOS",
+ "advertised_device_os_version", "1.0",
+ "complete_device_name", "TestUnd U1",
+ "is_full_desktop", "false",
+ "is_mobile", "false",
+ "is_phone", "false",
+ "form_factor", "Test-unknown",
+ "pixel_density", "69"))
+ .build();
+ }
+
+ public static com.scientiamobile.wurfl.core.Device mockDesktop() {
+
+ return builder().capabilities(Map.of(
+ "brand_name", "TestDesktop",
+ "model_name", "D1",
+ "ajax_support_javascript", "true",
+ "density_class", "1.5",
+ "is_connected_tv", "false",
+ "is_ott", "false",
+ "is_tablet", "false",
+ "resolution_height", "1080",
+ "resolution_width", "1920"
+ )).virtualCapabilities(
+ Map.of("advertised_device_os", "Windows",
+ "advertised_device_os_version", "10",
+ "complete_device_name", "TestDesktop D1",
+ "is_full_desktop", "true",
+ "is_mobile", "false",
+ "is_phone", "false",
+ "form_factor", "Desktop",
+ "pixel_density", "300"))
+ .build();
+ }
+
+ public static com.scientiamobile.wurfl.core.Device mockConnectedTv() {
+
+ return builder().capabilities(Map.of(
+ "brand_name", "TestConnectedTv",
+ "model_name", "C1",
+ "ajax_support_javascript", "true",
+ "density_class", "1.5",
+ "is_connected_tv", "true",
+ "is_ott", "false",
+ "is_tablet", "false",
+ "resolution_height", "1080",
+ "resolution_width", "1920"
+ )).virtualCapabilities(
+ Map.of("advertised_device_os", "WebOS",
+ "advertised_device_os_version", "4",
+ "complete_device_name", "TestConnectedTV C1",
+ "is_full_desktop", "false",
+ "is_mobile", "false",
+ "is_phone", "false",
+ "form_factor", "Smart-TV",
+ "pixel_density", "200"))
+ .build();
+ }
+
+ public static com.scientiamobile.wurfl.core.Device mockTablet() {
+
+ return builder().capabilities(Map.of(
+ "brand_name", "Samsung",
+ "model_name", "Galaxy Tab S9+",
+ "ajax_support_javascript", "true",
+ "density_class", "1.5",
+ "is_connected_tv", "false",
+ "is_ott", "false",
+ "is_tablet", "true",
+ "resolution_height", "1752",
+ "resolution_width", "2800"
+ )).virtualCapabilities(
+ Map.of("advertised_device_os", "Android",
+ "advertised_device_os_version", "13",
+ "complete_device_name", "Samsung Galaxy Tab S9+",
+ "is_full_desktop", "false",
+ "is_mobile", "false",
+ "is_phone", "false",
+ "form_factor", "Tablet",
+ "pixel_density", "274"))
+ .build();
+ }
+
+ }
+}
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..15a89de4ecf
--- /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;
+
+class AuctionRequestHeadersContextTest {
+
+ @Test
+ void fromShouldHandleNullHeaders() {
+ // when
+ final AuctionRequestHeadersContext result = AuctionRequestHeadersContext.from(null);
+
+ // then
+ assertThat(result.headers).isEmpty();
+ }
+
+ @Test
+ 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.headers)
+ .hasSize(2)
+ .containsEntry("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test")
+ .containsEntry("Header2", "value2");
+ }
+
+ @Test
+ 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.headers)
+ .hasSize(1)
+ .containsEntry("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test");
+ }
+
+ @Test
+ void fromShouldHandleEmptyMultiMap() {
+ // given
+ final CaseInsensitiveMultiMap emptyMultiMap = CaseInsensitiveMultiMap.empty();
+
+ // when
+ final AuctionRequestHeadersContext target = AuctionRequestHeadersContext.from(emptyMultiMap);
+
+ // then
+ assertThat(target.headers).isEmpty();
+ }
+}
diff --git a/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineInitializerTest.java b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineInitializerTest.java
new file mode 100644
index 00000000000..76d27053e75
--- /dev/null
+++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineInitializerTest.java
@@ -0,0 +1,125 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model;
+
+import com.scientiamobile.wurfl.core.GeneralWURFLEngine;
+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.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.exc.WURFLModuleConfigurationException;
+import org.junit.jupiter.api.function.Executable;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mock.Strictness.LENIENT;
+import static org.mockito.Mockito.mockStatic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class WURFLEngineInitializerTest {
+
+ @Mock(strictness = LENIENT)
+ private WURFLDeviceDetectionConfigProperties configProperties;
+
+ @Mock(strictness = LENIENT)
+ private WURFLEngine wurflEngine;
+
+ @BeforeEach
+ void setUp() {
+ when(configProperties.getWurflSnapshotUrl()).thenReturn("http://test.url/wurfl.zip");
+ when(configProperties.getWurflFileDirPath()).thenReturn("/test/path");
+ }
+
+ @Test
+ void downloadWurflFileIfNeededShouldDownloadWhenUrlAndPathArePresent() {
+ try (MockedStatic mockedStatic = mockStatic(GeneralWURFLEngine.class)) {
+ // when
+ WURFLEngineInitializer.downloadWurflFile(configProperties);
+
+ // then
+ mockedStatic.verify(() ->
+ GeneralWURFLEngine.wurflDownload("http://test.url/wurfl.zip", "/test/path"));
+ }
+ }
+
+ @Test
+ void verifyStaticCapabilitiesDefinitionShouldThrowExceptionWhenCapabilitiesAreNotDefined() {
+ // given
+ when(wurflEngine.getAllCapabilities()).thenReturn(Set.of(
+ "brand_name",
+ "density_class",
+ "is_connected_tv",
+ "is_ott",
+ "is_tablet",
+ "model_name"));
+
+ final String expFailedCheckMessage = """
+ 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(",", List.of(
+ "ajax_support_javascript",
+ "physical_form_factor",
+ "resolution_height",
+ "resolution_width"
+ )));
+
+ // when
+ final Executable exceptionSource = () -> WURFLEngineInitializer.verifyStaticCapabilitiesDefinition(wurflEngine);
+
+ // then
+ final Exception exception = assertThrows(WURFLModuleConfigurationException.class, exceptionSource);
+ assertThat(exception.getMessage()).isEqualTo(expFailedCheckMessage);
+ }
+
+ @Test
+ void verifyStaticCapabilitiesDefinitionShouldCompleteSuccessfullyWhenCapabilitiesAreDefined() {
+ // given
+ when(wurflEngine.getAllCapabilities()).thenReturn(Set.of(
+ "brand_name",
+ "density_class",
+ "is_connected_tv",
+ "is_ott",
+ "is_tablet",
+ "model_name",
+ "resolution_width",
+ "resolution_height",
+ "physical_form_factor",
+ "ajax_support_javascript"
+ ));
+
+ // when
+ var excOccurred = false;
+ try {
+ WURFLEngineInitializer.verifyStaticCapabilitiesDefinition(wurflEngine);
+ } catch (Exception e) {
+ excOccurred = true;
+ }
+
+ // then
+ assertThat(excOccurred).isFalse();
+ }
+
+ @Test
+ void builderShouldCreateWURFLEngineInitializerBuilderFromProperties() {
+ // given
+ when(configProperties.getWurflSnapshotUrl()).thenReturn("http://test.url/wurfl.zip");
+ when(configProperties.getWurflFileDirPath()).thenReturn("/test/path");
+ when(configProperties.getCacheSize()).thenReturn(1000);
+ when(configProperties.isWurflRunUpdater()).thenReturn(true);
+
+ // when
+ final var builder = WURFLEngineInitializer.builder()
+ .configProperties(configProperties);
+
+ // then
+ assertThat(builder).isNotNull();
+ assertThat(builder.build()).isNotNull();
+ assertThat(builder.toString()).isNotEmpty();
+ }
+}
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..f3d3be006fa
--- /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,199 @@
+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.BeforeEach;
+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;
+
+class HeadersResolverTest {
+
+ private HeadersResolver target;
+
+ private static final String TEST_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
+
+ @BeforeEach
+ void setUp() {
+ target = new HeadersResolver();
+ }
+
+ @Test
+ void resolveWithNullDeviceShouldReturnOriginalHeaders() {
+ // given
+ final Map headers = new HashMap<>();
+ headers.put("test", "value");
+
+ // when
+ final Map result = target.resolve(null, headers);
+
+ // then
+ assertThat(result).isEqualTo(headers);
+ }
+
+ @Test
+ 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 = target.resolve(device, new HashMap<>());
+
+ // then
+ assertThat(result).containsEntry("User-Agent", TEST_USER_AGENT);
+ }
+
+ @Test
+ 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 = target.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
+ 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 = target.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
+ void versionFromTokensShouldHandleNullAndEmptyInput() {
+ // when & then
+ assertThat(HeadersResolver.versionFromTokens(null)).isEmpty();
+ assertThat(HeadersResolver.versionFromTokens(List.of())).isEmpty();
+ }
+
+ @Test
+ void versionFromTokensShouldJoinValidTokens() {
+ // given
+ final List tokens = Arrays.asList("100", "0", "1234");
+
+ // when
+ final String result = HeadersResolver.versionFromTokens(tokens);
+
+ // then
+ assertThat(result).isEqualTo("100.0.1234");
+ }
+
+ @Test
+ 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 = target.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
+ void resolveWithNullDeviceAndNullHeadersShouldReturnEmptyMap() {
+ // when
+ final Map result = target.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/resolver/PlatformNameVersionTest.java b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/PlatformNameVersionTest.java
new file mode 100644
index 00000000000..666e4571908
--- /dev/null
+++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/PlatformNameVersionTest.java
@@ -0,0 +1,69 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.resolver;
+
+import com.iab.openrtb.request.BrandVersion;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class PlatformNameVersionTest {
+
+ @Test
+ void fromShouldReturnNullWhenPlatformIsNull() {
+ // when
+ final PlatformNameVersion target = PlatformNameVersion.from(null);
+
+ // then
+ assertThat(target).isNull();
+ }
+
+ @Test
+ void fromShouldCreatePlatformNameVersionWithValidInput() {
+ // given
+ final BrandVersion platform = new BrandVersion("Windows",
+ Arrays.asList("10", "0", "0"),
+ null);
+
+ // when
+ final PlatformNameVersion target = PlatformNameVersion.from(platform);
+
+ // then
+ assertThat(target).isNotNull();
+ assertThat(target.getPlatformName()).isEqualTo("Windows");
+ assertThat(target.getPlatformVersion()).isEqualTo("10.0.0");
+ }
+
+ @Test
+ void toStringShouldReturnFormattedString() {
+ // given
+ final BrandVersion platform = new BrandVersion("macOS",
+ Arrays.asList("13", "1"),
+ null);
+ final PlatformNameVersion target = PlatformNameVersion.from(platform);
+
+ // when
+ final String result = target.toString();
+
+ // then
+ assertThat(result).isEqualTo("macOS 13.1");
+ }
+
+ @Test
+ void fromShouldHandleEmptyVersionList() {
+ // given
+ final BrandVersion platform = new BrandVersion("Linux",
+ List.of(),
+ null);
+
+ // when
+ final PlatformNameVersion target = PlatformNameVersion.from(platform);
+
+ // then
+ assertThat(target).isNotNull();
+ assertThat(target.getPlatformName()).isEqualTo("Linux");
+ assertThat(target.getPlatformVersion()).isEmpty();
+ assertThat(target.toString()).isEqualTo("Linux ");
+ }
+}
diff --git a/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/AccountValidatorTest.java b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/AccountValidatorTest.java
new file mode 100644
index 00000000000..a21e2fca77b
--- /dev/null
+++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/AccountValidatorTest.java
@@ -0,0 +1,110 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1;
+
+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 java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class AccountValidatorTest {
+
+ private AuctionContext auctionContext;
+
+ @Mock
+ private Account account;
+
+ private AccountValidator validator;
+
+ @BeforeEach
+ void setUp() {
+ auctionContext = AuctionContext.builder().account(account).build();
+ }
+
+ @Test
+ void isAccountValidShouldReturnTrueWhenPublisherIdIsAllowed() {
+ // given
+ when(account.getId()).thenReturn("allowed-publisher");
+ final var accountValidatorBuiler = AccountValidator.builder()
+ .allowedPublisherIds(Collections.singletonMap("allowed-publisher", "allowed-publisher"))
+ .auctionContext(auctionContext);
+ assertThat(accountValidatorBuiler.toString()).isNotNull();
+ validator = accountValidatorBuiler.build();
+
+ // when
+ final boolean result = validator.isAccountValid();
+
+ // then
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void isAccountValidShouldReturnFalseWhenPublisherIdIsNotAllowed() {
+ // given
+ when(account.getId()).thenReturn("unknown-publisher");
+ validator = AccountValidator.builder()
+ .allowedPublisherIds(Collections.singletonMap("allowed-publisher", "allowed-publisher"))
+ .auctionContext(auctionContext)
+ .build();
+
+ // when
+ final boolean result = validator.isAccountValid();
+
+ // then
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ void isAccountValidShouldReturnFalseWhenAuctionContextIsNull() {
+ // given
+ validator = AccountValidator.builder()
+ .allowedPublisherIds(Collections.singletonMap("allowed-publisher", "allowed-publisher"))
+ .auctionContext(null)
+ .build();
+
+ // when
+ final boolean result = validator.isAccountValid();
+
+ // then
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ void isAccountValidShouldReturnFalseWhenPublisherIdIsEmpty() {
+ // given
+ when(account.getId()).thenReturn("");
+ validator = AccountValidator.builder()
+ .allowedPublisherIds(Collections.singletonMap("allowed-publisher", "allowed-publisher"))
+ .auctionContext(auctionContext)
+ .build();
+
+ // when
+ final boolean result = validator.isAccountValid();
+
+ // then
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ void isAccountValidShouldReturnFalseWhenAccountIsNull() {
+ // given
+ when(auctionContext.getAccount()).thenReturn(null);
+ validator = AccountValidator.builder()
+ .allowedPublisherIds(Collections.singletonMap("allowed-publisher", "allowed-publisher"))
+ .auctionContext(auctionContext)
+ .build();
+
+ // when
+ final boolean result = validator.isAccountValid();
+
+ // then
+ assertThat(result).isFalse();
+ }
+}
diff --git a/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/ExtWURFLMapperTest.java b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/ExtWURFLMapperTest.java
new file mode 100644
index 00000000000..8e717fcf1ce
--- /dev/null
+++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/ExtWURFLMapperTest.java
@@ -0,0 +1,131 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.iab.openrtb.request.Device;
+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 java.util.Arrays;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class ExtWURFLMapperTest {
+
+ @Mock
+ private com.scientiamobile.wurfl.core.Device wurflDevice;
+
+ @Mock
+ private Device device;
+
+ private ExtWURFLMapper target;
+ private List staticCaps;
+ private List virtualCaps;
+
+ @BeforeEach
+ public void setUp() {
+ staticCaps = Arrays.asList("brand_name", "model_name");
+ virtualCaps = Arrays.asList("is_mobile", "form_factor");
+
+ target = ExtWURFLMapper.builder()
+ .staticCaps(staticCaps)
+ .virtualCaps(virtualCaps)
+ .wurflDevice(wurflDevice)
+ .addExtCaps(true)
+ .build();
+ }
+
+ @Test
+ public void shouldMapStaticCapabilities() {
+ // given
+ when(wurflDevice.getCapability("brand_name")).thenReturn("Apple");
+ when(wurflDevice.getCapability("model_name")).thenReturn("iPhone");
+
+ // when
+ final JsonNode result = target.mapExtProperties();
+
+ // then
+ assertThat(result.get("brand_name").asText()).isEqualTo("Apple");
+ assertThat(result.get("model_name").asText()).isEqualTo("iPhone");
+ }
+
+ @Test
+ public void shouldMapVirtualCapabilities() {
+ // given
+ when(wurflDevice.getVirtualCapability("is_mobile")).thenReturn("true");
+ when(wurflDevice.getVirtualCapability("form_factor")).thenReturn("smartphone");
+
+ // when
+ final JsonNode result = target.mapExtProperties();
+
+ // then
+ assertThat(result.get("is_mobile").asText()).isEqualTo("true");
+ assertThat(result.get("form_factor").asText()).isEqualTo("smartphone");
+ }
+
+ @Test
+ public void shouldMapWURFLId() {
+ // given
+ when(wurflDevice.getId()).thenReturn("test_wurfl_id");
+
+ // when
+ final JsonNode result = target.mapExtProperties();
+
+ // then
+ assertThat(result.get("wurfl_id").asText()).isEqualTo("test_wurfl_id");
+ }
+
+ @Test
+ public void shouldSkipNullCapabilities() {
+ // given
+ when(wurflDevice.getCapability("brand_name")).thenReturn(null);
+ when(wurflDevice.getCapability("model_name")).thenReturn("iPhone");
+ when(wurflDevice.getVirtualCapability("is_mobile")).thenReturn(null);
+
+ // when
+ final JsonNode result = target.mapExtProperties();
+
+ // then
+ assertThat(result.has("brand_name")).isFalse();
+ assertThat(result.get("model_name").asText()).isEqualTo("iPhone");
+ assertThat(result.has("is_mobile")).isFalse();
+ }
+
+ @Test
+ public void shouldHandleExceptionsGracefully() {
+ // given
+ when(wurflDevice.getCapability("brand_name")).thenThrow(new RuntimeException("Test exception"));
+ when(wurflDevice.getCapability("model_name")).thenReturn("iPhone");
+
+ // when
+ final JsonNode result = target.mapExtProperties();
+
+ // then
+ assertThat(result.has("brand_name")).isFalse();
+ assertThat(result.get("model_name")).isNotNull();
+ assertThat(result.get("model_name").asText()).isEqualTo("iPhone");
+ }
+
+ @Test
+ public void shouldNotAddExtCapsIfDisabled() {
+ // given
+ target = ExtWURFLMapper.builder()
+ .staticCaps(staticCaps)
+ .virtualCaps(virtualCaps)
+ .wurflDevice(wurflDevice)
+ .addExtCaps(false)
+ .build();
+
+ // when
+ final JsonNode result = target.mapExtProperties();
+
+ // then
+ assertThat(result.has("brand_name")).isFalse();
+ assertThat(result.has("model_name")).isFalse();
+ }
+}
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..f0f60f43383
--- /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,243 @@
+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.prebid.server.proto.openrtb.ext.request.ExtDevice;
+import lombok.extern.slf4j.Slf4j;
+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.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock.WURFLDeviceMockFactory.mockConnectedTv;
+import static org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock.WURFLDeviceMockFactory.mockDesktop;
+import static org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock.WURFLDeviceMockFactory.mockIPhone;
+import static org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock.WURFLDeviceMockFactory.mockMobileUndefinedDevice;
+import static org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock.WURFLDeviceMockFactory.mockOttDevice;
+import static org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock.WURFLDeviceMockFactory.mockTablet;
+import static org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock.WURFLDeviceMockFactory.mockUnknownDevice;
+
+@Slf4j
+@ExtendWith(MockitoExtension.class)
+class OrtbDeviceUpdaterTest {
+
+ private OrtbDeviceUpdater target;
+ private List staticCaps;
+ private List virtualCaps;
+
+ @BeforeEach
+ void setUp() {
+ target = new OrtbDeviceUpdater();
+ staticCaps = Arrays.asList("ajax_support_javascript", "brand_name", "density_class",
+ "is_connected_tv", "is_ott", "is_tablet", "model_name", "resolution_height", "resolution_width");
+ virtualCaps = Arrays.asList("advertised_device_os", "advertised_device_os_version",
+ "is_full_desktop", "pixel_density");
+ }
+
+ @Test
+ void updateShouldUpdateDeviceMakeWhenOriginalIsEmpty() {
+ // given
+ final var wurflDevice = mockIPhone();
+ final Device device = Device.builder().build();
+
+ // when
+ final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true);
+
+ // then
+ assertThat(result.getMake()).isEqualTo("Apple");
+ assertThat(result.getDevicetype()).isEqualTo(1);
+ }
+
+ @Test
+ void updateShouldNotUpdateDeviceMakeWhenOriginalExists() {
+ // given
+ final Device device = Device.builder().make("Samsung").build();
+ final var wurflDevice = mockIPhone();
+
+ // when
+ final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true);
+
+ // then
+ assertThat(result.getMake()).isEqualTo("Samsung");
+ }
+
+ @Test
+ void updateShouldNotUpdateDeviceMakeWhenOriginalBigIntegerExists() {
+ // given
+ final Device device = Device.builder().make("Apple").pxratio(new BigDecimal("1.0")).build();
+ final var wurflDevice = mockIPhone();
+
+ // when
+ final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true);
+
+ // then
+ assertThat(result.getMake()).isEqualTo("Apple");
+ assertThat(result.getPxratio()).isEqualTo("1.0");
+ }
+
+ @Test
+ void updateShouldUpdateDeviceModelWhenOriginalIsEmpty() {
+ // given
+ final Device device = Device.builder().build();
+ final var wurflDevice = mockIPhone();
+
+ // when
+ final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true);
+
+ // then
+ assertThat(result.getModel()).isEqualTo("iPhone");
+ }
+
+ @Test
+ void updateShouldUpdateDeviceOsWhenOriginalIsEmpty() {
+ // given
+ final Device device = Device.builder().build();
+ final var wurflDevice = mockIPhone();
+
+ // when
+ final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true);
+
+ // then
+ assertThat(result.getOs()).isEqualTo("iOS");
+ }
+
+ @Test
+ void updateShouldUpdateResolutionWhenOriginalIsEmpty() {
+ // given
+ final Device device = Device.builder().build();
+ final var wurflDevice = mockIPhone();
+
+ // when
+ final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true);
+
+ // then
+ assertThat(result.getW()).isEqualTo(3200);
+ assertThat(result.getH()).isEqualTo(1440);
+ }
+
+ @Test
+ void updateShouldHandleJavascriptSupport() {
+ // given
+ final Device device = Device.builder().build();
+ final var wurflDevice = mockIPhone();
+
+ // when
+ final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true);
+
+ // then
+ assertThat(result.getJs()).isEqualTo(1);
+ }
+
+ @Test
+ void updateShouldHandleOttDeviceType() {
+ // given
+ final Device device = Device.builder().build();
+ final var wurflDevice = mockOttDevice();
+ // when
+ final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true);
+
+ // then
+ assertThat(result.getDevicetype()).isEqualTo(7);
+ }
+
+ @Test
+ void updateShouldReturnDeviceOtherMobileWhenMobileIsNotPhoneOrTablet() {
+ // given
+ final Device device = Device.builder().build();
+ final var wurflDevice = mockMobileUndefinedDevice();
+ // when
+ final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true);
+ // then
+ assertThat(result.getDevicetype()).isEqualTo(6);
+ }
+
+ @Test
+ void updateShouldReturnNullWhenMobileTypeIsUnknown() {
+ // given
+ final Device device = Device.builder().build();
+ final var wurflDevice = mockUnknownDevice();
+ // when
+ final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true);
+ // then
+ assertThat(result.getDevicetype()).isNull();
+ }
+
+ @Test
+ void updateShouldHandlePersonalComputerDeviceType() {
+ // given
+ final Device device = Device.builder().build();
+ final var wurflDevice = mockDesktop();
+ // when
+ final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true);
+ // then
+ assertThat(result.getDevicetype()).isEqualTo(2);
+ }
+
+ @Test
+ void updateShouldHandleConnectedTvDeviceType() {
+ // given
+ final Device device = Device.builder().build();
+ final var wurflDevice = mockConnectedTv();
+ // when
+ final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true);
+ // then
+ assertThat(result.getDevicetype()).isEqualTo(3);
+ }
+
+ @Test
+ void updateShouldNotUpdateDeviceTypeWhenSet() {
+ // given
+ final Device device = Device.builder()
+ .devicetype(3)
+ .build();
+ final var wurflDevice = mockDesktop(); // device type 2
+ // when
+ final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true);
+ // then
+ assertThat(result.getDevicetype()).isEqualTo(3); // unchanged
+ }
+
+ @Test
+ void updateShouldHandleTabletDeviceType() {
+ // given
+ final Device device = Device.builder().build();
+ final var wurflDevice = mockTablet();
+ // when
+ final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true);
+ // then
+ assertThat(result.getDevicetype()).isEqualTo(5);
+ }
+
+ @Test
+ void updateShouldAddWurflPropertyToExtIfMissingAndPreserveExistingProperties() {
+ // given
+ final ExtDevice existingExt = ExtDevice.empty();
+ existingExt.addProperty("someProperty", TextNode.valueOf("value"));
+ final Device device = Device.builder()
+ .ext(existingExt)
+ .build();
+
+ final var wurflDevice = WURFLDeviceMock.WURFLDeviceMockFactory.mockIPhone();
+ final List staticCaps = List.of("brand_name");
+ final List virtualCaps = List.of("advertised_device_os");
+
+ final OrtbDeviceUpdater updater = new OrtbDeviceUpdater();
+
+ // when
+ final Device result = updater.update(device, wurflDevice, staticCaps, virtualCaps, true);
+
+ // then
+ final ExtDevice resultExt = result.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..b648015637e
--- /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,81 @@
+package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1;
+
+import io.vertx.core.Future;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+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.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class WURFLDeviceDetectionEntrypointHookTest {
+
+ private EntrypointPayload payload;
+ private InvocationContext context;
+
+ @BeforeEach
+ void setUp() {
+ payload = mock(EntrypointPayload.class);
+ context = mock(InvocationContext.class);
+ }
+
+ @Test
+ void codeShouldReturnCorrectHookCode() {
+
+ // given
+ final WURFLDeviceDetectionEntrypointHook target = new WURFLDeviceDetectionEntrypointHook();
+
+ // when
+ final String result = target.code();
+
+ // then
+ assertThat(result).isEqualTo("wurfl-devicedetection-entrypoint-hook");
+ }
+
+ @Test
+ 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();
+ when(payload.headers()).thenReturn(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
+ void callShouldHandleNullHeaders() {
+ // given
+ final WURFLDeviceDetectionEntrypointHook target = new WURFLDeviceDetectionEntrypointHook();
+
+ // when
+ when(payload.headers()).thenReturn(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..b2d36390f52
--- /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;
+
+class WURFLDeviceDetectionModuleTest {
+
+ @Test
+ 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
+ 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..5b5a1d01f74
--- /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,124 @@
+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.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties;
+import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock;
+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 static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class WURFLDeviceDetectionRawAuctionRequestHookTest {
+
+ @Mock
+ private WURFLEngine wurflEngine;
+
+ @Mock
+ private WURFLDeviceDetectionConfigProperties configProperties;
+
+ @Mock
+ private AuctionRequestPayload payload;
+
+ @Mock
+ private AuctionInvocationContext context;
+
+ private WURFLDeviceDetectionRawAuctionRequestHook target;
+
+ @BeforeEach
+ void setUp() {
+
+ target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflEngine, configProperties);
+ }
+
+ @Test
+ void codeShouldReturnCorrectHookCode() {
+ // when
+ final String result = target.code();
+
+ // then
+ assertThat(result).isEqualTo("wurfl-devicedetection-raw-auction-request");
+ }
+
+ @Test
+ void callShouldReturnNoActionWhenBidRequestIsNull() {
+ // given
+ when(payload.bidRequest()).thenReturn(null);
+
+ // when
+ final InvocationResult result = target.call(payload, context).result();
+
+ // then
+ assertThat(result.status()).isEqualTo(InvocationStatus.success);
+ assertThat(result.action()).isEqualTo(InvocationAction.no_action);
+ }
+
+ @Test
+ void callShouldReturnNoActionWhenDeviceIsNull() {
+ // given
+ final BidRequest bidRequest = BidRequest.builder().build();
+ when(payload.bidRequest()).thenReturn(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
+ void callShouldUpdateDeviceWhenWurflDeviceIsDetected() {
+ // 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();
+ when(payload.bidRequest()).thenReturn(bidRequest);
+
+ final CaseInsensitiveMultiMap headers = CaseInsensitiveMultiMap.builder()
+ .add("User-Agent", ua)
+ .build();
+ final AuctionRequestHeadersContext headersContext = AuctionRequestHeadersContext.from(headers);
+
+ // when
+ when(context.moduleContext()).thenReturn(headersContext);
+ final var wurflDevice = WURFLDeviceMock.WURFLDeviceMockFactory.mockIPhone();
+ when(wurflEngine.getDeviceForRequest(any(Map.class))).thenReturn(wurflDevice);
+
+ final InvocationResult result = target.call(payload, context).result();
+
+ // then
+ assertThat(result.status()).isEqualTo(InvocationStatus.success);
+ assertThat(result.action()).isEqualTo(InvocationAction.update);
+ }
+
+ @Test
+ void shouldEnrichDeviceWhenAllowedPublisherIdsIsEmpty() {
+ // given
+ when(configProperties.getAllowedPublisherIds()).thenReturn(Collections.emptyList());
+ target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflEngine, configProperties);
+
+ // when
+ final InvocationResult result = target.call(payload, context).result();
+
+ // then
+ assertThat(result.status()).isEqualTo(InvocationStatus.success);
+ }
+}
diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml
index d6bdcf20c34..c16e5072d26 100644
--- a/extra/modules/pom.xml
+++ b/extra/modules/pom.xml
@@ -24,6 +24,7 @@
pb-response-correction
greenbids-real-time-data
pb-request-correction
+
diff --git a/sample/configs/prebid-config-with-wurfl.yaml b/sample/configs/prebid-config-with-wurfl.yaml
new file mode 100644
index 00000000000..c9f6b1d63d2
--- /dev/null
+++ b/sample/configs/prebid-config-with-wurfl.yaml
@@ -0,0 +1,80 @@
+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"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+