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